summaryrefslogtreecommitdiff
path: root/venv/lib/python3.11/site-packages/sqlalchemy/orm
diff options
context:
space:
mode:
authorcyfraeviolae <cyfraeviolae>2024-04-03 03:10:44 -0400
committercyfraeviolae <cyfraeviolae>2024-04-03 03:10:44 -0400
commit6d7ba58f880be618ade07f8ea080fe8c4bf8a896 (patch)
treeb1c931051ffcebd2bd9d61d98d6233ffa289bbce /venv/lib/python3.11/site-packages/sqlalchemy/orm
parent4f884c9abc32990b4061a1bb6997b4b37e58ea0b (diff)
venv
Diffstat (limited to 'venv/lib/python3.11/site-packages/sqlalchemy/orm')
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py170
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/__init__.cpython-311.pycbin0 -> 8557 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/_orm_constructors.cpython-311.pycbin0 -> 100035 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/_typing.cpython-311.pycbin0 -> 7834 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/attributes.cpython-311.pycbin0 -> 104505 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/base.cpython-311.pycbin0 -> 32806 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/bulk_persistence.cpython-311.pycbin0 -> 70614 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/clsregistry.cpython-311.pycbin0 -> 26896 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/collections.cpython-311.pycbin0 -> 68350 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/context.cpython-311.pycbin0 -> 103753 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/decl_api.cpython-311.pycbin0 -> 70976 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/decl_base.cpython-311.pycbin0 -> 76416 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/dependency.cpython-311.pycbin0 -> 44509 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/descriptor_props.cpython-311.pycbin0 -> 53451 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/dynamic.cpython-311.pycbin0 -> 14108 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/evaluator.cpython-311.pycbin0 -> 17661 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/events.cpython-311.pycbin0 -> 140389 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/exc.cpython-311.pycbin0 -> 11103 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/identity.cpython-311.pycbin0 -> 13935 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/instrumentation.cpython-311.pycbin0 -> 33762 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/interfaces.cpython-311.pycbin0 -> 56553 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/loading.cpython-311.pycbin0 -> 51952 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/mapped_collection.cpython-311.pycbin0 -> 23740 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/mapper.cpython-311.pycbin0 -> 175532 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/path_registry.cpython-311.pycbin0 -> 34743 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/persistence.cpython-311.pycbin0 -> 50760 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/properties.cpython-311.pycbin0 -> 34341 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/query.cpython-311.pycbin0 -> 132189 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/relationships.cpython-311.pycbin0 -> 135454 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/scoping.cpython-311.pycbin0 -> 84345 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/session.cpython-311.pycbin0 -> 205815 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/state.cpython-311.pycbin0 -> 47732 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/state_changes.cpython-311.pycbin0 -> 7462 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/strategies.cpython-311.pycbin0 -> 109480 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/strategy_options.cpython-311.pycbin0 -> 90575 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/sync.cpython-311.pycbin0 -> 6977 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/unitofwork.cpython-311.pycbin0 -> 37079 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/util.cpython-311.pycbin0 -> 92891 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/writeonly.cpython-311.pycbin0 -> 29722 bytes
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/_orm_constructors.py2471
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/_typing.py179
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/attributes.py2835
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py971
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/bulk_persistence.py2048
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/clsregistry.py570
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/collections.py1618
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/context.py3243
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_api.py1875
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py2152
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py1304
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/descriptor_props.py1074
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/dynamic.py298
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/evaluator.py368
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/events.py3259
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/exc.py228
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/identity.py302
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/instrumentation.py754
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py1469
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/loading.py1665
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/mapped_collection.py560
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py4420
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/path_registry.py808
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/persistence.py1782
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/properties.py886
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py3394
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py3500
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/scoping.py2165
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py5238
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/state.py1136
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py198
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/strategies.py3344
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/strategy_options.py2555
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/sync.py164
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/unitofwork.py796
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/util.py2416
-rw-r--r--venv/lib/python3.11/site-packages/sqlalchemy/orm/writeonly.py678
76 files changed, 62893 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py
new file mode 100644
index 0000000..70a1129
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py
@@ -0,0 +1,170 @@
+# orm/__init__.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""
+Functional constructs for ORM configuration.
+
+See the SQLAlchemy object relational tutorial and mapper configuration
+documentation for an overview of how this module is used.
+
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+from . import exc as exc
+from . import mapper as mapperlib
+from . import strategy_options as strategy_options
+from ._orm_constructors import _mapper_fn as mapper
+from ._orm_constructors import aliased as aliased
+from ._orm_constructors import backref as backref
+from ._orm_constructors import clear_mappers as clear_mappers
+from ._orm_constructors import column_property as column_property
+from ._orm_constructors import composite as composite
+from ._orm_constructors import contains_alias as contains_alias
+from ._orm_constructors import create_session as create_session
+from ._orm_constructors import deferred as deferred
+from ._orm_constructors import dynamic_loader as dynamic_loader
+from ._orm_constructors import join as join
+from ._orm_constructors import mapped_column as mapped_column
+from ._orm_constructors import orm_insert_sentinel as orm_insert_sentinel
+from ._orm_constructors import outerjoin as outerjoin
+from ._orm_constructors import query_expression as query_expression
+from ._orm_constructors import relationship as relationship
+from ._orm_constructors import synonym as synonym
+from ._orm_constructors import with_loader_criteria as with_loader_criteria
+from ._orm_constructors import with_polymorphic as with_polymorphic
+from .attributes import AttributeEventToken as AttributeEventToken
+from .attributes import InstrumentedAttribute as InstrumentedAttribute
+from .attributes import QueryableAttribute as QueryableAttribute
+from .base import class_mapper as class_mapper
+from .base import DynamicMapped as DynamicMapped
+from .base import InspectionAttrExtensionType as InspectionAttrExtensionType
+from .base import LoaderCallableStatus as LoaderCallableStatus
+from .base import Mapped as Mapped
+from .base import NotExtension as NotExtension
+from .base import ORMDescriptor as ORMDescriptor
+from .base import PassiveFlag as PassiveFlag
+from .base import SQLORMExpression as SQLORMExpression
+from .base import WriteOnlyMapped as WriteOnlyMapped
+from .context import FromStatement as FromStatement
+from .context import QueryContext as QueryContext
+from .decl_api import add_mapped_attribute as add_mapped_attribute
+from .decl_api import as_declarative as as_declarative
+from .decl_api import declarative_base as declarative_base
+from .decl_api import declarative_mixin as declarative_mixin
+from .decl_api import DeclarativeBase as DeclarativeBase
+from .decl_api import DeclarativeBaseNoMeta as DeclarativeBaseNoMeta
+from .decl_api import DeclarativeMeta as DeclarativeMeta
+from .decl_api import declared_attr as declared_attr
+from .decl_api import has_inherited_table as has_inherited_table
+from .decl_api import MappedAsDataclass as MappedAsDataclass
+from .decl_api import registry as registry
+from .decl_api import synonym_for as synonym_for
+from .decl_base import MappedClassProtocol as MappedClassProtocol
+from .descriptor_props import Composite as Composite
+from .descriptor_props import CompositeProperty as CompositeProperty
+from .descriptor_props import Synonym as Synonym
+from .descriptor_props import SynonymProperty as SynonymProperty
+from .dynamic import AppenderQuery as AppenderQuery
+from .events import AttributeEvents as AttributeEvents
+from .events import InstanceEvents as InstanceEvents
+from .events import InstrumentationEvents as InstrumentationEvents
+from .events import MapperEvents as MapperEvents
+from .events import QueryEvents as QueryEvents
+from .events import SessionEvents as SessionEvents
+from .identity import IdentityMap as IdentityMap
+from .instrumentation import ClassManager as ClassManager
+from .interfaces import EXT_CONTINUE as EXT_CONTINUE
+from .interfaces import EXT_SKIP as EXT_SKIP
+from .interfaces import EXT_STOP as EXT_STOP
+from .interfaces import InspectionAttr as InspectionAttr
+from .interfaces import InspectionAttrInfo as InspectionAttrInfo
+from .interfaces import MANYTOMANY as MANYTOMANY
+from .interfaces import MANYTOONE as MANYTOONE
+from .interfaces import MapperProperty as MapperProperty
+from .interfaces import NO_KEY as NO_KEY
+from .interfaces import NO_VALUE as NO_VALUE
+from .interfaces import ONETOMANY as ONETOMANY
+from .interfaces import PropComparator as PropComparator
+from .interfaces import RelationshipDirection as RelationshipDirection
+from .interfaces import UserDefinedOption as UserDefinedOption
+from .loading import merge_frozen_result as merge_frozen_result
+from .loading import merge_result as merge_result
+from .mapped_collection import attribute_keyed_dict as attribute_keyed_dict
+from .mapped_collection import (
+ attribute_mapped_collection as attribute_mapped_collection,
+)
+from .mapped_collection import column_keyed_dict as column_keyed_dict
+from .mapped_collection import (
+ column_mapped_collection as column_mapped_collection,
+)
+from .mapped_collection import keyfunc_mapping as keyfunc_mapping
+from .mapped_collection import KeyFuncDict as KeyFuncDict
+from .mapped_collection import mapped_collection as mapped_collection
+from .mapped_collection import MappedCollection as MappedCollection
+from .mapper import configure_mappers as configure_mappers
+from .mapper import Mapper as Mapper
+from .mapper import reconstructor as reconstructor
+from .mapper import validates as validates
+from .properties import ColumnProperty as ColumnProperty
+from .properties import MappedColumn as MappedColumn
+from .properties import MappedSQLExpression as MappedSQLExpression
+from .query import AliasOption as AliasOption
+from .query import Query as Query
+from .relationships import foreign as foreign
+from .relationships import Relationship as Relationship
+from .relationships import RelationshipProperty as RelationshipProperty
+from .relationships import remote as remote
+from .scoping import QueryPropertyDescriptor as QueryPropertyDescriptor
+from .scoping import scoped_session as scoped_session
+from .session import close_all_sessions as close_all_sessions
+from .session import make_transient as make_transient
+from .session import make_transient_to_detached as make_transient_to_detached
+from .session import object_session as object_session
+from .session import ORMExecuteState as ORMExecuteState
+from .session import Session as Session
+from .session import sessionmaker as sessionmaker
+from .session import SessionTransaction as SessionTransaction
+from .session import SessionTransactionOrigin as SessionTransactionOrigin
+from .state import AttributeState as AttributeState
+from .state import InstanceState as InstanceState
+from .strategy_options import contains_eager as contains_eager
+from .strategy_options import defaultload as defaultload
+from .strategy_options import defer as defer
+from .strategy_options import immediateload as immediateload
+from .strategy_options import joinedload as joinedload
+from .strategy_options import lazyload as lazyload
+from .strategy_options import Load as Load
+from .strategy_options import load_only as load_only
+from .strategy_options import noload as noload
+from .strategy_options import raiseload as raiseload
+from .strategy_options import selectin_polymorphic as selectin_polymorphic
+from .strategy_options import selectinload as selectinload
+from .strategy_options import subqueryload as subqueryload
+from .strategy_options import undefer as undefer
+from .strategy_options import undefer_group as undefer_group
+from .strategy_options import with_expression as with_expression
+from .unitofwork import UOWTransaction as UOWTransaction
+from .util import Bundle as Bundle
+from .util import CascadeOptions as CascadeOptions
+from .util import LoaderCriteriaOption as LoaderCriteriaOption
+from .util import object_mapper as object_mapper
+from .util import polymorphic_union as polymorphic_union
+from .util import was_deleted as was_deleted
+from .util import with_parent as with_parent
+from .writeonly import WriteOnlyCollection as WriteOnlyCollection
+from .. import util as _sa_util
+
+
+def __go(lcls: Any) -> None:
+ _sa_util.preloaded.import_prefix("sqlalchemy.orm")
+ _sa_util.preloaded.import_prefix("sqlalchemy.ext")
+
+
+__go(locals())
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..8a0b109
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/__init__.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/_orm_constructors.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/_orm_constructors.cpython-311.pyc
new file mode 100644
index 0000000..2fa0118
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/_orm_constructors.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/_typing.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/_typing.cpython-311.pyc
new file mode 100644
index 0000000..90d7d47
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/_typing.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/attributes.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/attributes.cpython-311.pyc
new file mode 100644
index 0000000..d534677
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/attributes.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/base.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/base.cpython-311.pyc
new file mode 100644
index 0000000..3333b81
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/base.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/bulk_persistence.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/bulk_persistence.cpython-311.pyc
new file mode 100644
index 0000000..858b764
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/bulk_persistence.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/clsregistry.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/clsregistry.cpython-311.pyc
new file mode 100644
index 0000000..929ab36
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/clsregistry.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/collections.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/collections.cpython-311.pyc
new file mode 100644
index 0000000..64bba3c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/collections.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/context.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/context.cpython-311.pyc
new file mode 100644
index 0000000..a9888f5
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/context.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/decl_api.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/decl_api.cpython-311.pyc
new file mode 100644
index 0000000..6aba2cb
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/decl_api.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/decl_base.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/decl_base.cpython-311.pyc
new file mode 100644
index 0000000..7fbe0f4
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/decl_base.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/dependency.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/dependency.cpython-311.pyc
new file mode 100644
index 0000000..de66fd6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/dependency.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/descriptor_props.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/descriptor_props.cpython-311.pyc
new file mode 100644
index 0000000..2d3c764
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/descriptor_props.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/dynamic.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/dynamic.cpython-311.pyc
new file mode 100644
index 0000000..af2c1c3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/dynamic.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/evaluator.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/evaluator.cpython-311.pyc
new file mode 100644
index 0000000..dd10afb
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/evaluator.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/events.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/events.cpython-311.pyc
new file mode 100644
index 0000000..383eaf6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/events.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/exc.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/exc.cpython-311.pyc
new file mode 100644
index 0000000..a9f7995
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/exc.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/identity.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/identity.cpython-311.pyc
new file mode 100644
index 0000000..799b3f1
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/identity.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/instrumentation.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/instrumentation.cpython-311.pyc
new file mode 100644
index 0000000..e2986cd
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/instrumentation.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/interfaces.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/interfaces.cpython-311.pyc
new file mode 100644
index 0000000..15154d7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/interfaces.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/loading.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/loading.cpython-311.pyc
new file mode 100644
index 0000000..c5396e8
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/loading.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/mapped_collection.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/mapped_collection.cpython-311.pyc
new file mode 100644
index 0000000..44aea8f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/mapped_collection.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/mapper.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/mapper.cpython-311.pyc
new file mode 100644
index 0000000..58c19aa
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/mapper.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/path_registry.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/path_registry.cpython-311.pyc
new file mode 100644
index 0000000..8d8ba5f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/path_registry.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/persistence.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/persistence.cpython-311.pyc
new file mode 100644
index 0000000..566049c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/persistence.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/properties.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/properties.cpython-311.pyc
new file mode 100644
index 0000000..3754ca1
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/properties.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/query.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/query.cpython-311.pyc
new file mode 100644
index 0000000..d2df40d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/query.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/relationships.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/relationships.cpython-311.pyc
new file mode 100644
index 0000000..c66e5a4
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/relationships.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/scoping.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/scoping.cpython-311.pyc
new file mode 100644
index 0000000..e815007
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/scoping.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/session.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/session.cpython-311.pyc
new file mode 100644
index 0000000..f051751
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/session.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/state.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/state.cpython-311.pyc
new file mode 100644
index 0000000..a70802e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/state.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/state_changes.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/state_changes.cpython-311.pyc
new file mode 100644
index 0000000..bb62b49
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/state_changes.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/strategies.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/strategies.cpython-311.pyc
new file mode 100644
index 0000000..57424ef
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/strategies.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/strategy_options.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/strategy_options.cpython-311.pyc
new file mode 100644
index 0000000..e1fd9c9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/strategy_options.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/sync.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/sync.cpython-311.pyc
new file mode 100644
index 0000000..b1d21cc
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/sync.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/unitofwork.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/unitofwork.cpython-311.pyc
new file mode 100644
index 0000000..433eae1
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/unitofwork.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/util.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/util.cpython-311.pyc
new file mode 100644
index 0000000..17dd961
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/util.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/writeonly.cpython-311.pyc b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/writeonly.cpython-311.pyc
new file mode 100644
index 0000000..c3a0b5b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/__pycache__/writeonly.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/_orm_constructors.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/_orm_constructors.py
new file mode 100644
index 0000000..7cb536b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/_orm_constructors.py
@@ -0,0 +1,2471 @@
+# orm/_orm_constructors.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+from __future__ import annotations
+
+import typing
+from typing import Any
+from typing import Callable
+from typing import Collection
+from typing import Iterable
+from typing import NoReturn
+from typing import Optional
+from typing import overload
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from . import mapperlib as mapperlib
+from ._typing import _O
+from .descriptor_props import Composite
+from .descriptor_props import Synonym
+from .interfaces import _AttributeOptions
+from .properties import MappedColumn
+from .properties import MappedSQLExpression
+from .query import AliasOption
+from .relationships import _RelationshipArgumentType
+from .relationships import _RelationshipDeclared
+from .relationships import _RelationshipSecondaryArgument
+from .relationships import RelationshipProperty
+from .session import Session
+from .util import _ORMJoin
+from .util import AliasedClass
+from .util import AliasedInsp
+from .util import LoaderCriteriaOption
+from .. import sql
+from .. import util
+from ..exc import InvalidRequestError
+from ..sql._typing import _no_kw
+from ..sql.base import _NoArg
+from ..sql.base import SchemaEventTarget
+from ..sql.schema import _InsertSentinelColumnDefault
+from ..sql.schema import SchemaConst
+from ..sql.selectable import FromClause
+from ..util.typing import Annotated
+from ..util.typing import Literal
+
+if TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _ORMColumnExprArgument
+ from .descriptor_props import _CC
+ from .descriptor_props import _CompositeAttrType
+ from .interfaces import PropComparator
+ from .mapper import Mapper
+ from .query import Query
+ from .relationships import _LazyLoadArgumentType
+ from .relationships import _ORMColCollectionArgument
+ from .relationships import _ORMOrderByArgument
+ from .relationships import _RelationshipJoinConditionArgument
+ from .relationships import ORMBackrefArgument
+ from .session import _SessionBind
+ from ..sql._typing import _AutoIncrementType
+ from ..sql._typing import _ColumnExpressionArgument
+ from ..sql._typing import _FromClauseArgument
+ from ..sql._typing import _InfoType
+ from ..sql._typing import _OnClauseArgument
+ from ..sql._typing import _TypeEngineArgument
+ from ..sql.elements import ColumnElement
+ from ..sql.schema import _ServerDefaultArgument
+ from ..sql.schema import FetchedValue
+ from ..sql.selectable import Alias
+ from ..sql.selectable import Subquery
+
+
+_T = typing.TypeVar("_T")
+
+
+@util.deprecated(
+ "1.4",
+ "The :class:`.AliasOption` object is not necessary "
+ "for entities to be matched up to a query that is established "
+ "via :meth:`.Query.from_statement` and now does nothing.",
+ enable_warnings=False, # AliasOption itself warns
+)
+def contains_alias(alias: Union[Alias, Subquery]) -> AliasOption:
+ r"""Return a :class:`.MapperOption` that will indicate to the
+ :class:`_query.Query`
+ that the main table has been aliased.
+
+ """
+ return AliasOption(alias)
+
+
+def mapped_column(
+ __name_pos: Optional[
+ Union[str, _TypeEngineArgument[Any], SchemaEventTarget]
+ ] = None,
+ __type_pos: Optional[
+ Union[_TypeEngineArgument[Any], SchemaEventTarget]
+ ] = None,
+ *args: SchemaEventTarget,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Optional[Any] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ nullable: Optional[
+ Union[bool, Literal[SchemaConst.NULL_UNSPECIFIED]]
+ ] = SchemaConst.NULL_UNSPECIFIED,
+ primary_key: Optional[bool] = False,
+ deferred: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ deferred_group: Optional[str] = None,
+ deferred_raiseload: Optional[bool] = None,
+ use_existing_column: bool = False,
+ name: Optional[str] = None,
+ type_: Optional[_TypeEngineArgument[Any]] = None,
+ autoincrement: _AutoIncrementType = "auto",
+ doc: Optional[str] = None,
+ key: Optional[str] = None,
+ index: Optional[bool] = None,
+ unique: Optional[bool] = None,
+ info: Optional[_InfoType] = None,
+ onupdate: Optional[Any] = None,
+ insert_default: Optional[Any] = _NoArg.NO_ARG,
+ server_default: Optional[_ServerDefaultArgument] = None,
+ server_onupdate: Optional[FetchedValue] = None,
+ active_history: bool = False,
+ quote: Optional[bool] = None,
+ system: bool = False,
+ comment: Optional[str] = None,
+ sort_order: Union[_NoArg, int] = _NoArg.NO_ARG,
+ **kw: Any,
+) -> MappedColumn[Any]:
+ r"""declare a new ORM-mapped :class:`_schema.Column` construct
+ for use within :ref:`Declarative Table <orm_declarative_table>`
+ configuration.
+
+ The :func:`_orm.mapped_column` function provides an ORM-aware and
+ Python-typing-compatible construct which is used with
+ :ref:`declarative <orm_declarative_mapping>` mappings to indicate an
+ attribute that's mapped to a Core :class:`_schema.Column` object. It
+ provides the equivalent feature as mapping an attribute to a
+ :class:`_schema.Column` object directly when using Declarative,
+ specifically when using :ref:`Declarative Table <orm_declarative_table>`
+ configuration.
+
+ .. versionadded:: 2.0
+
+ :func:`_orm.mapped_column` is normally used with explicit typing along with
+ the :class:`_orm.Mapped` annotation type, where it can derive the SQL
+ type and nullability for the column based on what's present within the
+ :class:`_orm.Mapped` annotation. It also may be used without annotations
+ as a drop-in replacement for how :class:`_schema.Column` is used in
+ Declarative mappings in SQLAlchemy 1.x style.
+
+ For usage examples of :func:`_orm.mapped_column`, see the documentation
+ at :ref:`orm_declarative_table`.
+
+ .. seealso::
+
+ :ref:`orm_declarative_table` - complete documentation
+
+ :ref:`whatsnew_20_orm_declarative_typing` - migration notes for
+ Declarative mappings using 1.x style mappings
+
+ :param __name: String name to give to the :class:`_schema.Column`. This
+ is an optional, positional only argument that if present must be the
+ first positional argument passed. If omitted, the attribute name to
+ which the :func:`_orm.mapped_column` is mapped will be used as the SQL
+ column name.
+ :param __type: :class:`_types.TypeEngine` type or instance which will
+ indicate the datatype to be associated with the :class:`_schema.Column`.
+ This is an optional, positional-only argument that if present must
+ immediately follow the ``__name`` parameter if present also, or otherwise
+ be the first positional parameter. If omitted, the ultimate type for
+ the column may be derived either from the annotated type, or if a
+ :class:`_schema.ForeignKey` is present, from the datatype of the
+ referenced column.
+ :param \*args: Additional positional arguments include constructs such
+ as :class:`_schema.ForeignKey`, :class:`_schema.CheckConstraint`,
+ and :class:`_schema.Identity`, which are passed through to the constructed
+ :class:`_schema.Column`.
+ :param nullable: Optional bool, whether the column should be "NULL" or
+ "NOT NULL". If omitted, the nullability is derived from the type
+ annotation based on whether or not ``typing.Optional`` is present.
+ ``nullable`` defaults to ``True`` otherwise for non-primary key columns,
+ and ``False`` for primary key columns.
+ :param primary_key: optional bool, indicates the :class:`_schema.Column`
+ would be part of the table's primary key or not.
+ :param deferred: Optional bool - this keyword argument is consumed by the
+ ORM declarative process, and is not part of the :class:`_schema.Column`
+ itself; instead, it indicates that this column should be "deferred" for
+ loading as though mapped by :func:`_orm.deferred`.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_deferred_declarative`
+
+ :param deferred_group: Implies :paramref:`_orm.mapped_column.deferred`
+ to ``True``, and set the :paramref:`_orm.deferred.group` parameter.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_deferred_group`
+
+ :param deferred_raiseload: Implies :paramref:`_orm.mapped_column.deferred`
+ to ``True``, and set the :paramref:`_orm.deferred.raiseload` parameter.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_deferred_raiseload`
+
+ :param use_existing_column: if True, will attempt to locate the given
+ column name on an inherited superclass (typically single inheriting
+ superclass), and if present, will not produce a new column, mapping
+ to the superclass column as though it were omitted from this class.
+ This is used for mixins that add new columns to an inherited superclass.
+
+ .. seealso::
+
+ :ref:`orm_inheritance_column_conflicts`
+
+ .. versionadded:: 2.0.0b4
+
+ :param default: Passed directly to the
+ :paramref:`_schema.Column.default` parameter if the
+ :paramref:`_orm.mapped_column.insert_default` parameter is not present.
+ Additionally, when used with :ref:`orm_declarative_native_dataclasses`,
+ indicates a default Python value that should be applied to the keyword
+ constructor within the generated ``__init__()`` method.
+
+ Note that in the case of dataclass generation when
+ :paramref:`_orm.mapped_column.insert_default` is not present, this means
+ the :paramref:`_orm.mapped_column.default` value is used in **two**
+ places, both the ``__init__()`` method as well as the
+ :paramref:`_schema.Column.default` parameter. While this behavior may
+ change in a future release, for the moment this tends to "work out"; a
+ default of ``None`` will mean that the :class:`_schema.Column` gets no
+ default generator, whereas a default that refers to a non-``None`` Python
+ or SQL expression value will be assigned up front on the object when
+ ``__init__()`` is called, which is the same value that the Core
+ :class:`_sql.Insert` construct would use in any case, leading to the same
+ end result.
+
+ .. note:: When using Core level column defaults that are callables to
+ be interpreted by the underlying :class:`_schema.Column` in conjunction
+ with :ref:`ORM-mapped dataclasses
+ <orm_declarative_native_dataclasses>`, especially those that are
+ :ref:`context-aware default functions <context_default_functions>`,
+ **the** :paramref:`_orm.mapped_column.insert_default` **parameter must
+ be used instead**. This is necessary to disambiguate the callable from
+ being interpreted as a dataclass level default.
+
+ :param insert_default: Passed directly to the
+ :paramref:`_schema.Column.default` parameter; will supersede the value
+ of :paramref:`_orm.mapped_column.default` when present, however
+ :paramref:`_orm.mapped_column.default` will always apply to the
+ constructor default for a dataclasses mapping.
+
+ :param sort_order: An integer that indicates how this mapped column
+ should be sorted compared to the others when the ORM is creating a
+ :class:`_schema.Table`. Among mapped columns that have the same
+ value the default ordering is used, placing first the mapped columns
+ defined in the main class, then the ones in the super classes.
+ Defaults to 0. The sort is ascending.
+
+ .. versionadded:: 2.0.4
+
+ :param active_history=False:
+
+ When ``True``, indicates that the "previous" value for a
+ scalar attribute should be loaded when replaced, if not
+ already loaded. Normally, history tracking logic for
+ simple non-primary-key scalar values only needs to be
+ aware of the "new" value in order to perform a flush. This
+ flag is available for applications that make use of
+ :func:`.attributes.get_history` or :meth:`.Session.is_modified`
+ which also need to know the "previous" value of the attribute.
+
+ .. versionadded:: 2.0.10
+
+
+ :param init: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__init__()``
+ method as generated by the dataclass process.
+ :param repr: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__repr__()``
+ method as generated by the dataclass process.
+ :param default_factory: Specific to
+ :ref:`orm_declarative_native_dataclasses`,
+ specifies a default-value generation function that will take place
+ as part of the ``__init__()``
+ method as generated by the dataclass process.
+ :param compare: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be included in comparison operations when generating the
+ ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+ .. versionadded:: 2.0.0b4
+
+ :param kw_only: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be marked as keyword-only when generating the ``__init__()``.
+
+ :param \**kw: All remaining keyword arguments are passed through to the
+ constructor for the :class:`_schema.Column`.
+
+ """
+
+ return MappedColumn(
+ __name_pos,
+ __type_pos,
+ *args,
+ name=name,
+ type_=type_,
+ autoincrement=autoincrement,
+ insert_default=insert_default,
+ attribute_options=_AttributeOptions(
+ init, repr, default, default_factory, compare, kw_only
+ ),
+ doc=doc,
+ key=key,
+ index=index,
+ unique=unique,
+ info=info,
+ active_history=active_history,
+ nullable=nullable,
+ onupdate=onupdate,
+ primary_key=primary_key,
+ server_default=server_default,
+ server_onupdate=server_onupdate,
+ use_existing_column=use_existing_column,
+ quote=quote,
+ comment=comment,
+ system=system,
+ deferred=deferred,
+ deferred_group=deferred_group,
+ deferred_raiseload=deferred_raiseload,
+ sort_order=sort_order,
+ **kw,
+ )
+
+
+def orm_insert_sentinel(
+ name: Optional[str] = None,
+ type_: Optional[_TypeEngineArgument[Any]] = None,
+ *,
+ default: Optional[Any] = None,
+ omit_from_statements: bool = True,
+) -> MappedColumn[Any]:
+ """Provides a surrogate :func:`_orm.mapped_column` that generates
+ a so-called :term:`sentinel` column, allowing efficient bulk
+ inserts with deterministic RETURNING sorting for tables that don't
+ otherwise have qualifying primary key configurations.
+
+ Use of :func:`_orm.orm_insert_sentinel` is analogous to the use of the
+ :func:`_schema.insert_sentinel` construct within a Core
+ :class:`_schema.Table` construct.
+
+ Guidelines for adding this construct to a Declarative mapped class
+ are the same as that of the :func:`_schema.insert_sentinel` construct;
+ the database table itself also needs to have a column with this name
+ present.
+
+ For background on how this object is used, see the section
+ :ref:`engine_insertmanyvalues_sentinel_columns` as part of the
+ section :ref:`engine_insertmanyvalues`.
+
+ .. seealso::
+
+ :func:`_schema.insert_sentinel`
+
+ :ref:`engine_insertmanyvalues`
+
+ :ref:`engine_insertmanyvalues_sentinel_columns`
+
+
+ .. versionadded:: 2.0.10
+
+ """
+
+ return mapped_column(
+ name=name,
+ default=(
+ default if default is not None else _InsertSentinelColumnDefault()
+ ),
+ _omit_from_statements=omit_from_statements,
+ insert_sentinel=True,
+ use_existing_column=True,
+ nullable=True,
+ )
+
+
+@util.deprecated_params(
+ **{
+ arg: (
+ "2.0",
+ f"The :paramref:`_orm.column_property.{arg}` parameter is "
+ "deprecated for :func:`_orm.column_property`. This parameter "
+ "applies to a writeable-attribute in a Declarative Dataclasses "
+ "configuration only, and :func:`_orm.column_property` is treated "
+ "as a read-only attribute in this context.",
+ )
+ for arg in ("init", "kw_only", "default", "default_factory")
+ }
+)
+def column_property(
+ column: _ORMColumnExprArgument[_T],
+ *additional_columns: _ORMColumnExprArgument[Any],
+ group: Optional[str] = None,
+ deferred: bool = False,
+ raiseload: bool = False,
+ comparator_factory: Optional[Type[PropComparator[_T]]] = None,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Optional[Any] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ active_history: bool = False,
+ expire_on_flush: bool = True,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+) -> MappedSQLExpression[_T]:
+ r"""Provide a column-level property for use with a mapping.
+
+ With Declarative mappings, :func:`_orm.column_property` is used to
+ map read-only SQL expressions to a mapped class.
+
+ When using Imperative mappings, :func:`_orm.column_property` also
+ takes on the role of mapping table columns with additional features.
+ When using fully Declarative mappings, the :func:`_orm.mapped_column`
+ construct should be used for this purpose.
+
+ With Declarative Dataclass mappings, :func:`_orm.column_property`
+ is considered to be **read only**, and will not be included in the
+ Dataclass ``__init__()`` constructor.
+
+ The :func:`_orm.column_property` function returns an instance of
+ :class:`.ColumnProperty`.
+
+ .. seealso::
+
+ :ref:`mapper_column_property_sql_expressions` - general use of
+ :func:`_orm.column_property` to map SQL expressions
+
+ :ref:`orm_imperative_table_column_options` - usage of
+ :func:`_orm.column_property` with Imperative Table mappings to apply
+ additional options to a plain :class:`_schema.Column` object
+
+ :param \*cols:
+ list of Column objects to be mapped.
+
+ :param active_history=False:
+
+ Used only for Imperative Table mappings, or legacy-style Declarative
+ mappings (i.e. which have not been upgraded to
+ :func:`_orm.mapped_column`), for column-based attributes that are
+ expected to be writeable; use :func:`_orm.mapped_column` with
+ :paramref:`_orm.mapped_column.active_history` for Declarative mappings.
+ See that parameter for functional details.
+
+ :param comparator_factory: a class which extends
+ :class:`.ColumnProperty.Comparator` which provides custom SQL
+ clause generation for comparison operations.
+
+ :param group:
+ a group name for this property when marked as deferred.
+
+ :param deferred:
+ when True, the column property is "deferred", meaning that
+ it does not load immediately, and is instead loaded when the
+ attribute is first accessed on an instance. See also
+ :func:`~sqlalchemy.orm.deferred`.
+
+ :param doc:
+ optional string that will be applied as the doc on the
+ class-bound descriptor.
+
+ :param expire_on_flush=True:
+ Disable expiry on flush. A column_property() which refers
+ to a SQL expression (and not a single table-bound column)
+ is considered to be a "read only" property; populating it
+ has no effect on the state of data, and it can only return
+ database state. For this reason a column_property()'s value
+ is expired whenever the parent object is involved in a
+ flush, that is, has any kind of "dirty" state within a flush.
+ Setting this parameter to ``False`` will have the effect of
+ leaving any existing value present after the flush proceeds.
+ Note that the :class:`.Session` with default expiration
+ settings still expires
+ all attributes after a :meth:`.Session.commit` call, however.
+
+ :param info: Optional data dictionary which will be populated into the
+ :attr:`.MapperProperty.info` attribute of this object.
+
+ :param raiseload: if True, indicates the column should raise an error
+ when undeferred, rather than loading the value. This can be
+ altered at query time by using the :func:`.deferred` option with
+ raiseload=False.
+
+ .. versionadded:: 1.4
+
+ .. seealso::
+
+ :ref:`orm_queryguide_deferred_raiseload`
+
+ :param init:
+
+ :param default:
+
+ :param default_factory:
+
+ :param kw_only:
+
+ """
+ return MappedSQLExpression(
+ column,
+ *additional_columns,
+ attribute_options=_AttributeOptions(
+ False if init is _NoArg.NO_ARG else init,
+ repr,
+ default,
+ default_factory,
+ compare,
+ kw_only,
+ ),
+ group=group,
+ deferred=deferred,
+ raiseload=raiseload,
+ comparator_factory=comparator_factory,
+ active_history=active_history,
+ expire_on_flush=expire_on_flush,
+ info=info,
+ doc=doc,
+ _assume_readonly_dc_attributes=True,
+ )
+
+
+@overload
+def composite(
+ _class_or_attr: _CompositeAttrType[Any],
+ *attrs: _CompositeAttrType[Any],
+ group: Optional[str] = None,
+ deferred: bool = False,
+ raiseload: bool = False,
+ comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
+ active_history: bool = False,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Optional[Any] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+ **__kw: Any,
+) -> Composite[Any]: ...
+
+
+@overload
+def composite(
+ _class_or_attr: Type[_CC],
+ *attrs: _CompositeAttrType[Any],
+ group: Optional[str] = None,
+ deferred: bool = False,
+ raiseload: bool = False,
+ comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
+ active_history: bool = False,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Optional[Any] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+ **__kw: Any,
+) -> Composite[_CC]: ...
+
+
+@overload
+def composite(
+ _class_or_attr: Callable[..., _CC],
+ *attrs: _CompositeAttrType[Any],
+ group: Optional[str] = None,
+ deferred: bool = False,
+ raiseload: bool = False,
+ comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
+ active_history: bool = False,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Optional[Any] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+ **__kw: Any,
+) -> Composite[_CC]: ...
+
+
+def composite(
+ _class_or_attr: Union[
+ None, Type[_CC], Callable[..., _CC], _CompositeAttrType[Any]
+ ] = None,
+ *attrs: _CompositeAttrType[Any],
+ group: Optional[str] = None,
+ deferred: bool = False,
+ raiseload: bool = False,
+ comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
+ active_history: bool = False,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Optional[Any] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+ **__kw: Any,
+) -> Composite[Any]:
+ r"""Return a composite column-based property for use with a Mapper.
+
+ See the mapping documentation section :ref:`mapper_composite` for a
+ full usage example.
+
+ The :class:`.MapperProperty` returned by :func:`.composite`
+ is the :class:`.Composite`.
+
+ :param class\_:
+ The "composite type" class, or any classmethod or callable which
+ will produce a new instance of the composite object given the
+ column values in order.
+
+ :param \*attrs:
+ List of elements to be mapped, which may include:
+
+ * :class:`_schema.Column` objects
+ * :func:`_orm.mapped_column` constructs
+ * string names of other attributes on the mapped class, which may be
+ any other SQL or object-mapped attribute. This can for
+ example allow a composite that refers to a many-to-one relationship
+
+ :param active_history=False:
+ When ``True``, indicates that the "previous" value for a
+ scalar attribute should be loaded when replaced, if not
+ already loaded. See the same flag on :func:`.column_property`.
+
+ :param group:
+ A group name for this property when marked as deferred.
+
+ :param deferred:
+ When True, the column property is "deferred", meaning that it does
+ not load immediately, and is instead loaded when the attribute is
+ first accessed on an instance. See also
+ :func:`~sqlalchemy.orm.deferred`.
+
+ :param comparator_factory: a class which extends
+ :class:`.Composite.Comparator` which provides custom SQL
+ clause generation for comparison operations.
+
+ :param doc:
+ optional string that will be applied as the doc on the
+ class-bound descriptor.
+
+ :param info: Optional data dictionary which will be populated into the
+ :attr:`.MapperProperty.info` attribute of this object.
+
+ :param init: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__init__()``
+ method as generated by the dataclass process.
+ :param repr: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__repr__()``
+ method as generated by the dataclass process.
+ :param default_factory: Specific to
+ :ref:`orm_declarative_native_dataclasses`,
+ specifies a default-value generation function that will take place
+ as part of the ``__init__()``
+ method as generated by the dataclass process.
+
+ :param compare: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be included in comparison operations when generating the
+ ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+ .. versionadded:: 2.0.0b4
+
+ :param kw_only: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be marked as keyword-only when generating the ``__init__()``.
+
+ """
+ if __kw:
+ raise _no_kw()
+
+ return Composite(
+ _class_or_attr,
+ *attrs,
+ attribute_options=_AttributeOptions(
+ init, repr, default, default_factory, compare, kw_only
+ ),
+ group=group,
+ deferred=deferred,
+ raiseload=raiseload,
+ comparator_factory=comparator_factory,
+ active_history=active_history,
+ info=info,
+ doc=doc,
+ )
+
+
+def with_loader_criteria(
+ entity_or_base: _EntityType[Any],
+ where_criteria: Union[
+ _ColumnExpressionArgument[bool],
+ Callable[[Any], _ColumnExpressionArgument[bool]],
+ ],
+ loader_only: bool = False,
+ include_aliases: bool = False,
+ propagate_to_loaders: bool = True,
+ track_closure_variables: bool = True,
+) -> LoaderCriteriaOption:
+ """Add additional WHERE criteria to the load for all occurrences of
+ a particular entity.
+
+ .. versionadded:: 1.4
+
+ The :func:`_orm.with_loader_criteria` option is intended to add
+ limiting criteria to a particular kind of entity in a query,
+ **globally**, meaning it will apply to the entity as it appears
+ in the SELECT query as well as within any subqueries, join
+ conditions, and relationship loads, including both eager and lazy
+ loaders, without the need for it to be specified in any particular
+ part of the query. The rendering logic uses the same system used by
+ single table inheritance to ensure a certain discriminator is applied
+ to a table.
+
+ E.g., using :term:`2.0-style` queries, we can limit the way the
+ ``User.addresses`` collection is loaded, regardless of the kind
+ of loading used::
+
+ from sqlalchemy.orm import with_loader_criteria
+
+ stmt = select(User).options(
+ selectinload(User.addresses),
+ with_loader_criteria(Address, Address.email_address != 'foo'))
+ )
+
+ Above, the "selectinload" for ``User.addresses`` will apply the
+ given filtering criteria to the WHERE clause.
+
+ Another example, where the filtering will be applied to the
+ ON clause of the join, in this example using :term:`1.x style`
+ queries::
+
+ q = session.query(User).outerjoin(User.addresses).options(
+ with_loader_criteria(Address, Address.email_address != 'foo'))
+ )
+
+ The primary purpose of :func:`_orm.with_loader_criteria` is to use
+ it in the :meth:`_orm.SessionEvents.do_orm_execute` event handler
+ to ensure that all occurrences of a particular entity are filtered
+ in a certain way, such as filtering for access control roles. It
+ also can be used to apply criteria to relationship loads. In the
+ example below, we can apply a certain set of rules to all queries
+ emitted by a particular :class:`_orm.Session`::
+
+ session = Session(bind=engine)
+
+ @event.listens_for("do_orm_execute", session)
+ def _add_filtering_criteria(execute_state):
+
+ if (
+ execute_state.is_select
+ and not execute_state.is_column_load
+ and not execute_state.is_relationship_load
+ ):
+ execute_state.statement = execute_state.statement.options(
+ with_loader_criteria(
+ SecurityRole,
+ lambda cls: cls.role.in_(['some_role']),
+ include_aliases=True
+ )
+ )
+
+ In the above example, the :meth:`_orm.SessionEvents.do_orm_execute`
+ event will intercept all queries emitted using the
+ :class:`_orm.Session`. For those queries which are SELECT statements
+ and are not attribute or relationship loads a custom
+ :func:`_orm.with_loader_criteria` option is added to the query. The
+ :func:`_orm.with_loader_criteria` option will be used in the given
+ statement and will also be automatically propagated to all relationship
+ loads that descend from this query.
+
+ The criteria argument given is a ``lambda`` that accepts a ``cls``
+ argument. The given class will expand to include all mapped subclass
+ and need not itself be a mapped class.
+
+ .. tip::
+
+ When using :func:`_orm.with_loader_criteria` option in
+ conjunction with the :func:`_orm.contains_eager` loader option,
+ it's important to note that :func:`_orm.with_loader_criteria` only
+ affects the part of the query that determines what SQL is rendered
+ in terms of the WHERE and FROM clauses. The
+ :func:`_orm.contains_eager` option does not affect the rendering of
+ the SELECT statement outside of the columns clause, so does not have
+ any interaction with the :func:`_orm.with_loader_criteria` option.
+ However, the way things "work" is that :func:`_orm.contains_eager`
+ is meant to be used with a query that is already selecting from the
+ additional entities in some way, where
+ :func:`_orm.with_loader_criteria` can apply it's additional
+ criteria.
+
+ In the example below, assuming a mapping relationship as
+ ``A -> A.bs -> B``, the given :func:`_orm.with_loader_criteria`
+ option will affect the way in which the JOIN is rendered::
+
+ stmt = select(A).join(A.bs).options(
+ contains_eager(A.bs),
+ with_loader_criteria(B, B.flag == 1)
+ )
+
+ Above, the given :func:`_orm.with_loader_criteria` option will
+ affect the ON clause of the JOIN that is specified by
+ ``.join(A.bs)``, so is applied as expected. The
+ :func:`_orm.contains_eager` option has the effect that columns from
+ ``B`` are added to the columns clause::
+
+ SELECT
+ b.id, b.a_id, b.data, b.flag,
+ a.id AS id_1,
+ a.data AS data_1
+ FROM a JOIN b ON a.id = b.a_id AND b.flag = :flag_1
+
+
+ The use of the :func:`_orm.contains_eager` option within the above
+ statement has no effect on the behavior of the
+ :func:`_orm.with_loader_criteria` option. If the
+ :func:`_orm.contains_eager` option were omitted, the SQL would be
+ the same as regards the FROM and WHERE clauses, where
+ :func:`_orm.with_loader_criteria` continues to add its criteria to
+ the ON clause of the JOIN. The addition of
+ :func:`_orm.contains_eager` only affects the columns clause, in that
+ additional columns against ``b`` are added which are then consumed
+ by the ORM to produce ``B`` instances.
+
+ .. warning:: The use of a lambda inside of the call to
+ :func:`_orm.with_loader_criteria` is only invoked **once per unique
+ class**. Custom functions should not be invoked within this lambda.
+ See :ref:`engine_lambda_caching` for an overview of the "lambda SQL"
+ feature, which is for advanced use only.
+
+ :param entity_or_base: a mapped class, or a class that is a super
+ class of a particular set of mapped classes, to which the rule
+ will apply.
+
+ :param where_criteria: a Core SQL expression that applies limiting
+ criteria. This may also be a "lambda:" or Python function that
+ accepts a target class as an argument, when the given class is
+ a base with many different mapped subclasses.
+
+ .. note:: To support pickling, use a module-level Python function to
+ produce the SQL expression instead of a lambda or a fixed SQL
+ expression, which tend to not be picklable.
+
+ :param include_aliases: if True, apply the rule to :func:`_orm.aliased`
+ constructs as well.
+
+ :param propagate_to_loaders: defaults to True, apply to relationship
+ loaders such as lazy loaders. This indicates that the
+ option object itself including SQL expression is carried along with
+ each loaded instance. Set to ``False`` to prevent the object from
+ being assigned to individual instances.
+
+
+ .. seealso::
+
+ :ref:`examples_session_orm_events` - includes examples of using
+ :func:`_orm.with_loader_criteria`.
+
+ :ref:`do_orm_execute_global_criteria` - basic example on how to
+ combine :func:`_orm.with_loader_criteria` with the
+ :meth:`_orm.SessionEvents.do_orm_execute` event.
+
+ :param track_closure_variables: when False, closure variables inside
+ of a lambda expression will not be used as part of
+ any cache key. This allows more complex expressions to be used
+ inside of a lambda expression but requires that the lambda ensures
+ it returns the identical SQL every time given a particular class.
+
+ .. versionadded:: 1.4.0b2
+
+ """
+ return LoaderCriteriaOption(
+ entity_or_base,
+ where_criteria,
+ loader_only,
+ include_aliases,
+ propagate_to_loaders,
+ track_closure_variables,
+ )
+
+
+def relationship(
+ argument: Optional[_RelationshipArgumentType[Any]] = None,
+ secondary: Optional[_RelationshipSecondaryArgument] = None,
+ *,
+ uselist: Optional[bool] = None,
+ collection_class: Optional[
+ Union[Type[Collection[Any]], Callable[[], Collection[Any]]]
+ ] = None,
+ primaryjoin: Optional[_RelationshipJoinConditionArgument] = None,
+ secondaryjoin: Optional[_RelationshipJoinConditionArgument] = None,
+ back_populates: Optional[str] = None,
+ order_by: _ORMOrderByArgument = False,
+ backref: Optional[ORMBackrefArgument] = None,
+ overlaps: Optional[str] = None,
+ post_update: bool = False,
+ cascade: str = "save-update, merge",
+ viewonly: bool = False,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Union[_NoArg, _T] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ lazy: _LazyLoadArgumentType = "select",
+ passive_deletes: Union[Literal["all"], bool] = False,
+ passive_updates: bool = True,
+ active_history: bool = False,
+ enable_typechecks: bool = True,
+ foreign_keys: Optional[_ORMColCollectionArgument] = None,
+ remote_side: Optional[_ORMColCollectionArgument] = None,
+ join_depth: Optional[int] = None,
+ comparator_factory: Optional[
+ Type[RelationshipProperty.Comparator[Any]]
+ ] = None,
+ single_parent: bool = False,
+ innerjoin: bool = False,
+ distinct_target_key: Optional[bool] = None,
+ load_on_pending: bool = False,
+ query_class: Optional[Type[Query[Any]]] = None,
+ info: Optional[_InfoType] = None,
+ omit_join: Literal[None, False] = None,
+ sync_backref: Optional[bool] = None,
+ **kw: Any,
+) -> _RelationshipDeclared[Any]:
+ """Provide a relationship between two mapped classes.
+
+ This corresponds to a parent-child or associative table relationship.
+ The constructed class is an instance of :class:`.Relationship`.
+
+ .. seealso::
+
+ :ref:`tutorial_orm_related_objects` - tutorial introduction
+ to :func:`_orm.relationship` in the :ref:`unified_tutorial`
+
+ :ref:`relationship_config_toplevel` - narrative documentation
+
+ :param argument:
+ This parameter refers to the class that is to be related. It
+ accepts several forms, including a direct reference to the target
+ class itself, the :class:`_orm.Mapper` instance for the target class,
+ a Python callable / lambda that will return a reference to the
+ class or :class:`_orm.Mapper` when called, and finally a string
+ name for the class, which will be resolved from the
+ :class:`_orm.registry` in use in order to locate the class, e.g.::
+
+ class SomeClass(Base):
+ # ...
+
+ related = relationship("RelatedClass")
+
+ The :paramref:`_orm.relationship.argument` may also be omitted from the
+ :func:`_orm.relationship` construct entirely, and instead placed inside
+ a :class:`_orm.Mapped` annotation on the left side, which should
+ include a Python collection type if the relationship is expected
+ to be a collection, such as::
+
+ class SomeClass(Base):
+ # ...
+
+ related_items: Mapped[List["RelatedItem"]] = relationship()
+
+ Or for a many-to-one or one-to-one relationship::
+
+ class SomeClass(Base):
+ # ...
+
+ related_item: Mapped["RelatedItem"] = relationship()
+
+ .. seealso::
+
+ :ref:`orm_declarative_properties` - further detail
+ on relationship configuration when using Declarative.
+
+ :param secondary:
+ For a many-to-many relationship, specifies the intermediary
+ table, and is typically an instance of :class:`_schema.Table`.
+ In less common circumstances, the argument may also be specified
+ as an :class:`_expression.Alias` construct, or even a
+ :class:`_expression.Join` construct.
+
+ :paramref:`_orm.relationship.secondary` may
+ also be passed as a callable function which is evaluated at
+ mapper initialization time. When using Declarative, it may also
+ be a string argument noting the name of a :class:`_schema.Table`
+ that is
+ present in the :class:`_schema.MetaData`
+ collection associated with the
+ parent-mapped :class:`_schema.Table`.
+
+ .. warning:: When passed as a Python-evaluable string, the
+ argument is interpreted using Python's ``eval()`` function.
+ **DO NOT PASS UNTRUSTED INPUT TO THIS STRING**.
+ See :ref:`declarative_relationship_eval` for details on
+ declarative evaluation of :func:`_orm.relationship` arguments.
+
+ The :paramref:`_orm.relationship.secondary` keyword argument is
+ typically applied in the case where the intermediary
+ :class:`_schema.Table`
+ is not otherwise expressed in any direct class mapping. If the
+ "secondary" table is also explicitly mapped elsewhere (e.g. as in
+ :ref:`association_pattern`), one should consider applying the
+ :paramref:`_orm.relationship.viewonly` flag so that this
+ :func:`_orm.relationship`
+ is not used for persistence operations which
+ may conflict with those of the association object pattern.
+
+ .. seealso::
+
+ :ref:`relationships_many_to_many` - Reference example of "many
+ to many".
+
+ :ref:`self_referential_many_to_many` - Specifics on using
+ many-to-many in a self-referential case.
+
+ :ref:`declarative_many_to_many` - Additional options when using
+ Declarative.
+
+ :ref:`association_pattern` - an alternative to
+ :paramref:`_orm.relationship.secondary`
+ when composing association
+ table relationships, allowing additional attributes to be
+ specified on the association table.
+
+ :ref:`composite_secondary_join` - a lesser-used pattern which
+ in some cases can enable complex :func:`_orm.relationship` SQL
+ conditions to be used.
+
+ :param active_history=False:
+ When ``True``, indicates that the "previous" value for a
+ many-to-one reference should be loaded when replaced, if
+ not already loaded. Normally, history tracking logic for
+ simple many-to-ones only needs to be aware of the "new"
+ value in order to perform a flush. This flag is available
+ for applications that make use of
+ :func:`.attributes.get_history` which also need to know
+ the "previous" value of the attribute.
+
+ :param backref:
+ A reference to a string relationship name, or a :func:`_orm.backref`
+ construct, which will be used to automatically generate a new
+ :func:`_orm.relationship` on the related class, which then refers to this
+ one using a bi-directional :paramref:`_orm.relationship.back_populates`
+ configuration.
+
+ In modern Python, explicit use of :func:`_orm.relationship`
+ with :paramref:`_orm.relationship.back_populates` should be preferred,
+ as it is more robust in terms of mapper configuration as well as
+ more conceptually straightforward. It also integrates with
+ new :pep:`484` typing features introduced in SQLAlchemy 2.0 which
+ is not possible with dynamically generated attributes.
+
+ .. seealso::
+
+ :ref:`relationships_backref` - notes on using
+ :paramref:`_orm.relationship.backref`
+
+ :ref:`tutorial_orm_related_objects` - in the :ref:`unified_tutorial`,
+ presents an overview of bi-directional relationship configuration
+ and behaviors using :paramref:`_orm.relationship.back_populates`
+
+ :func:`.backref` - allows control over :func:`_orm.relationship`
+ configuration when using :paramref:`_orm.relationship.backref`.
+
+
+ :param back_populates:
+ Indicates the name of a :func:`_orm.relationship` on the related
+ class that will be synchronized with this one. It is usually
+ expected that the :func:`_orm.relationship` on the related class
+ also refer to this one. This allows objects on both sides of
+ each :func:`_orm.relationship` to synchronize in-Python state
+ changes and also provides directives to the :term:`unit of work`
+ flush process how changes along these relationships should
+ be persisted.
+
+ .. seealso::
+
+ :ref:`tutorial_orm_related_objects` - in the :ref:`unified_tutorial`,
+ presents an overview of bi-directional relationship configuration
+ and behaviors.
+
+ :ref:`relationship_patterns` - includes many examples of
+ :paramref:`_orm.relationship.back_populates`.
+
+ :paramref:`_orm.relationship.backref` - legacy form which allows
+ more succinct configuration, but does not support explicit typing
+
+ :param overlaps:
+ A string name or comma-delimited set of names of other relationships
+ on either this mapper, a descendant mapper, or a target mapper with
+ which this relationship may write to the same foreign keys upon
+ persistence. The only effect this has is to eliminate the
+ warning that this relationship will conflict with another upon
+ persistence. This is used for such relationships that are truly
+ capable of conflicting with each other on write, but the application
+ will ensure that no such conflicts occur.
+
+ .. versionadded:: 1.4
+
+ .. seealso::
+
+ :ref:`error_qzyx` - usage example
+
+ :param cascade:
+ A comma-separated list of cascade rules which determines how
+ Session operations should be "cascaded" from parent to child.
+ This defaults to ``False``, which means the default cascade
+ should be used - this default cascade is ``"save-update, merge"``.
+
+ The available cascades are ``save-update``, ``merge``,
+ ``expunge``, ``delete``, ``delete-orphan``, and ``refresh-expire``.
+ An additional option, ``all`` indicates shorthand for
+ ``"save-update, merge, refresh-expire,
+ expunge, delete"``, and is often used as in ``"all, delete-orphan"``
+ to indicate that related objects should follow along with the
+ parent object in all cases, and be deleted when de-associated.
+
+ .. seealso::
+
+ :ref:`unitofwork_cascades` - Full detail on each of the available
+ cascade options.
+
+ :param cascade_backrefs=False:
+ Legacy; this flag is always False.
+
+ .. versionchanged:: 2.0 "cascade_backrefs" functionality has been
+ removed.
+
+ :param collection_class:
+ A class or callable that returns a new list-holding object. will
+ be used in place of a plain list for storing elements.
+
+ .. seealso::
+
+ :ref:`custom_collections` - Introductory documentation and
+ examples.
+
+ :param comparator_factory:
+ A class which extends :class:`.Relationship.Comparator`
+ which provides custom SQL clause generation for comparison
+ operations.
+
+ .. seealso::
+
+ :class:`.PropComparator` - some detail on redefining comparators
+ at this level.
+
+ :ref:`custom_comparators` - Brief intro to this feature.
+
+
+ :param distinct_target_key=None:
+ Indicate if a "subquery" eager load should apply the DISTINCT
+ keyword to the innermost SELECT statement. When left as ``None``,
+ the DISTINCT keyword will be applied in those cases when the target
+ columns do not comprise the full primary key of the target table.
+ When set to ``True``, the DISTINCT keyword is applied to the
+ innermost SELECT unconditionally.
+
+ It may be desirable to set this flag to False when the DISTINCT is
+ reducing performance of the innermost subquery beyond that of what
+ duplicate innermost rows may be causing.
+
+ .. seealso::
+
+ :ref:`loading_toplevel` - includes an introduction to subquery
+ eager loading.
+
+ :param doc:
+ Docstring which will be applied to the resulting descriptor.
+
+ :param foreign_keys:
+
+ A list of columns which are to be used as "foreign key"
+ columns, or columns which refer to the value in a remote
+ column, within the context of this :func:`_orm.relationship`
+ object's :paramref:`_orm.relationship.primaryjoin` condition.
+ That is, if the :paramref:`_orm.relationship.primaryjoin`
+ condition of this :func:`_orm.relationship` is ``a.id ==
+ b.a_id``, and the values in ``b.a_id`` are required to be
+ present in ``a.id``, then the "foreign key" column of this
+ :func:`_orm.relationship` is ``b.a_id``.
+
+ In normal cases, the :paramref:`_orm.relationship.foreign_keys`
+ parameter is **not required.** :func:`_orm.relationship` will
+ automatically determine which columns in the
+ :paramref:`_orm.relationship.primaryjoin` condition are to be
+ considered "foreign key" columns based on those
+ :class:`_schema.Column` objects that specify
+ :class:`_schema.ForeignKey`,
+ or are otherwise listed as referencing columns in a
+ :class:`_schema.ForeignKeyConstraint` construct.
+ :paramref:`_orm.relationship.foreign_keys` is only needed when:
+
+ 1. There is more than one way to construct a join from the local
+ table to the remote table, as there are multiple foreign key
+ references present. Setting ``foreign_keys`` will limit the
+ :func:`_orm.relationship`
+ to consider just those columns specified
+ here as "foreign".
+
+ 2. The :class:`_schema.Table` being mapped does not actually have
+ :class:`_schema.ForeignKey` or
+ :class:`_schema.ForeignKeyConstraint`
+ constructs present, often because the table
+ was reflected from a database that does not support foreign key
+ reflection (MySQL MyISAM).
+
+ 3. The :paramref:`_orm.relationship.primaryjoin`
+ argument is used to
+ construct a non-standard join condition, which makes use of
+ columns or expressions that do not normally refer to their
+ "parent" column, such as a join condition expressed by a
+ complex comparison using a SQL function.
+
+ The :func:`_orm.relationship` construct will raise informative
+ error messages that suggest the use of the
+ :paramref:`_orm.relationship.foreign_keys` parameter when
+ presented with an ambiguous condition. In typical cases,
+ if :func:`_orm.relationship` doesn't raise any exceptions, the
+ :paramref:`_orm.relationship.foreign_keys` parameter is usually
+ not needed.
+
+ :paramref:`_orm.relationship.foreign_keys` may also be passed as a
+ callable function which is evaluated at mapper initialization time,
+ and may be passed as a Python-evaluable string when using
+ Declarative.
+
+ .. warning:: When passed as a Python-evaluable string, the
+ argument is interpreted using Python's ``eval()`` function.
+ **DO NOT PASS UNTRUSTED INPUT TO THIS STRING**.
+ See :ref:`declarative_relationship_eval` for details on
+ declarative evaluation of :func:`_orm.relationship` arguments.
+
+ .. seealso::
+
+ :ref:`relationship_foreign_keys`
+
+ :ref:`relationship_custom_foreign`
+
+ :func:`.foreign` - allows direct annotation of the "foreign"
+ columns within a :paramref:`_orm.relationship.primaryjoin`
+ condition.
+
+ :param info: Optional data dictionary which will be populated into the
+ :attr:`.MapperProperty.info` attribute of this object.
+
+ :param innerjoin=False:
+ When ``True``, joined eager loads will use an inner join to join
+ against related tables instead of an outer join. The purpose
+ of this option is generally one of performance, as inner joins
+ generally perform better than outer joins.
+
+ This flag can be set to ``True`` when the relationship references an
+ object via many-to-one using local foreign keys that are not
+ nullable, or when the reference is one-to-one or a collection that
+ is guaranteed to have one or at least one entry.
+
+ The option supports the same "nested" and "unnested" options as
+ that of :paramref:`_orm.joinedload.innerjoin`. See that flag
+ for details on nested / unnested behaviors.
+
+ .. seealso::
+
+ :paramref:`_orm.joinedload.innerjoin` - the option as specified by
+ loader option, including detail on nesting behavior.
+
+ :ref:`what_kind_of_loading` - Discussion of some details of
+ various loader options.
+
+
+ :param join_depth:
+ When non-``None``, an integer value indicating how many levels
+ deep "eager" loaders should join on a self-referring or cyclical
+ relationship. The number counts how many times the same Mapper
+ shall be present in the loading condition along a particular join
+ branch. When left at its default of ``None``, eager loaders
+ will stop chaining when they encounter a the same target mapper
+ which is already higher up in the chain. This option applies
+ both to joined- and subquery- eager loaders.
+
+ .. seealso::
+
+ :ref:`self_referential_eager_loading` - Introductory documentation
+ and examples.
+
+ :param lazy='select': specifies
+ How the related items should be loaded. Default value is
+ ``select``. Values include:
+
+ * ``select`` - items should be loaded lazily when the property is
+ first accessed, using a separate SELECT statement, or identity map
+ fetch for simple many-to-one references.
+
+ * ``immediate`` - items should be loaded as the parents are loaded,
+ using a separate SELECT statement, or identity map fetch for
+ simple many-to-one references.
+
+ * ``joined`` - items should be loaded "eagerly" in the same query as
+ that of the parent, using a JOIN or LEFT OUTER JOIN. Whether
+ the join is "outer" or not is determined by the
+ :paramref:`_orm.relationship.innerjoin` parameter.
+
+ * ``subquery`` - items should be loaded "eagerly" as the parents are
+ loaded, using one additional SQL statement, which issues a JOIN to
+ a subquery of the original statement, for each collection
+ requested.
+
+ * ``selectin`` - items should be loaded "eagerly" as the parents
+ are loaded, using one or more additional SQL statements, which
+ issues a JOIN to the immediate parent object, specifying primary
+ key identifiers using an IN clause.
+
+ * ``noload`` - no loading should occur at any time. The related
+ collection will remain empty. The ``noload`` strategy is not
+ recommended for general use. For a general use "never load"
+ approach, see :ref:`write_only_relationship`
+
+ * ``raise`` - lazy loading is disallowed; accessing
+ the attribute, if its value were not already loaded via eager
+ loading, will raise an :exc:`~sqlalchemy.exc.InvalidRequestError`.
+ This strategy can be used when objects are to be detached from
+ their attached :class:`.Session` after they are loaded.
+
+ * ``raise_on_sql`` - lazy loading that emits SQL is disallowed;
+ accessing the attribute, if its value were not already loaded via
+ eager loading, will raise an
+ :exc:`~sqlalchemy.exc.InvalidRequestError`, **if the lazy load
+ needs to emit SQL**. If the lazy load can pull the related value
+ from the identity map or determine that it should be None, the
+ value is loaded. This strategy can be used when objects will
+ remain associated with the attached :class:`.Session`, however
+ additional SELECT statements should be blocked.
+
+ * ``write_only`` - the attribute will be configured with a special
+ "virtual collection" that may receive
+ :meth:`_orm.WriteOnlyCollection.add` and
+ :meth:`_orm.WriteOnlyCollection.remove` commands to add or remove
+ individual objects, but will not under any circumstances load or
+ iterate the full set of objects from the database directly. Instead,
+ methods such as :meth:`_orm.WriteOnlyCollection.select`,
+ :meth:`_orm.WriteOnlyCollection.insert`,
+ :meth:`_orm.WriteOnlyCollection.update` and
+ :meth:`_orm.WriteOnlyCollection.delete` are provided which generate SQL
+ constructs that may be used to load and modify rows in bulk. Used for
+ large collections that are never appropriate to load at once into
+ memory.
+
+ The ``write_only`` loader style is configured automatically when
+ the :class:`_orm.WriteOnlyMapped` annotation is provided on the
+ left hand side within a Declarative mapping. See the section
+ :ref:`write_only_relationship` for examples.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`write_only_relationship` - in the :ref:`queryguide_toplevel`
+
+ * ``dynamic`` - the attribute will return a pre-configured
+ :class:`_query.Query` object for all read
+ operations, onto which further filtering operations can be
+ applied before iterating the results.
+
+ The ``dynamic`` loader style is configured automatically when
+ the :class:`_orm.DynamicMapped` annotation is provided on the
+ left hand side within a Declarative mapping. See the section
+ :ref:`dynamic_relationship` for examples.
+
+ .. legacy:: The "dynamic" lazy loader strategy is the legacy form of
+ what is now the "write_only" strategy described in the section
+ :ref:`write_only_relationship`.
+
+ .. seealso::
+
+ :ref:`dynamic_relationship` - in the :ref:`queryguide_toplevel`
+
+ :ref:`write_only_relationship` - more generally useful approach
+ for large collections that should not fully load into memory
+
+ * True - a synonym for 'select'
+
+ * False - a synonym for 'joined'
+
+ * None - a synonym for 'noload'
+
+ .. seealso::
+
+ :ref:`orm_queryguide_relationship_loaders` - Full documentation on
+ relationship loader configuration in the :ref:`queryguide_toplevel`.
+
+
+ :param load_on_pending=False:
+ Indicates loading behavior for transient or pending parent objects.
+
+ When set to ``True``, causes the lazy-loader to
+ issue a query for a parent object that is not persistent, meaning it
+ has never been flushed. This may take effect for a pending object
+ when autoflush is disabled, or for a transient object that has been
+ "attached" to a :class:`.Session` but is not part of its pending
+ collection.
+
+ The :paramref:`_orm.relationship.load_on_pending`
+ flag does not improve
+ behavior when the ORM is used normally - object references should be
+ constructed at the object level, not at the foreign key level, so
+ that they are present in an ordinary way before a flush proceeds.
+ This flag is not not intended for general use.
+
+ .. seealso::
+
+ :meth:`.Session.enable_relationship_loading` - this method
+ establishes "load on pending" behavior for the whole object, and
+ also allows loading on objects that remain transient or
+ detached.
+
+ :param order_by:
+ Indicates the ordering that should be applied when loading these
+ items. :paramref:`_orm.relationship.order_by`
+ is expected to refer to
+ one of the :class:`_schema.Column`
+ objects to which the target class is
+ mapped, or the attribute itself bound to the target class which
+ refers to the column.
+
+ :paramref:`_orm.relationship.order_by`
+ may also be passed as a callable
+ function which is evaluated at mapper initialization time, and may
+ be passed as a Python-evaluable string when using Declarative.
+
+ .. warning:: When passed as a Python-evaluable string, the
+ argument is interpreted using Python's ``eval()`` function.
+ **DO NOT PASS UNTRUSTED INPUT TO THIS STRING**.
+ See :ref:`declarative_relationship_eval` for details on
+ declarative evaluation of :func:`_orm.relationship` arguments.
+
+ :param passive_deletes=False:
+ Indicates loading behavior during delete operations.
+
+ A value of True indicates that unloaded child items should not
+ be loaded during a delete operation on the parent. Normally,
+ when a parent item is deleted, all child items are loaded so
+ that they can either be marked as deleted, or have their
+ foreign key to the parent set to NULL. Marking this flag as
+ True usually implies an ON DELETE <CASCADE|SET NULL> rule is in
+ place which will handle updating/deleting child rows on the
+ database side.
+
+ Additionally, setting the flag to the string value 'all' will
+ disable the "nulling out" of the child foreign keys, when the parent
+ object is deleted and there is no delete or delete-orphan cascade
+ enabled. This is typically used when a triggering or error raise
+ scenario is in place on the database side. Note that the foreign
+ key attributes on in-session child objects will not be changed after
+ a flush occurs so this is a very special use-case setting.
+ Additionally, the "nulling out" will still occur if the child
+ object is de-associated with the parent.
+
+ .. seealso::
+
+ :ref:`passive_deletes` - Introductory documentation
+ and examples.
+
+ :param passive_updates=True:
+ Indicates the persistence behavior to take when a referenced
+ primary key value changes in place, indicating that the referencing
+ foreign key columns will also need their value changed.
+
+ When True, it is assumed that ``ON UPDATE CASCADE`` is configured on
+ the foreign key in the database, and that the database will
+ handle propagation of an UPDATE from a source column to
+ dependent rows. When False, the SQLAlchemy
+ :func:`_orm.relationship`
+ construct will attempt to emit its own UPDATE statements to
+ modify related targets. However note that SQLAlchemy **cannot**
+ emit an UPDATE for more than one level of cascade. Also,
+ setting this flag to False is not compatible in the case where
+ the database is in fact enforcing referential integrity, unless
+ those constraints are explicitly "deferred", if the target backend
+ supports it.
+
+ It is highly advised that an application which is employing
+ mutable primary keys keeps ``passive_updates`` set to True,
+ and instead uses the referential integrity features of the database
+ itself in order to handle the change efficiently and fully.
+
+ .. seealso::
+
+ :ref:`passive_updates` - Introductory documentation and
+ examples.
+
+ :paramref:`.mapper.passive_updates` - a similar flag which
+ takes effect for joined-table inheritance mappings.
+
+ :param post_update:
+ This indicates that the relationship should be handled by a
+ second UPDATE statement after an INSERT or before a
+ DELETE. This flag is used to handle saving bi-directional
+ dependencies between two individual rows (i.e. each row
+ references the other), where it would otherwise be impossible to
+ INSERT or DELETE both rows fully since one row exists before the
+ other. Use this flag when a particular mapping arrangement will
+ incur two rows that are dependent on each other, such as a table
+ that has a one-to-many relationship to a set of child rows, and
+ also has a column that references a single child row within that
+ list (i.e. both tables contain a foreign key to each other). If
+ a flush operation returns an error that a "cyclical
+ dependency" was detected, this is a cue that you might want to
+ use :paramref:`_orm.relationship.post_update` to "break" the cycle.
+
+ .. seealso::
+
+ :ref:`post_update` - Introductory documentation and examples.
+
+ :param primaryjoin:
+ A SQL expression that will be used as the primary
+ join of the child object against the parent object, or in a
+ many-to-many relationship the join of the parent object to the
+ association table. By default, this value is computed based on the
+ foreign key relationships of the parent and child tables (or
+ association table).
+
+ :paramref:`_orm.relationship.primaryjoin` may also be passed as a
+ callable function which is evaluated at mapper initialization time,
+ and may be passed as a Python-evaluable string when using
+ Declarative.
+
+ .. warning:: When passed as a Python-evaluable string, the
+ argument is interpreted using Python's ``eval()`` function.
+ **DO NOT PASS UNTRUSTED INPUT TO THIS STRING**.
+ See :ref:`declarative_relationship_eval` for details on
+ declarative evaluation of :func:`_orm.relationship` arguments.
+
+ .. seealso::
+
+ :ref:`relationship_primaryjoin`
+
+ :param remote_side:
+ Used for self-referential relationships, indicates the column or
+ list of columns that form the "remote side" of the relationship.
+
+ :paramref:`_orm.relationship.remote_side` may also be passed as a
+ callable function which is evaluated at mapper initialization time,
+ and may be passed as a Python-evaluable string when using
+ Declarative.
+
+ .. warning:: When passed as a Python-evaluable string, the
+ argument is interpreted using Python's ``eval()`` function.
+ **DO NOT PASS UNTRUSTED INPUT TO THIS STRING**.
+ See :ref:`declarative_relationship_eval` for details on
+ declarative evaluation of :func:`_orm.relationship` arguments.
+
+ .. seealso::
+
+ :ref:`self_referential` - in-depth explanation of how
+ :paramref:`_orm.relationship.remote_side`
+ is used to configure self-referential relationships.
+
+ :func:`.remote` - an annotation function that accomplishes the
+ same purpose as :paramref:`_orm.relationship.remote_side`,
+ typically
+ when a custom :paramref:`_orm.relationship.primaryjoin` condition
+ is used.
+
+ :param query_class:
+ A :class:`_query.Query`
+ subclass that will be used internally by the
+ ``AppenderQuery`` returned by a "dynamic" relationship, that
+ is, a relationship that specifies ``lazy="dynamic"`` or was
+ otherwise constructed using the :func:`_orm.dynamic_loader`
+ function.
+
+ .. seealso::
+
+ :ref:`dynamic_relationship` - Introduction to "dynamic"
+ relationship loaders.
+
+ :param secondaryjoin:
+ A SQL expression that will be used as the join of
+ an association table to the child object. By default, this value is
+ computed based on the foreign key relationships of the association
+ and child tables.
+
+ :paramref:`_orm.relationship.secondaryjoin` may also be passed as a
+ callable function which is evaluated at mapper initialization time,
+ and may be passed as a Python-evaluable string when using
+ Declarative.
+
+ .. warning:: When passed as a Python-evaluable string, the
+ argument is interpreted using Python's ``eval()`` function.
+ **DO NOT PASS UNTRUSTED INPUT TO THIS STRING**.
+ See :ref:`declarative_relationship_eval` for details on
+ declarative evaluation of :func:`_orm.relationship` arguments.
+
+ .. seealso::
+
+ :ref:`relationship_primaryjoin`
+
+ :param single_parent:
+ When True, installs a validator which will prevent objects
+ from being associated with more than one parent at a time.
+ This is used for many-to-one or many-to-many relationships that
+ should be treated either as one-to-one or one-to-many. Its usage
+ is optional, except for :func:`_orm.relationship` constructs which
+ are many-to-one or many-to-many and also
+ specify the ``delete-orphan`` cascade option. The
+ :func:`_orm.relationship` construct itself will raise an error
+ instructing when this option is required.
+
+ .. seealso::
+
+ :ref:`unitofwork_cascades` - includes detail on when the
+ :paramref:`_orm.relationship.single_parent`
+ flag may be appropriate.
+
+ :param uselist:
+ A boolean that indicates if this property should be loaded as a
+ list or a scalar. In most cases, this value is determined
+ automatically by :func:`_orm.relationship` at mapper configuration
+ time. When using explicit :class:`_orm.Mapped` annotations,
+ :paramref:`_orm.relationship.uselist` may be derived from the
+ whether or not the annotation within :class:`_orm.Mapped` contains
+ a collection class.
+ Otherwise, :paramref:`_orm.relationship.uselist` may be derived from
+ the type and direction
+ of the relationship - one to many forms a list, many to one
+ forms a scalar, many to many is a list. If a scalar is desired
+ where normally a list would be present, such as a bi-directional
+ one-to-one relationship, use an appropriate :class:`_orm.Mapped`
+ annotation or set :paramref:`_orm.relationship.uselist` to False.
+
+ The :paramref:`_orm.relationship.uselist`
+ flag is also available on an
+ existing :func:`_orm.relationship`
+ construct as a read-only attribute,
+ which can be used to determine if this :func:`_orm.relationship`
+ deals
+ with collections or scalar attributes::
+
+ >>> User.addresses.property.uselist
+ True
+
+ .. seealso::
+
+ :ref:`relationships_one_to_one` - Introduction to the "one to
+ one" relationship pattern, which is typically when an alternate
+ setting for :paramref:`_orm.relationship.uselist` is involved.
+
+ :param viewonly=False:
+ When set to ``True``, the relationship is used only for loading
+ objects, and not for any persistence operation. A
+ :func:`_orm.relationship` which specifies
+ :paramref:`_orm.relationship.viewonly` can work
+ with a wider range of SQL operations within the
+ :paramref:`_orm.relationship.primaryjoin` condition, including
+ operations that feature the use of a variety of comparison operators
+ as well as SQL functions such as :func:`_expression.cast`. The
+ :paramref:`_orm.relationship.viewonly`
+ flag is also of general use when defining any kind of
+ :func:`_orm.relationship` that doesn't represent
+ the full set of related objects, to prevent modifications of the
+ collection from resulting in persistence operations.
+
+ .. seealso::
+
+ :ref:`relationship_viewonly_notes` - more details on best practices
+ when using :paramref:`_orm.relationship.viewonly`.
+
+ :param sync_backref:
+ A boolean that enables the events used to synchronize the in-Python
+ attributes when this relationship is target of either
+ :paramref:`_orm.relationship.backref` or
+ :paramref:`_orm.relationship.back_populates`.
+
+ Defaults to ``None``, which indicates that an automatic value should
+ be selected based on the value of the
+ :paramref:`_orm.relationship.viewonly` flag. When left at its
+ default, changes in state will be back-populated only if neither
+ sides of a relationship is viewonly.
+
+ .. versionadded:: 1.3.17
+
+ .. versionchanged:: 1.4 - A relationship that specifies
+ :paramref:`_orm.relationship.viewonly` automatically implies
+ that :paramref:`_orm.relationship.sync_backref` is ``False``.
+
+ .. seealso::
+
+ :paramref:`_orm.relationship.viewonly`
+
+ :param omit_join:
+ Allows manual control over the "selectin" automatic join
+ optimization. Set to ``False`` to disable the "omit join" feature
+ added in SQLAlchemy 1.3; or leave as ``None`` to leave automatic
+ optimization in place.
+
+ .. note:: This flag may only be set to ``False``. It is not
+ necessary to set it to ``True`` as the "omit_join" optimization is
+ automatically detected; if it is not detected, then the
+ optimization is not supported.
+
+ .. versionchanged:: 1.3.11 setting ``omit_join`` to True will now
+ emit a warning as this was not the intended use of this flag.
+
+ .. versionadded:: 1.3
+
+ :param init: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__init__()``
+ method as generated by the dataclass process.
+ :param repr: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__repr__()``
+ method as generated by the dataclass process.
+ :param default_factory: Specific to
+ :ref:`orm_declarative_native_dataclasses`,
+ specifies a default-value generation function that will take place
+ as part of the ``__init__()``
+ method as generated by the dataclass process.
+ :param compare: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be included in comparison operations when generating the
+ ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+ .. versionadded:: 2.0.0b4
+
+ :param kw_only: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be marked as keyword-only when generating the ``__init__()``.
+
+
+ """
+
+ return _RelationshipDeclared(
+ argument,
+ secondary=secondary,
+ uselist=uselist,
+ collection_class=collection_class,
+ primaryjoin=primaryjoin,
+ secondaryjoin=secondaryjoin,
+ back_populates=back_populates,
+ order_by=order_by,
+ backref=backref,
+ overlaps=overlaps,
+ post_update=post_update,
+ cascade=cascade,
+ viewonly=viewonly,
+ attribute_options=_AttributeOptions(
+ init, repr, default, default_factory, compare, kw_only
+ ),
+ lazy=lazy,
+ passive_deletes=passive_deletes,
+ passive_updates=passive_updates,
+ active_history=active_history,
+ enable_typechecks=enable_typechecks,
+ foreign_keys=foreign_keys,
+ remote_side=remote_side,
+ join_depth=join_depth,
+ comparator_factory=comparator_factory,
+ single_parent=single_parent,
+ innerjoin=innerjoin,
+ distinct_target_key=distinct_target_key,
+ load_on_pending=load_on_pending,
+ query_class=query_class,
+ info=info,
+ omit_join=omit_join,
+ sync_backref=sync_backref,
+ **kw,
+ )
+
+
+def synonym(
+ name: str,
+ *,
+ map_column: Optional[bool] = None,
+ descriptor: Optional[Any] = None,
+ comparator_factory: Optional[Type[PropComparator[_T]]] = None,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Union[_NoArg, _T] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+) -> Synonym[Any]:
+ """Denote an attribute name as a synonym to a mapped property,
+ in that the attribute will mirror the value and expression behavior
+ of another attribute.
+
+ e.g.::
+
+ class MyClass(Base):
+ __tablename__ = 'my_table'
+
+ id = Column(Integer, primary_key=True)
+ job_status = Column(String(50))
+
+ status = synonym("job_status")
+
+
+ :param name: the name of the existing mapped property. This
+ can refer to the string name ORM-mapped attribute
+ configured on the class, including column-bound attributes
+ and relationships.
+
+ :param descriptor: a Python :term:`descriptor` that will be used
+ as a getter (and potentially a setter) when this attribute is
+ accessed at the instance level.
+
+ :param map_column: **For classical mappings and mappings against
+ an existing Table object only**. if ``True``, the :func:`.synonym`
+ construct will locate the :class:`_schema.Column`
+ object upon the mapped
+ table that would normally be associated with the attribute name of
+ this synonym, and produce a new :class:`.ColumnProperty` that instead
+ maps this :class:`_schema.Column`
+ to the alternate name given as the "name"
+ argument of the synonym; in this way, the usual step of redefining
+ the mapping of the :class:`_schema.Column`
+ to be under a different name is
+ unnecessary. This is usually intended to be used when a
+ :class:`_schema.Column`
+ is to be replaced with an attribute that also uses a
+ descriptor, that is, in conjunction with the
+ :paramref:`.synonym.descriptor` parameter::
+
+ my_table = Table(
+ "my_table", metadata,
+ Column('id', Integer, primary_key=True),
+ Column('job_status', String(50))
+ )
+
+ class MyClass:
+ @property
+ def _job_status_descriptor(self):
+ return "Status: %s" % self._job_status
+
+
+ mapper(
+ MyClass, my_table, properties={
+ "job_status": synonym(
+ "_job_status", map_column=True,
+ descriptor=MyClass._job_status_descriptor)
+ }
+ )
+
+ Above, the attribute named ``_job_status`` is automatically
+ mapped to the ``job_status`` column::
+
+ >>> j1 = MyClass()
+ >>> j1._job_status = "employed"
+ >>> j1.job_status
+ Status: employed
+
+ When using Declarative, in order to provide a descriptor in
+ conjunction with a synonym, use the
+ :func:`sqlalchemy.ext.declarative.synonym_for` helper. However,
+ note that the :ref:`hybrid properties <mapper_hybrids>` feature
+ should usually be preferred, particularly when redefining attribute
+ behavior.
+
+ :param info: Optional data dictionary which will be populated into the
+ :attr:`.InspectionAttr.info` attribute of this object.
+
+ :param comparator_factory: A subclass of :class:`.PropComparator`
+ that will provide custom comparison behavior at the SQL expression
+ level.
+
+ .. note::
+
+ For the use case of providing an attribute which redefines both
+ Python-level and SQL-expression level behavior of an attribute,
+ please refer to the Hybrid attribute introduced at
+ :ref:`mapper_hybrids` for a more effective technique.
+
+ .. seealso::
+
+ :ref:`synonyms` - Overview of synonyms
+
+ :func:`.synonym_for` - a helper oriented towards Declarative
+
+ :ref:`mapper_hybrids` - The Hybrid Attribute extension provides an
+ updated approach to augmenting attribute behavior more flexibly
+ than can be achieved with synonyms.
+
+ """
+ return Synonym(
+ name,
+ map_column=map_column,
+ descriptor=descriptor,
+ comparator_factory=comparator_factory,
+ attribute_options=_AttributeOptions(
+ init, repr, default, default_factory, compare, kw_only
+ ),
+ doc=doc,
+ info=info,
+ )
+
+
+def create_session(
+ bind: Optional[_SessionBind] = None, **kwargs: Any
+) -> Session:
+ r"""Create a new :class:`.Session`
+ with no automation enabled by default.
+
+ This function is used primarily for testing. The usual
+ route to :class:`.Session` creation is via its constructor
+ or the :func:`.sessionmaker` function.
+
+ :param bind: optional, a single Connectable to use for all
+ database access in the created
+ :class:`~sqlalchemy.orm.session.Session`.
+
+ :param \*\*kwargs: optional, passed through to the
+ :class:`.Session` constructor.
+
+ :returns: an :class:`~sqlalchemy.orm.session.Session` instance
+
+ The defaults of create_session() are the opposite of that of
+ :func:`sessionmaker`; ``autoflush`` and ``expire_on_commit`` are
+ False.
+
+ Usage::
+
+ >>> from sqlalchemy.orm import create_session
+ >>> session = create_session()
+
+ It is recommended to use :func:`sessionmaker` instead of
+ create_session().
+
+ """
+
+ kwargs.setdefault("autoflush", False)
+ kwargs.setdefault("expire_on_commit", False)
+ return Session(bind=bind, **kwargs)
+
+
+def _mapper_fn(*arg: Any, **kw: Any) -> NoReturn:
+ """Placeholder for the now-removed ``mapper()`` function.
+
+ Classical mappings should be performed using the
+ :meth:`_orm.registry.map_imperatively` method.
+
+ This symbol remains in SQLAlchemy 2.0 to suit the deprecated use case
+ of using the ``mapper()`` function as a target for ORM event listeners,
+ which failed to be marked as deprecated in the 1.4 series.
+
+ Global ORM mapper listeners should instead use the :class:`_orm.Mapper`
+ class as the target.
+
+ .. versionchanged:: 2.0 The ``mapper()`` function was removed; the
+ symbol remains temporarily as a placeholder for the event listening
+ use case.
+
+ """
+ raise InvalidRequestError(
+ "The 'sqlalchemy.orm.mapper()' function is removed as of "
+ "SQLAlchemy 2.0. Use the "
+ "'sqlalchemy.orm.registry.map_imperatively()` "
+ "method of the ``sqlalchemy.orm.registry`` class to perform "
+ "classical mapping."
+ )
+
+
+def dynamic_loader(
+ argument: Optional[_RelationshipArgumentType[Any]] = None, **kw: Any
+) -> RelationshipProperty[Any]:
+ """Construct a dynamically-loading mapper property.
+
+ This is essentially the same as
+ using the ``lazy='dynamic'`` argument with :func:`relationship`::
+
+ dynamic_loader(SomeClass)
+
+ # is the same as
+
+ relationship(SomeClass, lazy="dynamic")
+
+ See the section :ref:`dynamic_relationship` for more details
+ on dynamic loading.
+
+ """
+ kw["lazy"] = "dynamic"
+ return relationship(argument, **kw)
+
+
+def backref(name: str, **kwargs: Any) -> ORMBackrefArgument:
+ """When using the :paramref:`_orm.relationship.backref` parameter,
+ provides specific parameters to be used when the new
+ :func:`_orm.relationship` is generated.
+
+ E.g.::
+
+ 'items':relationship(
+ SomeItem, backref=backref('parent', lazy='subquery'))
+
+ The :paramref:`_orm.relationship.backref` parameter is generally
+ considered to be legacy; for modern applications, using
+ explicit :func:`_orm.relationship` constructs linked together using
+ the :paramref:`_orm.relationship.back_populates` parameter should be
+ preferred.
+
+ .. seealso::
+
+ :ref:`relationships_backref` - background on backrefs
+
+ """
+
+ return (name, kwargs)
+
+
+def deferred(
+ column: _ORMColumnExprArgument[_T],
+ *additional_columns: _ORMColumnExprArgument[Any],
+ group: Optional[str] = None,
+ raiseload: bool = False,
+ comparator_factory: Optional[Type[PropComparator[_T]]] = None,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Optional[Any] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ active_history: bool = False,
+ expire_on_flush: bool = True,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+) -> MappedSQLExpression[_T]:
+ r"""Indicate a column-based mapped attribute that by default will
+ not load unless accessed.
+
+ When using :func:`_orm.mapped_column`, the same functionality as
+ that of :func:`_orm.deferred` construct is provided by using the
+ :paramref:`_orm.mapped_column.deferred` parameter.
+
+ :param \*columns: columns to be mapped. This is typically a single
+ :class:`_schema.Column` object,
+ however a collection is supported in order
+ to support multiple columns mapped under the same attribute.
+
+ :param raiseload: boolean, if True, indicates an exception should be raised
+ if the load operation is to take place.
+
+ .. versionadded:: 1.4
+
+
+ Additional arguments are the same as that of :func:`_orm.column_property`.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_deferred_imperative`
+
+ """
+ return MappedSQLExpression(
+ column,
+ *additional_columns,
+ attribute_options=_AttributeOptions(
+ init, repr, default, default_factory, compare, kw_only
+ ),
+ group=group,
+ deferred=True,
+ raiseload=raiseload,
+ comparator_factory=comparator_factory,
+ active_history=active_history,
+ expire_on_flush=expire_on_flush,
+ info=info,
+ doc=doc,
+ )
+
+
+def query_expression(
+ default_expr: _ORMColumnExprArgument[_T] = sql.null(),
+ *,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ expire_on_flush: bool = True,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+) -> MappedSQLExpression[_T]:
+ """Indicate an attribute that populates from a query-time SQL expression.
+
+ :param default_expr: Optional SQL expression object that will be used in
+ all cases if not assigned later with :func:`_orm.with_expression`.
+
+ .. versionadded:: 1.2
+
+ .. seealso::
+
+ :ref:`orm_queryguide_with_expression` - background and usage examples
+
+ """
+ prop = MappedSQLExpression(
+ default_expr,
+ attribute_options=_AttributeOptions(
+ False,
+ repr,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ compare,
+ _NoArg.NO_ARG,
+ ),
+ expire_on_flush=expire_on_flush,
+ info=info,
+ doc=doc,
+ _assume_readonly_dc_attributes=True,
+ )
+
+ prop.strategy_key = (("query_expression", True),)
+ return prop
+
+
+def clear_mappers() -> None:
+ """Remove all mappers from all classes.
+
+ .. versionchanged:: 1.4 This function now locates all
+ :class:`_orm.registry` objects and calls upon the
+ :meth:`_orm.registry.dispose` method of each.
+
+ This function removes all instrumentation from classes and disposes
+ of their associated mappers. Once called, the classes are unmapped
+ and can be later re-mapped with new mappers.
+
+ :func:`.clear_mappers` is *not* for normal use, as there is literally no
+ valid usage for it outside of very specific testing scenarios. Normally,
+ mappers are permanent structural components of user-defined classes, and
+ are never discarded independently of their class. If a mapped class
+ itself is garbage collected, its mapper is automatically disposed of as
+ well. As such, :func:`.clear_mappers` is only for usage in test suites
+ that re-use the same classes with different mappings, which is itself an
+ extremely rare use case - the only such use case is in fact SQLAlchemy's
+ own test suite, and possibly the test suites of other ORM extension
+ libraries which intend to test various combinations of mapper construction
+ upon a fixed set of classes.
+
+ """
+
+ mapperlib._dispose_registries(mapperlib._all_registries(), False)
+
+
+# I would really like a way to get the Type[] here that shows up
+# in a different way in typing tools, however there is no current method
+# that is accepted by mypy (subclass of Type[_O] works in pylance, rejected
+# by mypy).
+AliasedType = Annotated[Type[_O], "aliased"]
+
+
+@overload
+def aliased(
+ element: Type[_O],
+ alias: Optional[FromClause] = None,
+ name: Optional[str] = None,
+ flat: bool = False,
+ adapt_on_names: bool = False,
+) -> AliasedType[_O]: ...
+
+
+@overload
+def aliased(
+ element: Union[AliasedClass[_O], Mapper[_O], AliasedInsp[_O]],
+ alias: Optional[FromClause] = None,
+ name: Optional[str] = None,
+ flat: bool = False,
+ adapt_on_names: bool = False,
+) -> AliasedClass[_O]: ...
+
+
+@overload
+def aliased(
+ element: FromClause,
+ alias: None = None,
+ name: Optional[str] = None,
+ flat: bool = False,
+ adapt_on_names: bool = False,
+) -> FromClause: ...
+
+
+def aliased(
+ element: Union[_EntityType[_O], FromClause],
+ alias: Optional[FromClause] = None,
+ name: Optional[str] = None,
+ flat: bool = False,
+ adapt_on_names: bool = False,
+) -> Union[AliasedClass[_O], FromClause, AliasedType[_O]]:
+ """Produce an alias of the given element, usually an :class:`.AliasedClass`
+ instance.
+
+ E.g.::
+
+ my_alias = aliased(MyClass)
+
+ stmt = select(MyClass, my_alias).filter(MyClass.id > my_alias.id)
+ result = session.execute(stmt)
+
+ The :func:`.aliased` function is used to create an ad-hoc mapping of a
+ mapped class to a new selectable. By default, a selectable is generated
+ from the normally mapped selectable (typically a :class:`_schema.Table`
+ ) using the
+ :meth:`_expression.FromClause.alias` method. However, :func:`.aliased`
+ can also be
+ used to link the class to a new :func:`_expression.select` statement.
+ Also, the :func:`.with_polymorphic` function is a variant of
+ :func:`.aliased` that is intended to specify a so-called "polymorphic
+ selectable", that corresponds to the union of several joined-inheritance
+ subclasses at once.
+
+ For convenience, the :func:`.aliased` function also accepts plain
+ :class:`_expression.FromClause` constructs, such as a
+ :class:`_schema.Table` or
+ :func:`_expression.select` construct. In those cases, the
+ :meth:`_expression.FromClause.alias`
+ method is called on the object and the new
+ :class:`_expression.Alias` object returned. The returned
+ :class:`_expression.Alias` is not
+ ORM-mapped in this case.
+
+ .. seealso::
+
+ :ref:`tutorial_orm_entity_aliases` - in the :ref:`unified_tutorial`
+
+ :ref:`orm_queryguide_orm_aliases` - in the :ref:`queryguide_toplevel`
+
+ :param element: element to be aliased. Is normally a mapped class,
+ but for convenience can also be a :class:`_expression.FromClause`
+ element.
+
+ :param alias: Optional selectable unit to map the element to. This is
+ usually used to link the object to a subquery, and should be an aliased
+ select construct as one would produce from the
+ :meth:`_query.Query.subquery` method or
+ the :meth:`_expression.Select.subquery` or
+ :meth:`_expression.Select.alias` methods of the :func:`_expression.select`
+ construct.
+
+ :param name: optional string name to use for the alias, if not specified
+ by the ``alias`` parameter. The name, among other things, forms the
+ attribute name that will be accessible via tuples returned by a
+ :class:`_query.Query` object. Not supported when creating aliases
+ of :class:`_sql.Join` objects.
+
+ :param flat: Boolean, will be passed through to the
+ :meth:`_expression.FromClause.alias` call so that aliases of
+ :class:`_expression.Join` objects will alias the individual tables
+ inside the join, rather than creating a subquery. This is generally
+ supported by all modern databases with regards to right-nested joins
+ and generally produces more efficient queries.
+
+ :param adapt_on_names: if True, more liberal "matching" will be used when
+ mapping the mapped columns of the ORM entity to those of the
+ given selectable - a name-based match will be performed if the
+ given selectable doesn't otherwise have a column that corresponds
+ to one on the entity. The use case for this is when associating
+ an entity with some derived selectable such as one that uses
+ aggregate functions::
+
+ class UnitPrice(Base):
+ __tablename__ = 'unit_price'
+ ...
+ unit_id = Column(Integer)
+ price = Column(Numeric)
+
+ aggregated_unit_price = Session.query(
+ func.sum(UnitPrice.price).label('price')
+ ).group_by(UnitPrice.unit_id).subquery()
+
+ aggregated_unit_price = aliased(UnitPrice,
+ alias=aggregated_unit_price, adapt_on_names=True)
+
+ Above, functions on ``aggregated_unit_price`` which refer to
+ ``.price`` will return the
+ ``func.sum(UnitPrice.price).label('price')`` column, as it is
+ matched on the name "price". Ordinarily, the "price" function
+ wouldn't have any "column correspondence" to the actual
+ ``UnitPrice.price`` column as it is not a proxy of the original.
+
+ """
+ return AliasedInsp._alias_factory(
+ element,
+ alias=alias,
+ name=name,
+ flat=flat,
+ adapt_on_names=adapt_on_names,
+ )
+
+
+def with_polymorphic(
+ base: Union[Type[_O], Mapper[_O]],
+ classes: Union[Literal["*"], Iterable[Type[Any]]],
+ selectable: Union[Literal[False, None], FromClause] = False,
+ flat: bool = False,
+ polymorphic_on: Optional[ColumnElement[Any]] = None,
+ aliased: bool = False,
+ innerjoin: bool = False,
+ adapt_on_names: bool = False,
+ _use_mapper_path: bool = False,
+) -> AliasedClass[_O]:
+ """Produce an :class:`.AliasedClass` construct which specifies
+ columns for descendant mappers of the given base.
+
+ Using this method will ensure that each descendant mapper's
+ tables are included in the FROM clause, and will allow filter()
+ criterion to be used against those tables. The resulting
+ instances will also have those columns already loaded so that
+ no "post fetch" of those columns will be required.
+
+ .. seealso::
+
+ :ref:`with_polymorphic` - full discussion of
+ :func:`_orm.with_polymorphic`.
+
+ :param base: Base class to be aliased.
+
+ :param classes: a single class or mapper, or list of
+ class/mappers, which inherit from the base class.
+ Alternatively, it may also be the string ``'*'``, in which case
+ all descending mapped classes will be added to the FROM clause.
+
+ :param aliased: when True, the selectable will be aliased. For a
+ JOIN, this means the JOIN will be SELECTed from inside of a subquery
+ unless the :paramref:`_orm.with_polymorphic.flat` flag is set to
+ True, which is recommended for simpler use cases.
+
+ :param flat: Boolean, will be passed through to the
+ :meth:`_expression.FromClause.alias` call so that aliases of
+ :class:`_expression.Join` objects will alias the individual tables
+ inside the join, rather than creating a subquery. This is generally
+ supported by all modern databases with regards to right-nested joins
+ and generally produces more efficient queries. Setting this flag is
+ recommended as long as the resulting SQL is functional.
+
+ :param selectable: a table or subquery that will
+ be used in place of the generated FROM clause. This argument is
+ required if any of the desired classes use concrete table
+ inheritance, since SQLAlchemy currently cannot generate UNIONs
+ among tables automatically. If used, the ``selectable`` argument
+ must represent the full set of tables and columns mapped by every
+ mapped class. Otherwise, the unaccounted mapped columns will
+ result in their table being appended directly to the FROM clause
+ which will usually lead to incorrect results.
+
+ When left at its default value of ``False``, the polymorphic
+ selectable assigned to the base mapper is used for selecting rows.
+ However, it may also be passed as ``None``, which will bypass the
+ configured polymorphic selectable and instead construct an ad-hoc
+ selectable for the target classes given; for joined table inheritance
+ this will be a join that includes all target mappers and their
+ subclasses.
+
+ :param polymorphic_on: a column to be used as the "discriminator"
+ column for the given selectable. If not given, the polymorphic_on
+ attribute of the base classes' mapper will be used, if any. This
+ is useful for mappings that don't have polymorphic loading
+ behavior by default.
+
+ :param innerjoin: if True, an INNER JOIN will be used. This should
+ only be specified if querying for one specific subtype only
+
+ :param adapt_on_names: Passes through the
+ :paramref:`_orm.aliased.adapt_on_names`
+ parameter to the aliased object. This may be useful in situations where
+ the given selectable is not directly related to the existing mapped
+ selectable.
+
+ .. versionadded:: 1.4.33
+
+ """
+ return AliasedInsp._with_polymorphic_factory(
+ base,
+ classes,
+ selectable=selectable,
+ flat=flat,
+ polymorphic_on=polymorphic_on,
+ adapt_on_names=adapt_on_names,
+ aliased=aliased,
+ innerjoin=innerjoin,
+ _use_mapper_path=_use_mapper_path,
+ )
+
+
+def join(
+ left: _FromClauseArgument,
+ right: _FromClauseArgument,
+ onclause: Optional[_OnClauseArgument] = None,
+ isouter: bool = False,
+ full: bool = False,
+) -> _ORMJoin:
+ r"""Produce an inner join between left and right clauses.
+
+ :func:`_orm.join` is an extension to the core join interface
+ provided by :func:`_expression.join()`, where the
+ left and right selectable may be not only core selectable
+ objects such as :class:`_schema.Table`, but also mapped classes or
+ :class:`.AliasedClass` instances. The "on" clause can
+ be a SQL expression or an ORM mapped attribute
+ referencing a configured :func:`_orm.relationship`.
+
+ :func:`_orm.join` is not commonly needed in modern usage,
+ as its functionality is encapsulated within that of the
+ :meth:`_sql.Select.join` and :meth:`_query.Query.join`
+ methods. which feature a
+ significant amount of automation beyond :func:`_orm.join`
+ by itself. Explicit use of :func:`_orm.join`
+ with ORM-enabled SELECT statements involves use of the
+ :meth:`_sql.Select.select_from` method, as in::
+
+ from sqlalchemy.orm import join
+ stmt = select(User).\
+ select_from(join(User, Address, User.addresses)).\
+ filter(Address.email_address=='foo@bar.com')
+
+ In modern SQLAlchemy the above join can be written more
+ succinctly as::
+
+ stmt = select(User).\
+ join(User.addresses).\
+ filter(Address.email_address=='foo@bar.com')
+
+ .. warning:: using :func:`_orm.join` directly may not work properly
+ with modern ORM options such as :func:`_orm.with_loader_criteria`.
+ It is strongly recommended to use the idiomatic join patterns
+ provided by methods such as :meth:`.Select.join` and
+ :meth:`.Select.join_from` when creating ORM joins.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_joins` - in the :ref:`queryguide_toplevel` for
+ background on idiomatic ORM join patterns
+
+ """
+ return _ORMJoin(left, right, onclause, isouter, full)
+
+
+def outerjoin(
+ left: _FromClauseArgument,
+ right: _FromClauseArgument,
+ onclause: Optional[_OnClauseArgument] = None,
+ full: bool = False,
+) -> _ORMJoin:
+ """Produce a left outer join between left and right clauses.
+
+ This is the "outer join" version of the :func:`_orm.join` function,
+ featuring the same behavior except that an OUTER JOIN is generated.
+ See that function's documentation for other usage details.
+
+ """
+ return _ORMJoin(left, right, onclause, True, full)
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/_typing.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/_typing.py
new file mode 100644
index 0000000..f8ac059
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/_typing.py
@@ -0,0 +1,179 @@
+# orm/_typing.py
+# Copyright (C) 2022-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+from __future__ import annotations
+
+import operator
+from typing import Any
+from typing import Dict
+from typing import Mapping
+from typing import Optional
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from ..engine.interfaces import _CoreKnownExecutionOptions
+from ..sql import roles
+from ..sql._orm_types import DMLStrategyArgument as DMLStrategyArgument
+from ..sql._orm_types import (
+ SynchronizeSessionArgument as SynchronizeSessionArgument,
+)
+from ..sql._typing import _HasClauseElement
+from ..sql.elements import ColumnElement
+from ..util.typing import Protocol
+from ..util.typing import TypeGuard
+
+if TYPE_CHECKING:
+ from .attributes import AttributeImpl
+ from .attributes import CollectionAttributeImpl
+ from .attributes import HasCollectionAdapter
+ from .attributes import QueryableAttribute
+ from .base import PassiveFlag
+ from .decl_api import registry as _registry_type
+ from .interfaces import InspectionAttr
+ from .interfaces import MapperProperty
+ from .interfaces import ORMOption
+ from .interfaces import UserDefinedOption
+ from .mapper import Mapper
+ from .relationships import RelationshipProperty
+ from .state import InstanceState
+ from .util import AliasedClass
+ from .util import AliasedInsp
+ from ..sql._typing import _CE
+ from ..sql.base import ExecutableOption
+
+_T = TypeVar("_T", bound=Any)
+
+
+_T_co = TypeVar("_T_co", bound=Any, covariant=True)
+
+_O = TypeVar("_O", bound=object)
+"""The 'ORM mapped object' type.
+
+"""
+
+
+if TYPE_CHECKING:
+ _RegistryType = _registry_type
+
+_InternalEntityType = Union["Mapper[_T]", "AliasedInsp[_T]"]
+
+_ExternalEntityType = Union[Type[_T], "AliasedClass[_T]"]
+
+_EntityType = Union[
+ Type[_T], "AliasedClass[_T]", "Mapper[_T]", "AliasedInsp[_T]"
+]
+
+
+_ClassDict = Mapping[str, Any]
+_InstanceDict = Dict[str, Any]
+
+_IdentityKeyType = Tuple[Type[_T], Tuple[Any, ...], Optional[Any]]
+
+_ORMColumnExprArgument = Union[
+ ColumnElement[_T],
+ _HasClauseElement[_T],
+ roles.ExpressionElementRole[_T],
+]
+
+
+_ORMCOLEXPR = TypeVar("_ORMCOLEXPR", bound=ColumnElement[Any])
+
+
+class _OrmKnownExecutionOptions(_CoreKnownExecutionOptions, total=False):
+ populate_existing: bool
+ autoflush: bool
+ synchronize_session: SynchronizeSessionArgument
+ dml_strategy: DMLStrategyArgument
+ is_delete_using: bool
+ is_update_from: bool
+ render_nulls: bool
+
+
+OrmExecuteOptionsParameter = Union[
+ _OrmKnownExecutionOptions, Mapping[str, Any]
+]
+
+
+class _ORMAdapterProto(Protocol):
+ """protocol for the :class:`.AliasedInsp._orm_adapt_element` method
+ which is a synonym for :class:`.AliasedInsp._adapt_element`.
+
+
+ """
+
+ def __call__(self, obj: _CE, key: Optional[str] = None) -> _CE: ...
+
+
+class _LoaderCallable(Protocol):
+ def __call__(
+ self, state: InstanceState[Any], passive: PassiveFlag
+ ) -> Any: ...
+
+
+def is_orm_option(
+ opt: ExecutableOption,
+) -> TypeGuard[ORMOption]:
+ return not opt._is_core
+
+
+def is_user_defined_option(
+ opt: ExecutableOption,
+) -> TypeGuard[UserDefinedOption]:
+ return not opt._is_core and opt._is_user_defined # type: ignore
+
+
+def is_composite_class(obj: Any) -> bool:
+ # inlining is_dataclass(obj)
+ return hasattr(obj, "__composite_values__") or hasattr(
+ obj, "__dataclass_fields__"
+ )
+
+
+if TYPE_CHECKING:
+
+ def insp_is_mapper_property(
+ obj: Any,
+ ) -> TypeGuard[MapperProperty[Any]]: ...
+
+ def insp_is_mapper(obj: Any) -> TypeGuard[Mapper[Any]]: ...
+
+ def insp_is_aliased_class(obj: Any) -> TypeGuard[AliasedInsp[Any]]: ...
+
+ def insp_is_attribute(
+ obj: InspectionAttr,
+ ) -> TypeGuard[QueryableAttribute[Any]]: ...
+
+ def attr_is_internal_proxy(
+ obj: InspectionAttr,
+ ) -> TypeGuard[QueryableAttribute[Any]]: ...
+
+ def prop_is_relationship(
+ prop: MapperProperty[Any],
+ ) -> TypeGuard[RelationshipProperty[Any]]: ...
+
+ def is_collection_impl(
+ impl: AttributeImpl,
+ ) -> TypeGuard[CollectionAttributeImpl]: ...
+
+ def is_has_collection_adapter(
+ impl: AttributeImpl,
+ ) -> TypeGuard[HasCollectionAdapter]: ...
+
+else:
+ insp_is_mapper_property = operator.attrgetter("is_property")
+ insp_is_mapper = operator.attrgetter("is_mapper")
+ insp_is_aliased_class = operator.attrgetter("is_aliased_class")
+ insp_is_attribute = operator.attrgetter("is_attribute")
+ attr_is_internal_proxy = operator.attrgetter("_is_internal_proxy")
+ is_collection_impl = operator.attrgetter("collection")
+ prop_is_relationship = operator.attrgetter("_is_relationship")
+ is_has_collection_adapter = operator.attrgetter(
+ "_is_has_collection_adapter"
+ )
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/attributes.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/attributes.py
new file mode 100644
index 0000000..5b16ce3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/attributes.py
@@ -0,0 +1,2835 @@
+# orm/attributes.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: allow-untyped-defs, allow-untyped-calls
+
+"""Defines instrumentation for class attributes and their interaction
+with instances.
+
+This module is usually not directly visible to user applications, but
+defines a large part of the ORM's interactivity.
+
+
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import operator
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import ClassVar
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import NamedTuple
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import collections
+from . import exc as orm_exc
+from . import interfaces
+from ._typing import insp_is_aliased_class
+from .base import _DeclarativeMapped
+from .base import ATTR_EMPTY
+from .base import ATTR_WAS_SET
+from .base import CALLABLES_OK
+from .base import DEFERRED_HISTORY_LOAD
+from .base import INCLUDE_PENDING_MUTATIONS # noqa
+from .base import INIT_OK
+from .base import instance_dict as instance_dict
+from .base import instance_state as instance_state
+from .base import instance_str
+from .base import LOAD_AGAINST_COMMITTED
+from .base import LoaderCallableStatus
+from .base import manager_of_class as manager_of_class
+from .base import Mapped as Mapped # noqa
+from .base import NEVER_SET # noqa
+from .base import NO_AUTOFLUSH
+from .base import NO_CHANGE # noqa
+from .base import NO_KEY
+from .base import NO_RAISE
+from .base import NO_VALUE
+from .base import NON_PERSISTENT_OK # noqa
+from .base import opt_manager_of_class as opt_manager_of_class
+from .base import PASSIVE_CLASS_MISMATCH # noqa
+from .base import PASSIVE_NO_FETCH
+from .base import PASSIVE_NO_FETCH_RELATED # noqa
+from .base import PASSIVE_NO_INITIALIZE
+from .base import PASSIVE_NO_RESULT
+from .base import PASSIVE_OFF
+from .base import PASSIVE_ONLY_PERSISTENT
+from .base import PASSIVE_RETURN_NO_VALUE
+from .base import PassiveFlag
+from .base import RELATED_OBJECT_OK # noqa
+from .base import SQL_OK # noqa
+from .base import SQLORMExpression
+from .base import state_str
+from .. import event
+from .. import exc
+from .. import inspection
+from .. import util
+from ..event import dispatcher
+from ..event import EventTarget
+from ..sql import base as sql_base
+from ..sql import cache_key
+from ..sql import coercions
+from ..sql import roles
+from ..sql import visitors
+from ..sql.cache_key import HasCacheKey
+from ..sql.visitors import _TraverseInternalsType
+from ..sql.visitors import InternalTraversal
+from ..util.typing import Literal
+from ..util.typing import Self
+from ..util.typing import TypeGuard
+
+if TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _ExternalEntityType
+ from ._typing import _InstanceDict
+ from ._typing import _InternalEntityType
+ from ._typing import _LoaderCallable
+ from ._typing import _O
+ from .collections import _AdaptedCollectionProtocol
+ from .collections import CollectionAdapter
+ from .interfaces import MapperProperty
+ from .relationships import RelationshipProperty
+ from .state import InstanceState
+ from .util import AliasedInsp
+ from .writeonly import WriteOnlyAttributeImpl
+ from ..event.base import _Dispatch
+ from ..sql._typing import _ColumnExpressionArgument
+ from ..sql._typing import _DMLColumnArgument
+ from ..sql._typing import _InfoType
+ from ..sql._typing import _PropagateAttrsType
+ from ..sql.annotation import _AnnotationDict
+ from ..sql.elements import ColumnElement
+ from ..sql.elements import Label
+ from ..sql.operators import OperatorType
+ from ..sql.selectable import FromClause
+
+
+_T = TypeVar("_T")
+_T_co = TypeVar("_T_co", bound=Any, covariant=True)
+
+
+_AllPendingType = Sequence[
+ Tuple[Optional["InstanceState[Any]"], Optional[object]]
+]
+
+
+_UNKNOWN_ATTR_KEY = object()
+
+
+@inspection._self_inspects
+class QueryableAttribute(
+ _DeclarativeMapped[_T_co],
+ SQLORMExpression[_T_co],
+ interfaces.InspectionAttr,
+ interfaces.PropComparator[_T_co],
+ roles.JoinTargetRole,
+ roles.OnClauseRole,
+ sql_base.Immutable,
+ cache_key.SlotsMemoizedHasCacheKey,
+ util.MemoizedSlots,
+ EventTarget,
+):
+ """Base class for :term:`descriptor` objects that intercept
+ attribute events on behalf of a :class:`.MapperProperty`
+ object. The actual :class:`.MapperProperty` is accessible
+ via the :attr:`.QueryableAttribute.property`
+ attribute.
+
+
+ .. seealso::
+
+ :class:`.InstrumentedAttribute`
+
+ :class:`.MapperProperty`
+
+ :attr:`_orm.Mapper.all_orm_descriptors`
+
+ :attr:`_orm.Mapper.attrs`
+ """
+
+ __slots__ = (
+ "class_",
+ "key",
+ "impl",
+ "comparator",
+ "property",
+ "parent",
+ "expression",
+ "_of_type",
+ "_extra_criteria",
+ "_slots_dispatch",
+ "_propagate_attrs",
+ "_doc",
+ )
+
+ is_attribute = True
+
+ dispatch: dispatcher[QueryableAttribute[_T_co]]
+
+ class_: _ExternalEntityType[Any]
+ key: str
+ parententity: _InternalEntityType[Any]
+ impl: AttributeImpl
+ comparator: interfaces.PropComparator[_T_co]
+ _of_type: Optional[_InternalEntityType[Any]]
+ _extra_criteria: Tuple[ColumnElement[bool], ...]
+ _doc: Optional[str]
+
+ # PropComparator has a __visit_name__ to participate within
+ # traversals. Disambiguate the attribute vs. a comparator.
+ __visit_name__ = "orm_instrumented_attribute"
+
+ def __init__(
+ self,
+ class_: _ExternalEntityType[_O],
+ key: str,
+ parententity: _InternalEntityType[_O],
+ comparator: interfaces.PropComparator[_T_co],
+ impl: Optional[AttributeImpl] = None,
+ of_type: Optional[_InternalEntityType[Any]] = None,
+ extra_criteria: Tuple[ColumnElement[bool], ...] = (),
+ ):
+ self.class_ = class_
+ self.key = key
+
+ self._parententity = self.parent = parententity
+
+ # this attribute is non-None after mappers are set up, however in the
+ # interim class manager setup, there's a check for None to see if it
+ # needs to be populated, so we assign None here leaving the attribute
+ # in a temporarily not-type-correct state
+ self.impl = impl # type: ignore
+
+ assert comparator is not None
+ self.comparator = comparator
+ self._of_type = of_type
+ self._extra_criteria = extra_criteria
+ self._doc = None
+
+ manager = opt_manager_of_class(class_)
+ # manager is None in the case of AliasedClass
+ if manager:
+ # propagate existing event listeners from
+ # immediate superclass
+ for base in manager._bases:
+ if key in base:
+ self.dispatch._update(base[key].dispatch)
+ if base[key].dispatch._active_history:
+ self.dispatch._active_history = True # type: ignore
+
+ _cache_key_traversal = [
+ ("key", visitors.ExtendedInternalTraversal.dp_string),
+ ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
+ ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
+ ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
+ ]
+
+ def __reduce__(self) -> Any:
+ # this method is only used in terms of the
+ # sqlalchemy.ext.serializer extension
+ return (
+ _queryable_attribute_unreduce,
+ (
+ self.key,
+ self._parententity.mapper.class_,
+ self._parententity,
+ self._parententity.entity,
+ ),
+ )
+
+ @property
+ def _impl_uses_objects(self) -> bool:
+ return self.impl.uses_objects
+
+ def get_history(
+ self, instance: Any, passive: PassiveFlag = PASSIVE_OFF
+ ) -> History:
+ return self.impl.get_history(
+ instance_state(instance), instance_dict(instance), passive
+ )
+
+ @property
+ def info(self) -> _InfoType:
+ """Return the 'info' dictionary for the underlying SQL element.
+
+ The behavior here is as follows:
+
+ * If the attribute is a column-mapped property, i.e.
+ :class:`.ColumnProperty`, which is mapped directly
+ to a schema-level :class:`_schema.Column` object, this attribute
+ will return the :attr:`.SchemaItem.info` dictionary associated
+ with the core-level :class:`_schema.Column` object.
+
+ * If the attribute is a :class:`.ColumnProperty` but is mapped to
+ any other kind of SQL expression other than a
+ :class:`_schema.Column`,
+ the attribute will refer to the :attr:`.MapperProperty.info`
+ dictionary associated directly with the :class:`.ColumnProperty`,
+ assuming the SQL expression itself does not have its own ``.info``
+ attribute (which should be the case, unless a user-defined SQL
+ construct has defined one).
+
+ * If the attribute refers to any other kind of
+ :class:`.MapperProperty`, including :class:`.Relationship`,
+ the attribute will refer to the :attr:`.MapperProperty.info`
+ dictionary associated with that :class:`.MapperProperty`.
+
+ * To access the :attr:`.MapperProperty.info` dictionary of the
+ :class:`.MapperProperty` unconditionally, including for a
+ :class:`.ColumnProperty` that's associated directly with a
+ :class:`_schema.Column`, the attribute can be referred to using
+ :attr:`.QueryableAttribute.property` attribute, as
+ ``MyClass.someattribute.property.info``.
+
+ .. seealso::
+
+ :attr:`.SchemaItem.info`
+
+ :attr:`.MapperProperty.info`
+
+ """
+ return self.comparator.info
+
+ parent: _InternalEntityType[Any]
+ """Return an inspection instance representing the parent.
+
+ This will be either an instance of :class:`_orm.Mapper`
+ or :class:`.AliasedInsp`, depending upon the nature
+ of the parent entity which this attribute is associated
+ with.
+
+ """
+
+ expression: ColumnElement[_T_co]
+ """The SQL expression object represented by this
+ :class:`.QueryableAttribute`.
+
+ This will typically be an instance of a :class:`_sql.ColumnElement`
+ subclass representing a column expression.
+
+ """
+
+ def _memoized_attr_expression(self) -> ColumnElement[_T]:
+ annotations: _AnnotationDict
+
+ # applies only to Proxy() as used by hybrid.
+ # currently is an exception to typing rather than feeding through
+ # non-string keys.
+ # ideally Proxy() would have a separate set of methods to deal
+ # with this case.
+ entity_namespace = self._entity_namespace
+ assert isinstance(entity_namespace, HasCacheKey)
+
+ if self.key is _UNKNOWN_ATTR_KEY:
+ annotations = {"entity_namespace": entity_namespace}
+ else:
+ annotations = {
+ "proxy_key": self.key,
+ "proxy_owner": self._parententity,
+ "entity_namespace": entity_namespace,
+ }
+
+ ce = self.comparator.__clause_element__()
+ try:
+ if TYPE_CHECKING:
+ assert isinstance(ce, ColumnElement)
+ anno = ce._annotate
+ except AttributeError as ae:
+ raise exc.InvalidRequestError(
+ 'When interpreting attribute "%s" as a SQL expression, '
+ "expected __clause_element__() to return "
+ "a ClauseElement object, got: %r" % (self, ce)
+ ) from ae
+ else:
+ return anno(annotations)
+
+ def _memoized_attr__propagate_attrs(self) -> _PropagateAttrsType:
+ # this suits the case in coercions where we don't actually
+ # call ``__clause_element__()`` but still need to get
+ # resolved._propagate_attrs. See #6558.
+ return util.immutabledict(
+ {
+ "compile_state_plugin": "orm",
+ "plugin_subject": self._parentmapper,
+ }
+ )
+
+ @property
+ def _entity_namespace(self) -> _InternalEntityType[Any]:
+ return self._parententity
+
+ @property
+ def _annotations(self) -> _AnnotationDict:
+ return self.__clause_element__()._annotations
+
+ def __clause_element__(self) -> ColumnElement[_T_co]:
+ return self.expression
+
+ @property
+ def _from_objects(self) -> List[FromClause]:
+ return self.expression._from_objects
+
+ def _bulk_update_tuples(
+ self, value: Any
+ ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
+ """Return setter tuples for a bulk UPDATE."""
+
+ return self.comparator._bulk_update_tuples(value)
+
+ def adapt_to_entity(self, adapt_to_entity: AliasedInsp[Any]) -> Self:
+ assert not self._of_type
+ return self.__class__(
+ adapt_to_entity.entity,
+ self.key,
+ impl=self.impl,
+ comparator=self.comparator.adapt_to_entity(adapt_to_entity),
+ parententity=adapt_to_entity,
+ )
+
+ def of_type(self, entity: _EntityType[Any]) -> QueryableAttribute[_T]:
+ return QueryableAttribute(
+ self.class_,
+ self.key,
+ self._parententity,
+ impl=self.impl,
+ comparator=self.comparator.of_type(entity),
+ of_type=inspection.inspect(entity),
+ extra_criteria=self._extra_criteria,
+ )
+
+ def and_(
+ self, *clauses: _ColumnExpressionArgument[bool]
+ ) -> QueryableAttribute[bool]:
+ if TYPE_CHECKING:
+ assert isinstance(self.comparator, RelationshipProperty.Comparator)
+
+ exprs = tuple(
+ coercions.expect(roles.WhereHavingRole, clause)
+ for clause in util.coerce_generator_arg(clauses)
+ )
+
+ return QueryableAttribute(
+ self.class_,
+ self.key,
+ self._parententity,
+ impl=self.impl,
+ comparator=self.comparator.and_(*exprs),
+ of_type=self._of_type,
+ extra_criteria=self._extra_criteria + exprs,
+ )
+
+ def _clone(self, **kw: Any) -> QueryableAttribute[_T]:
+ return QueryableAttribute(
+ self.class_,
+ self.key,
+ self._parententity,
+ impl=self.impl,
+ comparator=self.comparator,
+ of_type=self._of_type,
+ extra_criteria=self._extra_criteria,
+ )
+
+ def label(self, name: Optional[str]) -> Label[_T_co]:
+ return self.__clause_element__().label(name)
+
+ def operate(
+ self, op: OperatorType, *other: Any, **kwargs: Any
+ ) -> ColumnElement[Any]:
+ return op(self.comparator, *other, **kwargs) # type: ignore[no-any-return] # noqa: E501
+
+ def reverse_operate(
+ self, op: OperatorType, other: Any, **kwargs: Any
+ ) -> ColumnElement[Any]:
+ return op(other, self.comparator, **kwargs) # type: ignore[no-any-return] # noqa: E501
+
+ def hasparent(
+ self, state: InstanceState[Any], optimistic: bool = False
+ ) -> bool:
+ return self.impl.hasparent(state, optimistic=optimistic) is not False
+
+ def __getattr__(self, key: str) -> Any:
+ try:
+ return util.MemoizedSlots.__getattr__(self, key)
+ except AttributeError:
+ pass
+
+ try:
+ return getattr(self.comparator, key)
+ except AttributeError as err:
+ raise AttributeError(
+ "Neither %r object nor %r object associated with %s "
+ "has an attribute %r"
+ % (
+ type(self).__name__,
+ type(self.comparator).__name__,
+ self,
+ key,
+ )
+ ) from err
+
+ def __str__(self) -> str:
+ return f"{self.class_.__name__}.{self.key}"
+
+ def _memoized_attr_property(self) -> Optional[MapperProperty[Any]]:
+ return self.comparator.property
+
+
+def _queryable_attribute_unreduce(
+ key: str,
+ mapped_class: Type[_O],
+ parententity: _InternalEntityType[_O],
+ entity: _ExternalEntityType[Any],
+) -> Any:
+ # this method is only used in terms of the
+ # sqlalchemy.ext.serializer extension
+ if insp_is_aliased_class(parententity):
+ return entity._get_from_serialized(key, mapped_class, parententity)
+ else:
+ return getattr(entity, key)
+
+
+class InstrumentedAttribute(QueryableAttribute[_T_co]):
+ """Class bound instrumented attribute which adds basic
+ :term:`descriptor` methods.
+
+ See :class:`.QueryableAttribute` for a description of most features.
+
+
+ """
+
+ __slots__ = ()
+
+ inherit_cache = True
+ """:meta private:"""
+
+ # hack to make __doc__ writeable on instances of
+ # InstrumentedAttribute, while still keeping classlevel
+ # __doc__ correct
+
+ @util.rw_hybridproperty
+ def __doc__(self) -> Optional[str]:
+ return self._doc
+
+ @__doc__.setter # type: ignore
+ def __doc__(self, value: Optional[str]) -> None:
+ self._doc = value
+
+ @__doc__.classlevel # type: ignore
+ def __doc__(cls) -> Optional[str]:
+ return super().__doc__
+
+ def __set__(self, instance: object, value: Any) -> None:
+ self.impl.set(
+ instance_state(instance), instance_dict(instance), value, None
+ )
+
+ def __delete__(self, instance: object) -> None:
+ self.impl.delete(instance_state(instance), instance_dict(instance))
+
+ @overload
+ def __get__(
+ self, instance: None, owner: Any
+ ) -> InstrumentedAttribute[_T_co]: ...
+
+ @overload
+ def __get__(self, instance: object, owner: Any) -> _T_co: ...
+
+ def __get__(
+ self, instance: Optional[object], owner: Any
+ ) -> Union[InstrumentedAttribute[_T_co], _T_co]:
+ if instance is None:
+ return self
+
+ dict_ = instance_dict(instance)
+ if self.impl.supports_population and self.key in dict_:
+ return dict_[self.key] # type: ignore[no-any-return]
+ else:
+ try:
+ state = instance_state(instance)
+ except AttributeError as err:
+ raise orm_exc.UnmappedInstanceError(instance) from err
+ return self.impl.get(state, dict_) # type: ignore[no-any-return]
+
+
+@dataclasses.dataclass(frozen=True)
+class AdHocHasEntityNamespace(HasCacheKey):
+ _traverse_internals: ClassVar[_TraverseInternalsType] = [
+ ("_entity_namespace", InternalTraversal.dp_has_cache_key),
+ ]
+
+ # py37 compat, no slots=True on dataclass
+ __slots__ = ("_entity_namespace",)
+ _entity_namespace: _InternalEntityType[Any]
+ is_mapper: ClassVar[bool] = False
+ is_aliased_class: ClassVar[bool] = False
+
+ @property
+ def entity_namespace(self):
+ return self._entity_namespace.entity_namespace
+
+
+def create_proxied_attribute(
+ descriptor: Any,
+) -> Callable[..., QueryableAttribute[Any]]:
+ """Create an QueryableAttribute / user descriptor hybrid.
+
+ Returns a new QueryableAttribute type that delegates descriptor
+ behavior and getattr() to the given descriptor.
+ """
+
+ # TODO: can move this to descriptor_props if the need for this
+ # function is removed from ext/hybrid.py
+
+ class Proxy(QueryableAttribute[Any]):
+ """Presents the :class:`.QueryableAttribute` interface as a
+ proxy on top of a Python descriptor / :class:`.PropComparator`
+ combination.
+
+ """
+
+ _extra_criteria = ()
+
+ # the attribute error catches inside of __getattr__ basically create a
+ # singularity if you try putting slots on this too
+ # __slots__ = ("descriptor", "original_property", "_comparator")
+
+ def __init__(
+ self,
+ class_,
+ key,
+ descriptor,
+ comparator,
+ adapt_to_entity=None,
+ doc=None,
+ original_property=None,
+ ):
+ self.class_ = class_
+ self.key = key
+ self.descriptor = descriptor
+ self.original_property = original_property
+ self._comparator = comparator
+ self._adapt_to_entity = adapt_to_entity
+ self._doc = self.__doc__ = doc
+
+ @property
+ def _parententity(self):
+ return inspection.inspect(self.class_, raiseerr=False)
+
+ @property
+ def parent(self):
+ return inspection.inspect(self.class_, raiseerr=False)
+
+ _is_internal_proxy = True
+
+ _cache_key_traversal = [
+ ("key", visitors.ExtendedInternalTraversal.dp_string),
+ ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
+ ]
+
+ @property
+ def _impl_uses_objects(self):
+ return (
+ self.original_property is not None
+ and getattr(self.class_, self.key).impl.uses_objects
+ )
+
+ @property
+ def _entity_namespace(self):
+ if hasattr(self._comparator, "_parententity"):
+ return self._comparator._parententity
+ else:
+ # used by hybrid attributes which try to remain
+ # agnostic of any ORM concepts like mappers
+ return AdHocHasEntityNamespace(self._parententity)
+
+ @property
+ def property(self):
+ return self.comparator.property
+
+ @util.memoized_property
+ def comparator(self):
+ if callable(self._comparator):
+ self._comparator = self._comparator()
+ if self._adapt_to_entity:
+ self._comparator = self._comparator.adapt_to_entity(
+ self._adapt_to_entity
+ )
+ return self._comparator
+
+ def adapt_to_entity(self, adapt_to_entity):
+ return self.__class__(
+ adapt_to_entity.entity,
+ self.key,
+ self.descriptor,
+ self._comparator,
+ adapt_to_entity,
+ )
+
+ def _clone(self, **kw):
+ return self.__class__(
+ self.class_,
+ self.key,
+ self.descriptor,
+ self._comparator,
+ adapt_to_entity=self._adapt_to_entity,
+ original_property=self.original_property,
+ )
+
+ def __get__(self, instance, owner):
+ retval = self.descriptor.__get__(instance, owner)
+ # detect if this is a plain Python @property, which just returns
+ # itself for class level access. If so, then return us.
+ # Otherwise, return the object returned by the descriptor.
+ if retval is self.descriptor and instance is None:
+ return self
+ else:
+ return retval
+
+ def __str__(self) -> str:
+ return f"{self.class_.__name__}.{self.key}"
+
+ def __getattr__(self, attribute):
+ """Delegate __getattr__ to the original descriptor and/or
+ comparator."""
+
+ # this is unfortunately very complicated, and is easily prone
+ # to recursion overflows when implementations of related
+ # __getattr__ schemes are changed
+
+ try:
+ return util.MemoizedSlots.__getattr__(self, attribute)
+ except AttributeError:
+ pass
+
+ try:
+ return getattr(descriptor, attribute)
+ except AttributeError as err:
+ if attribute == "comparator":
+ raise AttributeError("comparator") from err
+ try:
+ # comparator itself might be unreachable
+ comparator = self.comparator
+ except AttributeError as err2:
+ raise AttributeError(
+ "Neither %r object nor unconfigured comparator "
+ "object associated with %s has an attribute %r"
+ % (type(descriptor).__name__, self, attribute)
+ ) from err2
+ else:
+ try:
+ return getattr(comparator, attribute)
+ except AttributeError as err3:
+ raise AttributeError(
+ "Neither %r object nor %r object "
+ "associated with %s has an attribute %r"
+ % (
+ type(descriptor).__name__,
+ type(comparator).__name__,
+ self,
+ attribute,
+ )
+ ) from err3
+
+ Proxy.__name__ = type(descriptor).__name__ + "Proxy"
+
+ util.monkeypatch_proxied_specials(
+ Proxy, type(descriptor), name="descriptor", from_instance=descriptor
+ )
+ return Proxy
+
+
+OP_REMOVE = util.symbol("REMOVE")
+OP_APPEND = util.symbol("APPEND")
+OP_REPLACE = util.symbol("REPLACE")
+OP_BULK_REPLACE = util.symbol("BULK_REPLACE")
+OP_MODIFIED = util.symbol("MODIFIED")
+
+
+class AttributeEventToken:
+ """A token propagated throughout the course of a chain of attribute
+ events.
+
+ Serves as an indicator of the source of the event and also provides
+ a means of controlling propagation across a chain of attribute
+ operations.
+
+ The :class:`.Event` object is sent as the ``initiator`` argument
+ when dealing with events such as :meth:`.AttributeEvents.append`,
+ :meth:`.AttributeEvents.set`,
+ and :meth:`.AttributeEvents.remove`.
+
+ The :class:`.Event` object is currently interpreted by the backref
+ event handlers, and is used to control the propagation of operations
+ across two mutually-dependent attributes.
+
+ .. versionchanged:: 2.0 Changed the name from ``AttributeEvent``
+ to ``AttributeEventToken``.
+
+ :attribute impl: The :class:`.AttributeImpl` which is the current event
+ initiator.
+
+ :attribute op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE`,
+ :attr:`.OP_REPLACE`, or :attr:`.OP_BULK_REPLACE`, indicating the
+ source operation.
+
+ """
+
+ __slots__ = "impl", "op", "parent_token"
+
+ def __init__(self, attribute_impl: AttributeImpl, op: util.symbol):
+ self.impl = attribute_impl
+ self.op = op
+ self.parent_token = self.impl.parent_token
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, AttributeEventToken)
+ and other.impl is self.impl
+ and other.op == self.op
+ )
+
+ @property
+ def key(self):
+ return self.impl.key
+
+ def hasparent(self, state):
+ return self.impl.hasparent(state)
+
+
+AttributeEvent = AttributeEventToken # legacy
+Event = AttributeEventToken # legacy
+
+
+class AttributeImpl:
+ """internal implementation for instrumented attributes."""
+
+ collection: bool
+ default_accepts_scalar_loader: bool
+ uses_objects: bool
+ supports_population: bool
+ dynamic: bool
+
+ _is_has_collection_adapter = False
+
+ _replace_token: AttributeEventToken
+ _remove_token: AttributeEventToken
+ _append_token: AttributeEventToken
+
+ def __init__(
+ self,
+ class_: _ExternalEntityType[_O],
+ key: str,
+ callable_: Optional[_LoaderCallable],
+ dispatch: _Dispatch[QueryableAttribute[Any]],
+ trackparent: bool = False,
+ compare_function: Optional[Callable[..., bool]] = None,
+ active_history: bool = False,
+ parent_token: Optional[AttributeEventToken] = None,
+ load_on_unexpire: bool = True,
+ send_modified_events: bool = True,
+ accepts_scalar_loader: Optional[bool] = None,
+ **kwargs: Any,
+ ):
+ r"""Construct an AttributeImpl.
+
+ :param \class_: associated class
+
+ :param key: string name of the attribute
+
+ :param \callable_:
+ optional function which generates a callable based on a parent
+ instance, which produces the "default" values for a scalar or
+ collection attribute when it's first accessed, if not present
+ already.
+
+ :param trackparent:
+ if True, attempt to track if an instance has a parent attached
+ to it via this attribute.
+
+ :param compare_function:
+ a function that compares two values which are normally
+ assignable to this attribute.
+
+ :param active_history:
+ indicates that get_history() should always return the "old" value,
+ even if it means executing a lazy callable upon attribute change.
+
+ :param parent_token:
+ Usually references the MapperProperty, used as a key for
+ the hasparent() function to identify an "owning" attribute.
+ Allows multiple AttributeImpls to all match a single
+ owner attribute.
+
+ :param load_on_unexpire:
+ if False, don't include this attribute in a load-on-expired
+ operation, i.e. the "expired_attribute_loader" process.
+ The attribute can still be in the "expired" list and be
+ considered to be "expired". Previously, this flag was called
+ "expire_missing" and is only used by a deferred column
+ attribute.
+
+ :param send_modified_events:
+ if False, the InstanceState._modified_event method will have no
+ effect; this means the attribute will never show up as changed in a
+ history entry.
+
+ """
+ self.class_ = class_
+ self.key = key
+ self.callable_ = callable_
+ self.dispatch = dispatch
+ self.trackparent = trackparent
+ self.parent_token = parent_token or self
+ self.send_modified_events = send_modified_events
+ if compare_function is None:
+ self.is_equal = operator.eq
+ else:
+ self.is_equal = compare_function
+
+ if accepts_scalar_loader is not None:
+ self.accepts_scalar_loader = accepts_scalar_loader
+ else:
+ self.accepts_scalar_loader = self.default_accepts_scalar_loader
+
+ _deferred_history = kwargs.pop("_deferred_history", False)
+ self._deferred_history = _deferred_history
+
+ if active_history:
+ self.dispatch._active_history = True
+
+ self.load_on_unexpire = load_on_unexpire
+ self._modified_token = AttributeEventToken(self, OP_MODIFIED)
+
+ __slots__ = (
+ "class_",
+ "key",
+ "callable_",
+ "dispatch",
+ "trackparent",
+ "parent_token",
+ "send_modified_events",
+ "is_equal",
+ "load_on_unexpire",
+ "_modified_token",
+ "accepts_scalar_loader",
+ "_deferred_history",
+ )
+
+ def __str__(self) -> str:
+ return f"{self.class_.__name__}.{self.key}"
+
+ def _get_active_history(self):
+ """Backwards compat for impl.active_history"""
+
+ return self.dispatch._active_history
+
+ def _set_active_history(self, value):
+ self.dispatch._active_history = value
+
+ active_history = property(_get_active_history, _set_active_history)
+
+ def hasparent(
+ self, state: InstanceState[Any], optimistic: bool = False
+ ) -> bool:
+ """Return the boolean value of a `hasparent` flag attached to
+ the given state.
+
+ The `optimistic` flag determines what the default return value
+ should be if no `hasparent` flag can be located.
+
+ As this function is used to determine if an instance is an
+ *orphan*, instances that were loaded from storage should be
+ assumed to not be orphans, until a True/False value for this
+ flag is set.
+
+ An instance attribute that is loaded by a callable function
+ will also not have a `hasparent` flag.
+
+ """
+ msg = "This AttributeImpl is not configured to track parents."
+ assert self.trackparent, msg
+
+ return (
+ state.parents.get(id(self.parent_token), optimistic) is not False
+ )
+
+ def sethasparent(
+ self,
+ state: InstanceState[Any],
+ parent_state: InstanceState[Any],
+ value: bool,
+ ) -> None:
+ """Set a boolean flag on the given item corresponding to
+ whether or not it is attached to a parent object via the
+ attribute represented by this ``InstrumentedAttribute``.
+
+ """
+ msg = "This AttributeImpl is not configured to track parents."
+ assert self.trackparent, msg
+
+ id_ = id(self.parent_token)
+ if value:
+ state.parents[id_] = parent_state
+ else:
+ if id_ in state.parents:
+ last_parent = state.parents[id_]
+
+ if (
+ last_parent is not False
+ and last_parent.key != parent_state.key
+ ):
+ if last_parent.obj() is None:
+ raise orm_exc.StaleDataError(
+ "Removing state %s from parent "
+ "state %s along attribute '%s', "
+ "but the parent record "
+ "has gone stale, can't be sure this "
+ "is the most recent parent."
+ % (
+ state_str(state),
+ state_str(parent_state),
+ self.key,
+ )
+ )
+
+ return
+
+ state.parents[id_] = False
+
+ def get_history(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> History:
+ raise NotImplementedError()
+
+ def get_all_pending(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PASSIVE_NO_INITIALIZE,
+ ) -> _AllPendingType:
+ """Return a list of tuples of (state, obj)
+ for all objects in this attribute's current state
+ + history.
+
+ Only applies to object-based attributes.
+
+ This is an inlining of existing functionality
+ which roughly corresponds to:
+
+ get_state_history(
+ state,
+ key,
+ passive=PASSIVE_NO_INITIALIZE).sum()
+
+ """
+ raise NotImplementedError()
+
+ def _default_value(
+ self, state: InstanceState[Any], dict_: _InstanceDict
+ ) -> Any:
+ """Produce an empty value for an uninitialized scalar attribute."""
+
+ assert self.key not in dict_, (
+ "_default_value should only be invoked for an "
+ "uninitialized or expired attribute"
+ )
+
+ value = None
+ for fn in self.dispatch.init_scalar:
+ ret = fn(state, value, dict_)
+ if ret is not ATTR_EMPTY:
+ value = ret
+
+ return value
+
+ def get(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> Any:
+ """Retrieve a value from the given object.
+ If a callable is assembled on this object's attribute, and
+ passive is False, the callable will be executed and the
+ resulting value will be set as the new value for this attribute.
+ """
+ if self.key in dict_:
+ return dict_[self.key]
+ else:
+ # if history present, don't load
+ key = self.key
+ if (
+ key not in state.committed_state
+ or state.committed_state[key] is NO_VALUE
+ ):
+ if not passive & CALLABLES_OK:
+ return PASSIVE_NO_RESULT
+
+ value = self._fire_loader_callables(state, key, passive)
+
+ if value is PASSIVE_NO_RESULT or value is NO_VALUE:
+ return value
+ elif value is ATTR_WAS_SET:
+ try:
+ return dict_[key]
+ except KeyError as err:
+ # TODO: no test coverage here.
+ raise KeyError(
+ "Deferred loader for attribute "
+ "%r failed to populate "
+ "correctly" % key
+ ) from err
+ elif value is not ATTR_EMPTY:
+ return self.set_committed_value(state, dict_, value)
+
+ if not passive & INIT_OK:
+ return NO_VALUE
+ else:
+ return self._default_value(state, dict_)
+
+ def _fire_loader_callables(
+ self, state: InstanceState[Any], key: str, passive: PassiveFlag
+ ) -> Any:
+ if (
+ self.accepts_scalar_loader
+ and self.load_on_unexpire
+ and key in state.expired_attributes
+ ):
+ return state._load_expired(state, passive)
+ elif key in state.callables:
+ callable_ = state.callables[key]
+ return callable_(state, passive)
+ elif self.callable_:
+ return self.callable_(state, passive)
+ else:
+ return ATTR_EMPTY
+
+ def append(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> None:
+ self.set(state, dict_, value, initiator, passive=passive)
+
+ def remove(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> None:
+ self.set(
+ state, dict_, None, initiator, passive=passive, check_old=value
+ )
+
+ def pop(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> None:
+ self.set(
+ state,
+ dict_,
+ None,
+ initiator,
+ passive=passive,
+ check_old=value,
+ pop=True,
+ )
+
+ def set(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken] = None,
+ passive: PassiveFlag = PASSIVE_OFF,
+ check_old: Any = None,
+ pop: bool = False,
+ ) -> None:
+ raise NotImplementedError()
+
+ def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None:
+ raise NotImplementedError()
+
+ def get_committed_value(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> Any:
+ """return the unchanged value of this attribute"""
+
+ if self.key in state.committed_state:
+ value = state.committed_state[self.key]
+ if value is NO_VALUE:
+ return None
+ else:
+ return value
+ else:
+ return self.get(state, dict_, passive=passive)
+
+ def set_committed_value(self, state, dict_, value):
+ """set an attribute value on the given instance and 'commit' it."""
+
+ dict_[self.key] = value
+ state._commit(dict_, [self.key])
+ return value
+
+
+class ScalarAttributeImpl(AttributeImpl):
+ """represents a scalar value-holding InstrumentedAttribute."""
+
+ default_accepts_scalar_loader = True
+ uses_objects = False
+ supports_population = True
+ collection = False
+ dynamic = False
+
+ __slots__ = "_replace_token", "_append_token", "_remove_token"
+
+ def __init__(self, *arg, **kw):
+ super().__init__(*arg, **kw)
+ self._replace_token = self._append_token = AttributeEventToken(
+ self, OP_REPLACE
+ )
+ self._remove_token = AttributeEventToken(self, OP_REMOVE)
+
+ def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None:
+ if self.dispatch._active_history:
+ old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE)
+ else:
+ old = dict_.get(self.key, NO_VALUE)
+
+ if self.dispatch.remove:
+ self.fire_remove_event(state, dict_, old, self._remove_token)
+ state._modified_event(dict_, self, old)
+
+ existing = dict_.pop(self.key, NO_VALUE)
+ if (
+ existing is NO_VALUE
+ and old is NO_VALUE
+ and not state.expired
+ and self.key not in state.expired_attributes
+ ):
+ raise AttributeError("%s object does not have a value" % self)
+
+ def get_history(
+ self,
+ state: InstanceState[Any],
+ dict_: Dict[str, Any],
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> History:
+ if self.key in dict_:
+ return History.from_scalar_attribute(self, state, dict_[self.key])
+ elif self.key in state.committed_state:
+ return History.from_scalar_attribute(self, state, NO_VALUE)
+ else:
+ if passive & INIT_OK:
+ passive ^= INIT_OK
+ current = self.get(state, dict_, passive=passive)
+ if current is PASSIVE_NO_RESULT:
+ return HISTORY_BLANK
+ else:
+ return History.from_scalar_attribute(self, state, current)
+
+ def set(
+ self,
+ state: InstanceState[Any],
+ dict_: Dict[str, Any],
+ value: Any,
+ initiator: Optional[AttributeEventToken] = None,
+ passive: PassiveFlag = PASSIVE_OFF,
+ check_old: Optional[object] = None,
+ pop: bool = False,
+ ) -> None:
+ if self.dispatch._active_history:
+ old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE)
+ else:
+ old = dict_.get(self.key, NO_VALUE)
+
+ if self.dispatch.set:
+ value = self.fire_replace_event(
+ state, dict_, value, old, initiator
+ )
+ state._modified_event(dict_, self, old)
+ dict_[self.key] = value
+
+ def fire_replace_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: _T,
+ previous: Any,
+ initiator: Optional[AttributeEventToken],
+ ) -> _T:
+ for fn in self.dispatch.set:
+ value = fn(
+ state, value, previous, initiator or self._replace_token
+ )
+ return value
+
+ def fire_remove_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ ) -> None:
+ for fn in self.dispatch.remove:
+ fn(state, value, initiator or self._remove_token)
+
+
+class ScalarObjectAttributeImpl(ScalarAttributeImpl):
+ """represents a scalar-holding InstrumentedAttribute,
+ where the target object is also instrumented.
+
+ Adds events to delete/set operations.
+
+ """
+
+ default_accepts_scalar_loader = False
+ uses_objects = True
+ supports_population = True
+ collection = False
+
+ __slots__ = ()
+
+ def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None:
+ if self.dispatch._active_history:
+ old = self.get(
+ state,
+ dict_,
+ passive=PASSIVE_ONLY_PERSISTENT
+ | NO_AUTOFLUSH
+ | LOAD_AGAINST_COMMITTED,
+ )
+ else:
+ old = self.get(
+ state,
+ dict_,
+ passive=PASSIVE_NO_FETCH ^ INIT_OK
+ | LOAD_AGAINST_COMMITTED
+ | NO_RAISE,
+ )
+
+ self.fire_remove_event(state, dict_, old, self._remove_token)
+
+ existing = dict_.pop(self.key, NO_VALUE)
+
+ # if the attribute is expired, we currently have no way to tell
+ # that an object-attribute was expired vs. not loaded. So
+ # for this test, we look to see if the object has a DB identity.
+ if (
+ existing is NO_VALUE
+ and old is not PASSIVE_NO_RESULT
+ and state.key is None
+ ):
+ raise AttributeError("%s object does not have a value" % self)
+
+ def get_history(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> History:
+ if self.key in dict_:
+ current = dict_[self.key]
+ else:
+ if passive & INIT_OK:
+ passive ^= INIT_OK
+ current = self.get(state, dict_, passive=passive)
+ if current is PASSIVE_NO_RESULT:
+ return HISTORY_BLANK
+
+ if not self._deferred_history:
+ return History.from_object_attribute(self, state, current)
+ else:
+ original = state.committed_state.get(self.key, _NO_HISTORY)
+ if original is PASSIVE_NO_RESULT:
+ loader_passive = passive | (
+ PASSIVE_ONLY_PERSISTENT
+ | NO_AUTOFLUSH
+ | LOAD_AGAINST_COMMITTED
+ | NO_RAISE
+ | DEFERRED_HISTORY_LOAD
+ )
+ original = self._fire_loader_callables(
+ state, self.key, loader_passive
+ )
+ return History.from_object_attribute(
+ self, state, current, original=original
+ )
+
+ def get_all_pending(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PASSIVE_NO_INITIALIZE,
+ ) -> _AllPendingType:
+ if self.key in dict_:
+ current = dict_[self.key]
+ elif passive & CALLABLES_OK:
+ current = self.get(state, dict_, passive=passive)
+ else:
+ return []
+
+ ret: _AllPendingType
+
+ # can't use __hash__(), can't use __eq__() here
+ if (
+ current is not None
+ and current is not PASSIVE_NO_RESULT
+ and current is not NO_VALUE
+ ):
+ ret = [(instance_state(current), current)]
+ else:
+ ret = [(None, None)]
+
+ if self.key in state.committed_state:
+ original = state.committed_state[self.key]
+ if (
+ original is not None
+ and original is not PASSIVE_NO_RESULT
+ and original is not NO_VALUE
+ and original is not current
+ ):
+ ret.append((instance_state(original), original))
+ return ret
+
+ def set(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken] = None,
+ passive: PassiveFlag = PASSIVE_OFF,
+ check_old: Any = None,
+ pop: bool = False,
+ ) -> None:
+ """Set a value on the given InstanceState."""
+
+ if self.dispatch._active_history:
+ old = self.get(
+ state,
+ dict_,
+ passive=PASSIVE_ONLY_PERSISTENT
+ | NO_AUTOFLUSH
+ | LOAD_AGAINST_COMMITTED,
+ )
+ else:
+ old = self.get(
+ state,
+ dict_,
+ passive=PASSIVE_NO_FETCH ^ INIT_OK
+ | LOAD_AGAINST_COMMITTED
+ | NO_RAISE,
+ )
+
+ if (
+ check_old is not None
+ and old is not PASSIVE_NO_RESULT
+ and check_old is not old
+ ):
+ if pop:
+ return
+ else:
+ raise ValueError(
+ "Object %s not associated with %s on attribute '%s'"
+ % (instance_str(check_old), state_str(state), self.key)
+ )
+
+ value = self.fire_replace_event(state, dict_, value, old, initiator)
+ dict_[self.key] = value
+
+ def fire_remove_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ ) -> None:
+ if self.trackparent and value not in (
+ None,
+ PASSIVE_NO_RESULT,
+ NO_VALUE,
+ ):
+ self.sethasparent(instance_state(value), state, False)
+
+ for fn in self.dispatch.remove:
+ fn(state, value, initiator or self._remove_token)
+
+ state._modified_event(dict_, self, value)
+
+ def fire_replace_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: _T,
+ previous: Any,
+ initiator: Optional[AttributeEventToken],
+ ) -> _T:
+ if self.trackparent:
+ if previous is not value and previous not in (
+ None,
+ PASSIVE_NO_RESULT,
+ NO_VALUE,
+ ):
+ self.sethasparent(instance_state(previous), state, False)
+
+ for fn in self.dispatch.set:
+ value = fn(
+ state, value, previous, initiator or self._replace_token
+ )
+
+ state._modified_event(dict_, self, previous)
+
+ if self.trackparent:
+ if value is not None:
+ self.sethasparent(instance_state(value), state, True)
+
+ return value
+
+
+class HasCollectionAdapter:
+ __slots__ = ()
+
+ collection: bool
+ _is_has_collection_adapter = True
+
+ def _dispose_previous_collection(
+ self,
+ state: InstanceState[Any],
+ collection: _AdaptedCollectionProtocol,
+ adapter: CollectionAdapter,
+ fire_event: bool,
+ ) -> None:
+ raise NotImplementedError()
+
+ @overload
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: Literal[None] = ...,
+ passive: Literal[PassiveFlag.PASSIVE_OFF] = ...,
+ ) -> CollectionAdapter: ...
+
+ @overload
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: _AdaptedCollectionProtocol = ...,
+ passive: PassiveFlag = ...,
+ ) -> CollectionAdapter: ...
+
+ @overload
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: Optional[_AdaptedCollectionProtocol] = ...,
+ passive: PassiveFlag = ...,
+ ) -> Union[
+ Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
+ ]: ...
+
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: Optional[_AdaptedCollectionProtocol] = None,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ ) -> Union[
+ Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
+ ]:
+ raise NotImplementedError()
+
+ def set(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken] = None,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ check_old: Any = None,
+ pop: bool = False,
+ _adapt: bool = True,
+ ) -> None:
+ raise NotImplementedError()
+
+
+if TYPE_CHECKING:
+
+ def _is_collection_attribute_impl(
+ impl: AttributeImpl,
+ ) -> TypeGuard[CollectionAttributeImpl]: ...
+
+else:
+ _is_collection_attribute_impl = operator.attrgetter("collection")
+
+
+class CollectionAttributeImpl(HasCollectionAdapter, AttributeImpl):
+ """A collection-holding attribute that instruments changes in membership.
+
+ Only handles collections of instrumented objects.
+
+ InstrumentedCollectionAttribute holds an arbitrary, user-specified
+ container object (defaulting to a list) and brokers access to the
+ CollectionAdapter, a "view" onto that object that presents consistent bag
+ semantics to the orm layer independent of the user data implementation.
+
+ """
+
+ uses_objects = True
+ collection = True
+ default_accepts_scalar_loader = False
+ supports_population = True
+ dynamic = False
+
+ _bulk_replace_token: AttributeEventToken
+
+ __slots__ = (
+ "copy",
+ "collection_factory",
+ "_append_token",
+ "_remove_token",
+ "_bulk_replace_token",
+ "_duck_typed_as",
+ )
+
+ def __init__(
+ self,
+ class_,
+ key,
+ callable_,
+ dispatch,
+ typecallable=None,
+ trackparent=False,
+ copy_function=None,
+ compare_function=None,
+ **kwargs,
+ ):
+ super().__init__(
+ class_,
+ key,
+ callable_,
+ dispatch,
+ trackparent=trackparent,
+ compare_function=compare_function,
+ **kwargs,
+ )
+
+ if copy_function is None:
+ copy_function = self.__copy
+ self.copy = copy_function
+ self.collection_factory = typecallable
+ self._append_token = AttributeEventToken(self, OP_APPEND)
+ self._remove_token = AttributeEventToken(self, OP_REMOVE)
+ self._bulk_replace_token = AttributeEventToken(self, OP_BULK_REPLACE)
+ self._duck_typed_as = util.duck_type_collection(
+ self.collection_factory()
+ )
+
+ if getattr(self.collection_factory, "_sa_linker", None):
+
+ @event.listens_for(self, "init_collection")
+ def link(target, collection, collection_adapter):
+ collection._sa_linker(collection_adapter)
+
+ @event.listens_for(self, "dispose_collection")
+ def unlink(target, collection, collection_adapter):
+ collection._sa_linker(None)
+
+ def __copy(self, item):
+ return [y for y in collections.collection_adapter(item)]
+
+ def get_history(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> History:
+ current = self.get(state, dict_, passive=passive)
+
+ if current is PASSIVE_NO_RESULT:
+ if (
+ passive & PassiveFlag.INCLUDE_PENDING_MUTATIONS
+ and self.key in state._pending_mutations
+ ):
+ pending = state._pending_mutations[self.key]
+ return pending.merge_with_history(HISTORY_BLANK)
+ else:
+ return HISTORY_BLANK
+ else:
+ if passive & PassiveFlag.INCLUDE_PENDING_MUTATIONS:
+ # this collection is loaded / present. should not be any
+ # pending mutations
+ assert self.key not in state._pending_mutations
+
+ return History.from_collection(self, state, current)
+
+ def get_all_pending(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PASSIVE_NO_INITIALIZE,
+ ) -> _AllPendingType:
+ # NOTE: passive is ignored here at the moment
+
+ if self.key not in dict_:
+ return []
+
+ current = dict_[self.key]
+ current = getattr(current, "_sa_adapter")
+
+ if self.key in state.committed_state:
+ original = state.committed_state[self.key]
+ if original is not NO_VALUE:
+ current_states = [
+ ((c is not None) and instance_state(c) or None, c)
+ for c in current
+ ]
+ original_states = [
+ ((c is not None) and instance_state(c) or None, c)
+ for c in original
+ ]
+
+ current_set = dict(current_states)
+ original_set = dict(original_states)
+
+ return (
+ [
+ (s, o)
+ for s, o in current_states
+ if s not in original_set
+ ]
+ + [(s, o) for s, o in current_states if s in original_set]
+ + [
+ (s, o)
+ for s, o in original_states
+ if s not in current_set
+ ]
+ )
+
+ return [(instance_state(o), o) for o in current]
+
+ def fire_append_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: _T,
+ initiator: Optional[AttributeEventToken],
+ key: Optional[Any],
+ ) -> _T:
+ for fn in self.dispatch.append:
+ value = fn(state, value, initiator or self._append_token, key=key)
+
+ state._modified_event(dict_, self, NO_VALUE, True)
+
+ if self.trackparent and value is not None:
+ self.sethasparent(instance_state(value), state, True)
+
+ return value
+
+ def fire_append_wo_mutation_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: _T,
+ initiator: Optional[AttributeEventToken],
+ key: Optional[Any],
+ ) -> _T:
+ for fn in self.dispatch.append_wo_mutation:
+ value = fn(state, value, initiator or self._append_token, key=key)
+
+ return value
+
+ def fire_pre_remove_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ initiator: Optional[AttributeEventToken],
+ key: Optional[Any],
+ ) -> None:
+ """A special event used for pop() operations.
+
+ The "remove" event needs to have the item to be removed passed to
+ it, which in the case of pop from a set, we don't have a way to access
+ the item before the operation. the event is used for all pop()
+ operations (even though set.pop is the one where it is really needed).
+
+ """
+ state._modified_event(dict_, self, NO_VALUE, True)
+
+ def fire_remove_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ key: Optional[Any],
+ ) -> None:
+ if self.trackparent and value is not None:
+ self.sethasparent(instance_state(value), state, False)
+
+ for fn in self.dispatch.remove:
+ fn(state, value, initiator or self._remove_token, key=key)
+
+ state._modified_event(dict_, self, NO_VALUE, True)
+
+ def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None:
+ if self.key not in dict_:
+ return
+
+ state._modified_event(dict_, self, NO_VALUE, True)
+
+ collection = self.get_collection(state, state.dict)
+ collection.clear_with_event()
+
+ # key is always present because we checked above. e.g.
+ # del is a no-op if collection not present.
+ del dict_[self.key]
+
+ def _default_value(
+ self, state: InstanceState[Any], dict_: _InstanceDict
+ ) -> _AdaptedCollectionProtocol:
+ """Produce an empty collection for an un-initialized attribute"""
+
+ assert self.key not in dict_, (
+ "_default_value should only be invoked for an "
+ "uninitialized or expired attribute"
+ )
+
+ if self.key in state._empty_collections:
+ return state._empty_collections[self.key]
+
+ adapter, user_data = self._initialize_collection(state)
+ adapter._set_empty(user_data)
+ return user_data
+
+ def _initialize_collection(
+ self, state: InstanceState[Any]
+ ) -> Tuple[CollectionAdapter, _AdaptedCollectionProtocol]:
+ adapter, collection = state.manager.initialize_collection(
+ self.key, state, self.collection_factory
+ )
+
+ self.dispatch.init_collection(state, collection, adapter)
+
+ return adapter, collection
+
+ def append(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> None:
+ collection = self.get_collection(
+ state, dict_, user_data=None, passive=passive
+ )
+ if collection is PASSIVE_NO_RESULT:
+ value = self.fire_append_event(
+ state, dict_, value, initiator, key=NO_KEY
+ )
+ assert (
+ self.key not in dict_
+ ), "Collection was loaded during event handling."
+ state._get_pending_mutation(self.key).append(value)
+ else:
+ if TYPE_CHECKING:
+ assert isinstance(collection, CollectionAdapter)
+ collection.append_with_event(value, initiator)
+
+ def remove(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> None:
+ collection = self.get_collection(
+ state, state.dict, user_data=None, passive=passive
+ )
+ if collection is PASSIVE_NO_RESULT:
+ self.fire_remove_event(state, dict_, value, initiator, key=NO_KEY)
+ assert (
+ self.key not in dict_
+ ), "Collection was loaded during event handling."
+ state._get_pending_mutation(self.key).remove(value)
+ else:
+ if TYPE_CHECKING:
+ assert isinstance(collection, CollectionAdapter)
+ collection.remove_with_event(value, initiator)
+
+ def pop(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> None:
+ try:
+ # TODO: better solution here would be to add
+ # a "popper" role to collections.py to complement
+ # "remover".
+ self.remove(state, dict_, value, initiator, passive=passive)
+ except (ValueError, KeyError, IndexError):
+ pass
+
+ def set(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken] = None,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ check_old: Any = None,
+ pop: bool = False,
+ _adapt: bool = True,
+ ) -> None:
+ iterable = orig_iterable = value
+ new_keys = None
+
+ # pulling a new collection first so that an adaptation exception does
+ # not trigger a lazy load of the old collection.
+ new_collection, user_data = self._initialize_collection(state)
+ if _adapt:
+ if new_collection._converter is not None:
+ iterable = new_collection._converter(iterable)
+ else:
+ setting_type = util.duck_type_collection(iterable)
+ receiving_type = self._duck_typed_as
+
+ if setting_type is not receiving_type:
+ given = (
+ iterable is None
+ and "None"
+ or iterable.__class__.__name__
+ )
+ wanted = self._duck_typed_as.__name__
+ raise TypeError(
+ "Incompatible collection type: %s is not %s-like"
+ % (given, wanted)
+ )
+
+ # If the object is an adapted collection, return the (iterable)
+ # adapter.
+ if hasattr(iterable, "_sa_iterator"):
+ iterable = iterable._sa_iterator()
+ elif setting_type is dict:
+ new_keys = list(iterable)
+ iterable = iterable.values()
+ else:
+ iterable = iter(iterable)
+ elif util.duck_type_collection(iterable) is dict:
+ new_keys = list(value)
+
+ new_values = list(iterable)
+
+ evt = self._bulk_replace_token
+
+ self.dispatch.bulk_replace(state, new_values, evt, keys=new_keys)
+
+ # propagate NO_RAISE in passive through to the get() for the
+ # existing object (ticket #8862)
+ old = self.get(
+ state,
+ dict_,
+ passive=PASSIVE_ONLY_PERSISTENT ^ (passive & PassiveFlag.NO_RAISE),
+ )
+ if old is PASSIVE_NO_RESULT:
+ old = self._default_value(state, dict_)
+ elif old is orig_iterable:
+ # ignore re-assignment of the current collection, as happens
+ # implicitly with in-place operators (foo.collection |= other)
+ return
+
+ # place a copy of "old" in state.committed_state
+ state._modified_event(dict_, self, old, True)
+
+ old_collection = old._sa_adapter
+
+ dict_[self.key] = user_data
+
+ collections.bulk_replace(
+ new_values, old_collection, new_collection, initiator=evt
+ )
+
+ self._dispose_previous_collection(state, old, old_collection, True)
+
+ def _dispose_previous_collection(
+ self,
+ state: InstanceState[Any],
+ collection: _AdaptedCollectionProtocol,
+ adapter: CollectionAdapter,
+ fire_event: bool,
+ ) -> None:
+ del collection._sa_adapter
+
+ # discarding old collection make sure it is not referenced in empty
+ # collections.
+ state._empty_collections.pop(self.key, None)
+ if fire_event:
+ self.dispatch.dispose_collection(state, collection, adapter)
+
+ def _invalidate_collection(
+ self, collection: _AdaptedCollectionProtocol
+ ) -> None:
+ adapter = getattr(collection, "_sa_adapter")
+ adapter.invalidated = True
+
+ def set_committed_value(
+ self, state: InstanceState[Any], dict_: _InstanceDict, value: Any
+ ) -> _AdaptedCollectionProtocol:
+ """Set an attribute value on the given instance and 'commit' it."""
+
+ collection, user_data = self._initialize_collection(state)
+
+ if value:
+ collection.append_multiple_without_event(value)
+
+ state.dict[self.key] = user_data
+
+ state._commit(dict_, [self.key])
+
+ if self.key in state._pending_mutations:
+ # pending items exist. issue a modified event,
+ # add/remove new items.
+ state._modified_event(dict_, self, user_data, True)
+
+ pending = state._pending_mutations.pop(self.key)
+ added = pending.added_items
+ removed = pending.deleted_items
+ for item in added:
+ collection.append_without_event(item)
+ for item in removed:
+ collection.remove_without_event(item)
+
+ return user_data
+
+ @overload
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: Literal[None] = ...,
+ passive: Literal[PassiveFlag.PASSIVE_OFF] = ...,
+ ) -> CollectionAdapter: ...
+
+ @overload
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: _AdaptedCollectionProtocol = ...,
+ passive: PassiveFlag = ...,
+ ) -> CollectionAdapter: ...
+
+ @overload
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: Optional[_AdaptedCollectionProtocol] = ...,
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> Union[
+ Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
+ ]: ...
+
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: Optional[_AdaptedCollectionProtocol] = None,
+ passive: PassiveFlag = PASSIVE_OFF,
+ ) -> Union[
+ Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
+ ]:
+ """Retrieve the CollectionAdapter associated with the given state.
+
+ if user_data is None, retrieves it from the state using normal
+ "get()" rules, which will fire lazy callables or return the "empty"
+ collection value.
+
+ """
+ if user_data is None:
+ fetch_user_data = self.get(state, dict_, passive=passive)
+ if fetch_user_data is LoaderCallableStatus.PASSIVE_NO_RESULT:
+ return fetch_user_data
+ else:
+ user_data = cast("_AdaptedCollectionProtocol", fetch_user_data)
+
+ return user_data._sa_adapter
+
+
+def backref_listeners(
+ attribute: QueryableAttribute[Any], key: str, uselist: bool
+) -> None:
+ """Apply listeners to synchronize a two-way relationship."""
+
+ # use easily recognizable names for stack traces.
+
+ # in the sections marked "tokens to test for a recursive loop",
+ # this is somewhat brittle and very performance-sensitive logic
+ # that is specific to how we might arrive at each event. a marker
+ # that can target us directly to arguments being invoked against
+ # the impl might be simpler, but could interfere with other systems.
+
+ parent_token = attribute.impl.parent_token
+ parent_impl = attribute.impl
+
+ def _acceptable_key_err(child_state, initiator, child_impl):
+ raise ValueError(
+ "Bidirectional attribute conflict detected: "
+ 'Passing object %s to attribute "%s" '
+ 'triggers a modify event on attribute "%s" '
+ 'via the backref "%s".'
+ % (
+ state_str(child_state),
+ initiator.parent_token,
+ child_impl.parent_token,
+ attribute.impl.parent_token,
+ )
+ )
+
+ def emit_backref_from_scalar_set_event(
+ state, child, oldchild, initiator, **kw
+ ):
+ if oldchild is child:
+ return child
+ if (
+ oldchild is not None
+ and oldchild is not PASSIVE_NO_RESULT
+ and oldchild is not NO_VALUE
+ ):
+ # With lazy=None, there's no guarantee that the full collection is
+ # present when updating via a backref.
+ old_state, old_dict = (
+ instance_state(oldchild),
+ instance_dict(oldchild),
+ )
+ impl = old_state.manager[key].impl
+
+ # tokens to test for a recursive loop.
+ if not impl.collection and not impl.dynamic:
+ check_recursive_token = impl._replace_token
+ else:
+ check_recursive_token = impl._remove_token
+
+ if initiator is not check_recursive_token:
+ impl.pop(
+ old_state,
+ old_dict,
+ state.obj(),
+ parent_impl._append_token,
+ passive=PASSIVE_NO_FETCH,
+ )
+
+ if child is not None:
+ child_state, child_dict = (
+ instance_state(child),
+ instance_dict(child),
+ )
+ child_impl = child_state.manager[key].impl
+
+ if (
+ initiator.parent_token is not parent_token
+ and initiator.parent_token is not child_impl.parent_token
+ ):
+ _acceptable_key_err(state, initiator, child_impl)
+
+ # tokens to test for a recursive loop.
+ check_append_token = child_impl._append_token
+ check_bulk_replace_token = (
+ child_impl._bulk_replace_token
+ if _is_collection_attribute_impl(child_impl)
+ else None
+ )
+
+ if (
+ initiator is not check_append_token
+ and initiator is not check_bulk_replace_token
+ ):
+ child_impl.append(
+ child_state,
+ child_dict,
+ state.obj(),
+ initiator,
+ passive=PASSIVE_NO_FETCH,
+ )
+ return child
+
+ def emit_backref_from_collection_append_event(
+ state, child, initiator, **kw
+ ):
+ if child is None:
+ return
+
+ child_state, child_dict = instance_state(child), instance_dict(child)
+ child_impl = child_state.manager[key].impl
+
+ if (
+ initiator.parent_token is not parent_token
+ and initiator.parent_token is not child_impl.parent_token
+ ):
+ _acceptable_key_err(state, initiator, child_impl)
+
+ # tokens to test for a recursive loop.
+ check_append_token = child_impl._append_token
+ check_bulk_replace_token = (
+ child_impl._bulk_replace_token
+ if _is_collection_attribute_impl(child_impl)
+ else None
+ )
+
+ if (
+ initiator is not check_append_token
+ and initiator is not check_bulk_replace_token
+ ):
+ child_impl.append(
+ child_state,
+ child_dict,
+ state.obj(),
+ initiator,
+ passive=PASSIVE_NO_FETCH,
+ )
+ return child
+
+ def emit_backref_from_collection_remove_event(
+ state, child, initiator, **kw
+ ):
+ if (
+ child is not None
+ and child is not PASSIVE_NO_RESULT
+ and child is not NO_VALUE
+ ):
+ child_state, child_dict = (
+ instance_state(child),
+ instance_dict(child),
+ )
+ child_impl = child_state.manager[key].impl
+
+ check_replace_token: Optional[AttributeEventToken]
+
+ # tokens to test for a recursive loop.
+ if not child_impl.collection and not child_impl.dynamic:
+ check_remove_token = child_impl._remove_token
+ check_replace_token = child_impl._replace_token
+ check_for_dupes_on_remove = uselist and not parent_impl.dynamic
+ else:
+ check_remove_token = child_impl._remove_token
+ check_replace_token = (
+ child_impl._bulk_replace_token
+ if _is_collection_attribute_impl(child_impl)
+ else None
+ )
+ check_for_dupes_on_remove = False
+
+ if (
+ initiator is not check_remove_token
+ and initiator is not check_replace_token
+ ):
+ if not check_for_dupes_on_remove or not util.has_dupes(
+ # when this event is called, the item is usually
+ # present in the list, except for a pop() operation.
+ state.dict[parent_impl.key],
+ child,
+ ):
+ child_impl.pop(
+ child_state,
+ child_dict,
+ state.obj(),
+ initiator,
+ passive=PASSIVE_NO_FETCH,
+ )
+
+ if uselist:
+ event.listen(
+ attribute,
+ "append",
+ emit_backref_from_collection_append_event,
+ retval=True,
+ raw=True,
+ include_key=True,
+ )
+ else:
+ event.listen(
+ attribute,
+ "set",
+ emit_backref_from_scalar_set_event,
+ retval=True,
+ raw=True,
+ include_key=True,
+ )
+ # TODO: need coverage in test/orm/ of remove event
+ event.listen(
+ attribute,
+ "remove",
+ emit_backref_from_collection_remove_event,
+ retval=True,
+ raw=True,
+ include_key=True,
+ )
+
+
+_NO_HISTORY = util.symbol("NO_HISTORY")
+_NO_STATE_SYMBOLS = frozenset([id(PASSIVE_NO_RESULT), id(NO_VALUE)])
+
+
+class History(NamedTuple):
+ """A 3-tuple of added, unchanged and deleted values,
+ representing the changes which have occurred on an instrumented
+ attribute.
+
+ The easiest way to get a :class:`.History` object for a particular
+ attribute on an object is to use the :func:`_sa.inspect` function::
+
+ from sqlalchemy import inspect
+
+ hist = inspect(myobject).attrs.myattribute.history
+
+ Each tuple member is an iterable sequence:
+
+ * ``added`` - the collection of items added to the attribute (the first
+ tuple element).
+
+ * ``unchanged`` - the collection of items that have not changed on the
+ attribute (the second tuple element).
+
+ * ``deleted`` - the collection of items that have been removed from the
+ attribute (the third tuple element).
+
+ """
+
+ added: Union[Tuple[()], List[Any]]
+ unchanged: Union[Tuple[()], List[Any]]
+ deleted: Union[Tuple[()], List[Any]]
+
+ def __bool__(self) -> bool:
+ return self != HISTORY_BLANK
+
+ def empty(self) -> bool:
+ """Return True if this :class:`.History` has no changes
+ and no existing, unchanged state.
+
+ """
+
+ return not bool((self.added or self.deleted) or self.unchanged)
+
+ def sum(self) -> Sequence[Any]:
+ """Return a collection of added + unchanged + deleted."""
+
+ return (
+ (self.added or []) + (self.unchanged or []) + (self.deleted or [])
+ )
+
+ def non_deleted(self) -> Sequence[Any]:
+ """Return a collection of added + unchanged."""
+
+ return (self.added or []) + (self.unchanged or [])
+
+ def non_added(self) -> Sequence[Any]:
+ """Return a collection of unchanged + deleted."""
+
+ return (self.unchanged or []) + (self.deleted or [])
+
+ def has_changes(self) -> bool:
+ """Return True if this :class:`.History` has changes."""
+
+ return bool(self.added or self.deleted)
+
+ def _merge(self, added: Iterable[Any], deleted: Iterable[Any]) -> History:
+ return History(
+ list(self.added) + list(added),
+ self.unchanged,
+ list(self.deleted) + list(deleted),
+ )
+
+ def as_state(self) -> History:
+ return History(
+ [
+ (c is not None) and instance_state(c) or None
+ for c in self.added
+ ],
+ [
+ (c is not None) and instance_state(c) or None
+ for c in self.unchanged
+ ],
+ [
+ (c is not None) and instance_state(c) or None
+ for c in self.deleted
+ ],
+ )
+
+ @classmethod
+ def from_scalar_attribute(
+ cls,
+ attribute: ScalarAttributeImpl,
+ state: InstanceState[Any],
+ current: Any,
+ ) -> History:
+ original = state.committed_state.get(attribute.key, _NO_HISTORY)
+
+ deleted: Union[Tuple[()], List[Any]]
+
+ if original is _NO_HISTORY:
+ if current is NO_VALUE:
+ return cls((), (), ())
+ else:
+ return cls((), [current], ())
+ # don't let ClauseElement expressions here trip things up
+ elif (
+ current is not NO_VALUE
+ and attribute.is_equal(current, original) is True
+ ):
+ return cls((), [current], ())
+ else:
+ # current convention on native scalars is to not
+ # include information
+ # about missing previous value in "deleted", but
+ # we do include None, which helps in some primary
+ # key situations
+ if id(original) in _NO_STATE_SYMBOLS:
+ deleted = ()
+ # indicate a "del" operation occurred when we don't have
+ # the previous value as: ([None], (), ())
+ if id(current) in _NO_STATE_SYMBOLS:
+ current = None
+ else:
+ deleted = [original]
+ if current is NO_VALUE:
+ return cls((), (), deleted)
+ else:
+ return cls([current], (), deleted)
+
+ @classmethod
+ def from_object_attribute(
+ cls,
+ attribute: ScalarObjectAttributeImpl,
+ state: InstanceState[Any],
+ current: Any,
+ original: Any = _NO_HISTORY,
+ ) -> History:
+ deleted: Union[Tuple[()], List[Any]]
+
+ if original is _NO_HISTORY:
+ original = state.committed_state.get(attribute.key, _NO_HISTORY)
+
+ if original is _NO_HISTORY:
+ if current is NO_VALUE:
+ return cls((), (), ())
+ else:
+ return cls((), [current], ())
+ elif current is original and current is not NO_VALUE:
+ return cls((), [current], ())
+ else:
+ # current convention on related objects is to not
+ # include information
+ # about missing previous value in "deleted", and
+ # to also not include None - the dependency.py rules
+ # ignore the None in any case.
+ if id(original) in _NO_STATE_SYMBOLS or original is None:
+ deleted = ()
+ # indicate a "del" operation occurred when we don't have
+ # the previous value as: ([None], (), ())
+ if id(current) in _NO_STATE_SYMBOLS:
+ current = None
+ else:
+ deleted = [original]
+ if current is NO_VALUE:
+ return cls((), (), deleted)
+ else:
+ return cls([current], (), deleted)
+
+ @classmethod
+ def from_collection(
+ cls,
+ attribute: CollectionAttributeImpl,
+ state: InstanceState[Any],
+ current: Any,
+ ) -> History:
+ original = state.committed_state.get(attribute.key, _NO_HISTORY)
+ if current is NO_VALUE:
+ return cls((), (), ())
+
+ current = getattr(current, "_sa_adapter")
+ if original is NO_VALUE:
+ return cls(list(current), (), ())
+ elif original is _NO_HISTORY:
+ return cls((), list(current), ())
+ else:
+ current_states = [
+ ((c is not None) and instance_state(c) or None, c)
+ for c in current
+ ]
+ original_states = [
+ ((c is not None) and instance_state(c) or None, c)
+ for c in original
+ ]
+
+ current_set = dict(current_states)
+ original_set = dict(original_states)
+
+ return cls(
+ [o for s, o in current_states if s not in original_set],
+ [o for s, o in current_states if s in original_set],
+ [o for s, o in original_states if s not in current_set],
+ )
+
+
+HISTORY_BLANK = History((), (), ())
+
+
+def get_history(
+ obj: object, key: str, passive: PassiveFlag = PASSIVE_OFF
+) -> History:
+ """Return a :class:`.History` record for the given object
+ and attribute key.
+
+ This is the **pre-flush** history for a given attribute, which is
+ reset each time the :class:`.Session` flushes changes to the
+ current database transaction.
+
+ .. note::
+
+ Prefer to use the :attr:`.AttributeState.history` and
+ :meth:`.AttributeState.load_history` accessors to retrieve the
+ :class:`.History` for instance attributes.
+
+
+ :param obj: an object whose class is instrumented by the
+ attributes package.
+
+ :param key: string attribute name.
+
+ :param passive: indicates loading behavior for the attribute
+ if the value is not already present. This is a
+ bitflag attribute, which defaults to the symbol
+ :attr:`.PASSIVE_OFF` indicating all necessary SQL
+ should be emitted.
+
+ .. seealso::
+
+ :attr:`.AttributeState.history`
+
+ :meth:`.AttributeState.load_history` - retrieve history
+ using loader callables if the value is not locally present.
+
+ """
+
+ return get_state_history(instance_state(obj), key, passive)
+
+
+def get_state_history(
+ state: InstanceState[Any], key: str, passive: PassiveFlag = PASSIVE_OFF
+) -> History:
+ return state.get_history(key, passive)
+
+
+def has_parent(
+ cls: Type[_O], obj: _O, key: str, optimistic: bool = False
+) -> bool:
+ """TODO"""
+ manager = manager_of_class(cls)
+ state = instance_state(obj)
+ return manager.has_parent(state, key, optimistic)
+
+
+def register_attribute(
+ class_: Type[_O],
+ key: str,
+ *,
+ comparator: interfaces.PropComparator[_T],
+ parententity: _InternalEntityType[_O],
+ doc: Optional[str] = None,
+ **kw: Any,
+) -> InstrumentedAttribute[_T]:
+ desc = register_descriptor(
+ class_, key, comparator=comparator, parententity=parententity, doc=doc
+ )
+ register_attribute_impl(class_, key, **kw)
+ return desc
+
+
+def register_attribute_impl(
+ class_: Type[_O],
+ key: str,
+ uselist: bool = False,
+ callable_: Optional[_LoaderCallable] = None,
+ useobject: bool = False,
+ impl_class: Optional[Type[AttributeImpl]] = None,
+ backref: Optional[str] = None,
+ **kw: Any,
+) -> QueryableAttribute[Any]:
+ manager = manager_of_class(class_)
+ if uselist:
+ factory = kw.pop("typecallable", None)
+ typecallable = manager.instrument_collection_class(
+ key, factory or list
+ )
+ else:
+ typecallable = kw.pop("typecallable", None)
+
+ dispatch = cast(
+ "_Dispatch[QueryableAttribute[Any]]", manager[key].dispatch
+ ) # noqa: E501
+
+ impl: AttributeImpl
+
+ if impl_class:
+ # TODO: this appears to be the WriteOnlyAttributeImpl /
+ # DynamicAttributeImpl constructor which is hardcoded
+ impl = cast("Type[WriteOnlyAttributeImpl]", impl_class)(
+ class_, key, dispatch, **kw
+ )
+ elif uselist:
+ impl = CollectionAttributeImpl(
+ class_, key, callable_, dispatch, typecallable=typecallable, **kw
+ )
+ elif useobject:
+ impl = ScalarObjectAttributeImpl(
+ class_, key, callable_, dispatch, **kw
+ )
+ else:
+ impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
+
+ manager[key].impl = impl
+
+ if backref:
+ backref_listeners(manager[key], backref, uselist)
+
+ manager.post_configure_attribute(key)
+ return manager[key]
+
+
+def register_descriptor(
+ class_: Type[Any],
+ key: str,
+ *,
+ comparator: interfaces.PropComparator[_T],
+ parententity: _InternalEntityType[Any],
+ doc: Optional[str] = None,
+) -> InstrumentedAttribute[_T]:
+ manager = manager_of_class(class_)
+
+ descriptor = InstrumentedAttribute(
+ class_, key, comparator=comparator, parententity=parententity
+ )
+
+ descriptor.__doc__ = doc # type: ignore
+
+ manager.instrument_attribute(key, descriptor)
+ return descriptor
+
+
+def unregister_attribute(class_: Type[Any], key: str) -> None:
+ manager_of_class(class_).uninstrument_attribute(key)
+
+
+def init_collection(obj: object, key: str) -> CollectionAdapter:
+ """Initialize a collection attribute and return the collection adapter.
+
+ This function is used to provide direct access to collection internals
+ for a previously unloaded attribute. e.g.::
+
+ collection_adapter = init_collection(someobject, 'elements')
+ for elem in values:
+ collection_adapter.append_without_event(elem)
+
+ For an easier way to do the above, see
+ :func:`~sqlalchemy.orm.attributes.set_committed_value`.
+
+ :param obj: a mapped object
+
+ :param key: string attribute name where the collection is located.
+
+ """
+ state = instance_state(obj)
+ dict_ = state.dict
+ return init_state_collection(state, dict_, key)
+
+
+def init_state_collection(
+ state: InstanceState[Any], dict_: _InstanceDict, key: str
+) -> CollectionAdapter:
+ """Initialize a collection attribute and return the collection adapter.
+
+ Discards any existing collection which may be there.
+
+ """
+ attr = state.manager[key].impl
+
+ if TYPE_CHECKING:
+ assert isinstance(attr, HasCollectionAdapter)
+
+ old = dict_.pop(key, None) # discard old collection
+ if old is not None:
+ old_collection = old._sa_adapter
+ attr._dispose_previous_collection(state, old, old_collection, False)
+
+ user_data = attr._default_value(state, dict_)
+ adapter: CollectionAdapter = attr.get_collection(
+ state, dict_, user_data, passive=PassiveFlag.PASSIVE_NO_FETCH
+ )
+ adapter._reset_empty()
+
+ return adapter
+
+
+def set_committed_value(instance, key, value):
+ """Set the value of an attribute with no history events.
+
+ Cancels any previous history present. The value should be
+ a scalar value for scalar-holding attributes, or
+ an iterable for any collection-holding attribute.
+
+ This is the same underlying method used when a lazy loader
+ fires off and loads additional data from the database.
+ In particular, this method can be used by application code
+ which has loaded additional attributes or collections through
+ separate queries, which can then be attached to an instance
+ as though it were part of its original loaded state.
+
+ """
+ state, dict_ = instance_state(instance), instance_dict(instance)
+ state.manager[key].impl.set_committed_value(state, dict_, value)
+
+
+def set_attribute(
+ instance: object,
+ key: str,
+ value: Any,
+ initiator: Optional[AttributeEventToken] = None,
+) -> None:
+ """Set the value of an attribute, firing history events.
+
+ This function may be used regardless of instrumentation
+ applied directly to the class, i.e. no descriptors are required.
+ Custom attribute management schemes will need to make usage
+ of this method to establish attribute state as understood
+ by SQLAlchemy.
+
+ :param instance: the object that will be modified
+
+ :param key: string name of the attribute
+
+ :param value: value to assign
+
+ :param initiator: an instance of :class:`.Event` that would have
+ been propagated from a previous event listener. This argument
+ is used when the :func:`.set_attribute` function is being used within
+ an existing event listening function where an :class:`.Event` object
+ is being supplied; the object may be used to track the origin of the
+ chain of events.
+
+ .. versionadded:: 1.2.3
+
+ """
+ state, dict_ = instance_state(instance), instance_dict(instance)
+ state.manager[key].impl.set(state, dict_, value, initiator)
+
+
+def get_attribute(instance: object, key: str) -> Any:
+ """Get the value of an attribute, firing any callables required.
+
+ This function may be used regardless of instrumentation
+ applied directly to the class, i.e. no descriptors are required.
+ Custom attribute management schemes will need to make usage
+ of this method to make usage of attribute state as understood
+ by SQLAlchemy.
+
+ """
+ state, dict_ = instance_state(instance), instance_dict(instance)
+ return state.manager[key].impl.get(state, dict_)
+
+
+def del_attribute(instance: object, key: str) -> None:
+ """Delete the value of an attribute, firing history events.
+
+ This function may be used regardless of instrumentation
+ applied directly to the class, i.e. no descriptors are required.
+ Custom attribute management schemes will need to make usage
+ of this method to establish attribute state as understood
+ by SQLAlchemy.
+
+ """
+ state, dict_ = instance_state(instance), instance_dict(instance)
+ state.manager[key].impl.delete(state, dict_)
+
+
+def flag_modified(instance: object, key: str) -> None:
+ """Mark an attribute on an instance as 'modified'.
+
+ This sets the 'modified' flag on the instance and
+ establishes an unconditional change event for the given attribute.
+ The attribute must have a value present, else an
+ :class:`.InvalidRequestError` is raised.
+
+ To mark an object "dirty" without referring to any specific attribute
+ so that it is considered within a flush, use the
+ :func:`.attributes.flag_dirty` call.
+
+ .. seealso::
+
+ :func:`.attributes.flag_dirty`
+
+ """
+ state, dict_ = instance_state(instance), instance_dict(instance)
+ impl = state.manager[key].impl
+ impl.dispatch.modified(state, impl._modified_token)
+ state._modified_event(dict_, impl, NO_VALUE, is_userland=True)
+
+
+def flag_dirty(instance: object) -> None:
+ """Mark an instance as 'dirty' without any specific attribute mentioned.
+
+ This is a special operation that will allow the object to travel through
+ the flush process for interception by events such as
+ :meth:`.SessionEvents.before_flush`. Note that no SQL will be emitted in
+ the flush process for an object that has no changes, even if marked dirty
+ via this method. However, a :meth:`.SessionEvents.before_flush` handler
+ will be able to see the object in the :attr:`.Session.dirty` collection and
+ may establish changes on it, which will then be included in the SQL
+ emitted.
+
+ .. versionadded:: 1.2
+
+ .. seealso::
+
+ :func:`.attributes.flag_modified`
+
+ """
+
+ state, dict_ = instance_state(instance), instance_dict(instance)
+ state._modified_event(dict_, None, NO_VALUE, is_userland=True)
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py
new file mode 100644
index 0000000..c900529
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py
@@ -0,0 +1,971 @@
+# orm/base.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""Constants and rudimental functions used throughout the ORM.
+
+"""
+
+from __future__ import annotations
+
+from enum import Enum
+import operator
+import typing
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Generic
+from typing import no_type_check
+from typing import Optional
+from typing import overload
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import exc
+from ._typing import insp_is_mapper
+from .. import exc as sa_exc
+from .. import inspection
+from .. import util
+from ..sql import roles
+from ..sql.elements import SQLColumnExpression
+from ..sql.elements import SQLCoreOperations
+from ..util import FastIntFlag
+from ..util.langhelpers import TypingOnly
+from ..util.typing import Literal
+
+if typing.TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _ExternalEntityType
+ from ._typing import _InternalEntityType
+ from .attributes import InstrumentedAttribute
+ from .dynamic import AppenderQuery
+ from .instrumentation import ClassManager
+ from .interfaces import PropComparator
+ from .mapper import Mapper
+ from .state import InstanceState
+ from .util import AliasedClass
+ from .writeonly import WriteOnlyCollection
+ from ..sql._typing import _ColumnExpressionArgument
+ from ..sql._typing import _InfoType
+ from ..sql.elements import ColumnElement
+ from ..sql.operators import OperatorType
+
+_T = TypeVar("_T", bound=Any)
+_T_co = TypeVar("_T_co", bound=Any, covariant=True)
+
+_O = TypeVar("_O", bound=object)
+
+
+class LoaderCallableStatus(Enum):
+ PASSIVE_NO_RESULT = 0
+ """Symbol returned by a loader callable or other attribute/history
+ retrieval operation when a value could not be determined, based
+ on loader callable flags.
+ """
+
+ PASSIVE_CLASS_MISMATCH = 1
+ """Symbol indicating that an object is locally present for a given
+ primary key identity but it is not of the requested class. The
+ return value is therefore None and no SQL should be emitted."""
+
+ ATTR_WAS_SET = 2
+ """Symbol returned by a loader callable to indicate the
+ retrieved value, or values, were assigned to their attributes
+ on the target object.
+ """
+
+ ATTR_EMPTY = 3
+ """Symbol used internally to indicate an attribute had no callable."""
+
+ NO_VALUE = 4
+ """Symbol which may be placed as the 'previous' value of an attribute,
+ indicating no value was loaded for an attribute when it was modified,
+ and flags indicated we were not to load it.
+ """
+
+ NEVER_SET = NO_VALUE
+ """
+ Synonymous with NO_VALUE
+
+ .. versionchanged:: 1.4 NEVER_SET was merged with NO_VALUE
+
+ """
+
+
+(
+ PASSIVE_NO_RESULT,
+ PASSIVE_CLASS_MISMATCH,
+ ATTR_WAS_SET,
+ ATTR_EMPTY,
+ NO_VALUE,
+) = tuple(LoaderCallableStatus)
+
+NEVER_SET = NO_VALUE
+
+
+class PassiveFlag(FastIntFlag):
+ """Bitflag interface that passes options onto loader callables"""
+
+ NO_CHANGE = 0
+ """No callables or SQL should be emitted on attribute access
+ and no state should change
+ """
+
+ CALLABLES_OK = 1
+ """Loader callables can be fired off if a value
+ is not present.
+ """
+
+ SQL_OK = 2
+ """Loader callables can emit SQL at least on scalar value attributes."""
+
+ RELATED_OBJECT_OK = 4
+ """Callables can use SQL to load related objects as well
+ as scalar value attributes.
+ """
+
+ INIT_OK = 8
+ """Attributes should be initialized with a blank
+ value (None or an empty collection) upon get, if no other
+ value can be obtained.
+ """
+
+ NON_PERSISTENT_OK = 16
+ """Callables can be emitted if the parent is not persistent."""
+
+ LOAD_AGAINST_COMMITTED = 32
+ """Callables should use committed values as primary/foreign keys during a
+ load.
+ """
+
+ NO_AUTOFLUSH = 64
+ """Loader callables should disable autoflush.""",
+
+ NO_RAISE = 128
+ """Loader callables should not raise any assertions"""
+
+ DEFERRED_HISTORY_LOAD = 256
+ """indicates special load of the previous value of an attribute"""
+
+ INCLUDE_PENDING_MUTATIONS = 512
+
+ # pre-packaged sets of flags used as inputs
+ PASSIVE_OFF = (
+ RELATED_OBJECT_OK | NON_PERSISTENT_OK | INIT_OK | CALLABLES_OK | SQL_OK
+ )
+ "Callables can be emitted in all cases."
+
+ PASSIVE_RETURN_NO_VALUE = PASSIVE_OFF ^ INIT_OK
+ """PASSIVE_OFF ^ INIT_OK"""
+
+ PASSIVE_NO_INITIALIZE = PASSIVE_RETURN_NO_VALUE ^ CALLABLES_OK
+ "PASSIVE_RETURN_NO_VALUE ^ CALLABLES_OK"
+
+ PASSIVE_NO_FETCH = PASSIVE_OFF ^ SQL_OK
+ "PASSIVE_OFF ^ SQL_OK"
+
+ PASSIVE_NO_FETCH_RELATED = PASSIVE_OFF ^ RELATED_OBJECT_OK
+ "PASSIVE_OFF ^ RELATED_OBJECT_OK"
+
+ PASSIVE_ONLY_PERSISTENT = PASSIVE_OFF ^ NON_PERSISTENT_OK
+ "PASSIVE_OFF ^ NON_PERSISTENT_OK"
+
+ PASSIVE_MERGE = PASSIVE_OFF | NO_RAISE
+ """PASSIVE_OFF | NO_RAISE
+
+ Symbol used specifically for session.merge() and similar cases
+
+ """
+
+
+(
+ NO_CHANGE,
+ CALLABLES_OK,
+ SQL_OK,
+ RELATED_OBJECT_OK,
+ INIT_OK,
+ NON_PERSISTENT_OK,
+ LOAD_AGAINST_COMMITTED,
+ NO_AUTOFLUSH,
+ NO_RAISE,
+ DEFERRED_HISTORY_LOAD,
+ INCLUDE_PENDING_MUTATIONS,
+ PASSIVE_OFF,
+ PASSIVE_RETURN_NO_VALUE,
+ PASSIVE_NO_INITIALIZE,
+ PASSIVE_NO_FETCH,
+ PASSIVE_NO_FETCH_RELATED,
+ PASSIVE_ONLY_PERSISTENT,
+ PASSIVE_MERGE,
+) = PassiveFlag.__members__.values()
+
+DEFAULT_MANAGER_ATTR = "_sa_class_manager"
+DEFAULT_STATE_ATTR = "_sa_instance_state"
+
+
+class EventConstants(Enum):
+ EXT_CONTINUE = 1
+ EXT_STOP = 2
+ EXT_SKIP = 3
+ NO_KEY = 4
+ """indicates an :class:`.AttributeEvent` event that did not have any
+ key argument.
+
+ .. versionadded:: 2.0
+
+ """
+
+
+EXT_CONTINUE, EXT_STOP, EXT_SKIP, NO_KEY = tuple(EventConstants)
+
+
+class RelationshipDirection(Enum):
+ """enumeration which indicates the 'direction' of a
+ :class:`_orm.RelationshipProperty`.
+
+ :class:`.RelationshipDirection` is accessible from the
+ :attr:`_orm.Relationship.direction` attribute of
+ :class:`_orm.RelationshipProperty`.
+
+ """
+
+ ONETOMANY = 1
+ """Indicates the one-to-many direction for a :func:`_orm.relationship`.
+
+ This symbol is typically used by the internals but may be exposed within
+ certain API features.
+
+ """
+
+ MANYTOONE = 2
+ """Indicates the many-to-one direction for a :func:`_orm.relationship`.
+
+ This symbol is typically used by the internals but may be exposed within
+ certain API features.
+
+ """
+
+ MANYTOMANY = 3
+ """Indicates the many-to-many direction for a :func:`_orm.relationship`.
+
+ This symbol is typically used by the internals but may be exposed within
+ certain API features.
+
+ """
+
+
+ONETOMANY, MANYTOONE, MANYTOMANY = tuple(RelationshipDirection)
+
+
+class InspectionAttrExtensionType(Enum):
+ """Symbols indicating the type of extension that a
+ :class:`.InspectionAttr` is part of."""
+
+
+class NotExtension(InspectionAttrExtensionType):
+ NOT_EXTENSION = "not_extension"
+ """Symbol indicating an :class:`InspectionAttr` that's
+ not part of sqlalchemy.ext.
+
+ Is assigned to the :attr:`.InspectionAttr.extension_type`
+ attribute.
+
+ """
+
+
+_never_set = frozenset([NEVER_SET])
+
+_none_set = frozenset([None, NEVER_SET, PASSIVE_NO_RESULT])
+
+_SET_DEFERRED_EXPIRED = util.symbol("SET_DEFERRED_EXPIRED")
+
+_DEFER_FOR_STATE = util.symbol("DEFER_FOR_STATE")
+
+_RAISE_FOR_STATE = util.symbol("RAISE_FOR_STATE")
+
+
+_F = TypeVar("_F", bound=Callable[..., Any])
+_Self = TypeVar("_Self")
+
+
+def _assertions(
+ *assertions: Any,
+) -> Callable[[_F], _F]:
+ @util.decorator
+ def generate(fn: _F, self: _Self, *args: Any, **kw: Any) -> _Self:
+ for assertion in assertions:
+ assertion(self, fn.__name__)
+ fn(self, *args, **kw)
+ return self
+
+ return generate
+
+
+if TYPE_CHECKING:
+
+ def manager_of_class(cls: Type[_O]) -> ClassManager[_O]: ...
+
+ @overload
+ def opt_manager_of_class(cls: AliasedClass[Any]) -> None: ...
+
+ @overload
+ def opt_manager_of_class(
+ cls: _ExternalEntityType[_O],
+ ) -> Optional[ClassManager[_O]]: ...
+
+ def opt_manager_of_class(
+ cls: _ExternalEntityType[_O],
+ ) -> Optional[ClassManager[_O]]: ...
+
+ def instance_state(instance: _O) -> InstanceState[_O]: ...
+
+ def instance_dict(instance: object) -> Dict[str, Any]: ...
+
+else:
+ # these can be replaced by sqlalchemy.ext.instrumentation
+ # if augmented class instrumentation is enabled.
+
+ def manager_of_class(cls):
+ try:
+ return cls.__dict__[DEFAULT_MANAGER_ATTR]
+ except KeyError as ke:
+ raise exc.UnmappedClassError(
+ cls, f"Can't locate an instrumentation manager for class {cls}"
+ ) from ke
+
+ def opt_manager_of_class(cls):
+ return cls.__dict__.get(DEFAULT_MANAGER_ATTR)
+
+ instance_state = operator.attrgetter(DEFAULT_STATE_ATTR)
+
+ instance_dict = operator.attrgetter("__dict__")
+
+
+def instance_str(instance: object) -> str:
+ """Return a string describing an instance."""
+
+ return state_str(instance_state(instance))
+
+
+def state_str(state: InstanceState[Any]) -> str:
+ """Return a string describing an instance via its InstanceState."""
+
+ if state is None:
+ return "None"
+ else:
+ return "<%s at 0x%x>" % (state.class_.__name__, id(state.obj()))
+
+
+def state_class_str(state: InstanceState[Any]) -> str:
+ """Return a string describing an instance's class via its
+ InstanceState.
+ """
+
+ if state is None:
+ return "None"
+ else:
+ return "<%s>" % (state.class_.__name__,)
+
+
+def attribute_str(instance: object, attribute: str) -> str:
+ return instance_str(instance) + "." + attribute
+
+
+def state_attribute_str(state: InstanceState[Any], attribute: str) -> str:
+ return state_str(state) + "." + attribute
+
+
+def object_mapper(instance: _T) -> Mapper[_T]:
+ """Given an object, return the primary Mapper associated with the object
+ instance.
+
+ Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError`
+ if no mapping is configured.
+
+ This function is available via the inspection system as::
+
+ inspect(instance).mapper
+
+ Using the inspection system will raise
+ :class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is
+ not part of a mapping.
+
+ """
+ return object_state(instance).mapper
+
+
+def object_state(instance: _T) -> InstanceState[_T]:
+ """Given an object, return the :class:`.InstanceState`
+ associated with the object.
+
+ Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError`
+ if no mapping is configured.
+
+ Equivalent functionality is available via the :func:`_sa.inspect`
+ function as::
+
+ inspect(instance)
+
+ Using the inspection system will raise
+ :class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is
+ not part of a mapping.
+
+ """
+ state = _inspect_mapped_object(instance)
+ if state is None:
+ raise exc.UnmappedInstanceError(instance)
+ else:
+ return state
+
+
+@inspection._inspects(object)
+def _inspect_mapped_object(instance: _T) -> Optional[InstanceState[_T]]:
+ try:
+ return instance_state(instance)
+ except (exc.UnmappedClassError,) + exc.NO_STATE:
+ return None
+
+
+def _class_to_mapper(
+ class_or_mapper: Union[Mapper[_T], Type[_T]]
+) -> Mapper[_T]:
+ # can't get mypy to see an overload for this
+ insp = inspection.inspect(class_or_mapper, False)
+ if insp is not None:
+ return insp.mapper # type: ignore
+ else:
+ assert isinstance(class_or_mapper, type)
+ raise exc.UnmappedClassError(class_or_mapper)
+
+
+def _mapper_or_none(
+ entity: Union[Type[_T], _InternalEntityType[_T]]
+) -> Optional[Mapper[_T]]:
+ """Return the :class:`_orm.Mapper` for the given class or None if the
+ class is not mapped.
+ """
+
+ # can't get mypy to see an overload for this
+ insp = inspection.inspect(entity, False)
+ if insp is not None:
+ return insp.mapper # type: ignore
+ else:
+ return None
+
+
+def _is_mapped_class(entity: Any) -> bool:
+ """Return True if the given object is a mapped class,
+ :class:`_orm.Mapper`, or :class:`.AliasedClass`.
+ """
+
+ insp = inspection.inspect(entity, False)
+ return (
+ insp is not None
+ and not insp.is_clause_element
+ and (insp.is_mapper or insp.is_aliased_class)
+ )
+
+
+def _is_aliased_class(entity: Any) -> bool:
+ insp = inspection.inspect(entity, False)
+ return insp is not None and getattr(insp, "is_aliased_class", False)
+
+
+@no_type_check
+def _entity_descriptor(entity: _EntityType[Any], key: str) -> Any:
+ """Return a class attribute given an entity and string name.
+
+ May return :class:`.InstrumentedAttribute` or user-defined
+ attribute.
+
+ """
+ insp = inspection.inspect(entity)
+ if insp.is_selectable:
+ description = entity
+ entity = insp.c
+ elif insp.is_aliased_class:
+ entity = insp.entity
+ description = entity
+ elif hasattr(insp, "mapper"):
+ description = entity = insp.mapper.class_
+ else:
+ description = entity
+
+ try:
+ return getattr(entity, key)
+ except AttributeError as err:
+ raise sa_exc.InvalidRequestError(
+ "Entity '%s' has no property '%s'" % (description, key)
+ ) from err
+
+
+if TYPE_CHECKING:
+
+ def _state_mapper(state: InstanceState[_O]) -> Mapper[_O]: ...
+
+else:
+ _state_mapper = util.dottedgetter("manager.mapper")
+
+
+def _inspect_mapped_class(
+ class_: Type[_O], configure: bool = False
+) -> Optional[Mapper[_O]]:
+ try:
+ class_manager = opt_manager_of_class(class_)
+ if class_manager is None or not class_manager.is_mapped:
+ return None
+ mapper = class_manager.mapper
+ except exc.NO_STATE:
+ return None
+ else:
+ if configure:
+ mapper._check_configure()
+ return mapper
+
+
+def _parse_mapper_argument(arg: Union[Mapper[_O], Type[_O]]) -> Mapper[_O]:
+ insp = inspection.inspect(arg, raiseerr=False)
+ if insp_is_mapper(insp):
+ return insp
+
+ raise sa_exc.ArgumentError(f"Mapper or mapped class expected, got {arg!r}")
+
+
+def class_mapper(class_: Type[_O], configure: bool = True) -> Mapper[_O]:
+ """Given a class, return the primary :class:`_orm.Mapper` associated
+ with the key.
+
+ Raises :exc:`.UnmappedClassError` if no mapping is configured
+ on the given class, or :exc:`.ArgumentError` if a non-class
+ object is passed.
+
+ Equivalent functionality is available via the :func:`_sa.inspect`
+ function as::
+
+ inspect(some_mapped_class)
+
+ Using the inspection system will raise
+ :class:`sqlalchemy.exc.NoInspectionAvailable` if the class is not mapped.
+
+ """
+ mapper = _inspect_mapped_class(class_, configure=configure)
+ if mapper is None:
+ if not isinstance(class_, type):
+ raise sa_exc.ArgumentError(
+ "Class object expected, got '%r'." % (class_,)
+ )
+ raise exc.UnmappedClassError(class_)
+ else:
+ return mapper
+
+
+class InspectionAttr:
+ """A base class applied to all ORM objects and attributes that are
+ related to things that can be returned by the :func:`_sa.inspect` function.
+
+ The attributes defined here allow the usage of simple boolean
+ checks to test basic facts about the object returned.
+
+ While the boolean checks here are basically the same as using
+ the Python isinstance() function, the flags here can be used without
+ the need to import all of these classes, and also such that
+ the SQLAlchemy class system can change while leaving the flags
+ here intact for forwards-compatibility.
+
+ """
+
+ __slots__: Tuple[str, ...] = ()
+
+ is_selectable = False
+ """Return True if this object is an instance of
+ :class:`_expression.Selectable`."""
+
+ is_aliased_class = False
+ """True if this object is an instance of :class:`.AliasedClass`."""
+
+ is_instance = False
+ """True if this object is an instance of :class:`.InstanceState`."""
+
+ is_mapper = False
+ """True if this object is an instance of :class:`_orm.Mapper`."""
+
+ is_bundle = False
+ """True if this object is an instance of :class:`.Bundle`."""
+
+ is_property = False
+ """True if this object is an instance of :class:`.MapperProperty`."""
+
+ is_attribute = False
+ """True if this object is a Python :term:`descriptor`.
+
+ This can refer to one of many types. Usually a
+ :class:`.QueryableAttribute` which handles attributes events on behalf
+ of a :class:`.MapperProperty`. But can also be an extension type
+ such as :class:`.AssociationProxy` or :class:`.hybrid_property`.
+ The :attr:`.InspectionAttr.extension_type` will refer to a constant
+ identifying the specific subtype.
+
+ .. seealso::
+
+ :attr:`_orm.Mapper.all_orm_descriptors`
+
+ """
+
+ _is_internal_proxy = False
+ """True if this object is an internal proxy object.
+
+ .. versionadded:: 1.2.12
+
+ """
+
+ is_clause_element = False
+ """True if this object is an instance of
+ :class:`_expression.ClauseElement`."""
+
+ extension_type: InspectionAttrExtensionType = NotExtension.NOT_EXTENSION
+ """The extension type, if any.
+ Defaults to :attr:`.interfaces.NotExtension.NOT_EXTENSION`
+
+ .. seealso::
+
+ :class:`.HybridExtensionType`
+
+ :class:`.AssociationProxyExtensionType`
+
+ """
+
+
+class InspectionAttrInfo(InspectionAttr):
+ """Adds the ``.info`` attribute to :class:`.InspectionAttr`.
+
+ The rationale for :class:`.InspectionAttr` vs. :class:`.InspectionAttrInfo`
+ is that the former is compatible as a mixin for classes that specify
+ ``__slots__``; this is essentially an implementation artifact.
+
+ """
+
+ __slots__ = ()
+
+ @util.ro_memoized_property
+ def info(self) -> _InfoType:
+ """Info dictionary associated with the object, allowing user-defined
+ data to be associated with this :class:`.InspectionAttr`.
+
+ The dictionary is generated when first accessed. Alternatively,
+ it can be specified as a constructor argument to the
+ :func:`.column_property`, :func:`_orm.relationship`, or
+ :func:`.composite`
+ functions.
+
+ .. seealso::
+
+ :attr:`.QueryableAttribute.info`
+
+ :attr:`.SchemaItem.info`
+
+ """
+ return {}
+
+
+class SQLORMOperations(SQLCoreOperations[_T_co], TypingOnly):
+ __slots__ = ()
+
+ if typing.TYPE_CHECKING:
+
+ def of_type(
+ self, class_: _EntityType[Any]
+ ) -> PropComparator[_T_co]: ...
+
+ def and_(
+ self, *criteria: _ColumnExpressionArgument[bool]
+ ) -> PropComparator[bool]: ...
+
+ def any( # noqa: A001
+ self,
+ criterion: Optional[_ColumnExpressionArgument[bool]] = None,
+ **kwargs: Any,
+ ) -> ColumnElement[bool]: ...
+
+ def has(
+ self,
+ criterion: Optional[_ColumnExpressionArgument[bool]] = None,
+ **kwargs: Any,
+ ) -> ColumnElement[bool]: ...
+
+
+class ORMDescriptor(Generic[_T_co], TypingOnly):
+ """Represent any Python descriptor that provides a SQL expression
+ construct at the class level."""
+
+ __slots__ = ()
+
+ if typing.TYPE_CHECKING:
+
+ @overload
+ def __get__(
+ self, instance: Any, owner: Literal[None]
+ ) -> ORMDescriptor[_T_co]: ...
+
+ @overload
+ def __get__(
+ self, instance: Literal[None], owner: Any
+ ) -> SQLCoreOperations[_T_co]: ...
+
+ @overload
+ def __get__(self, instance: object, owner: Any) -> _T_co: ...
+
+ def __get__(
+ self, instance: object, owner: Any
+ ) -> Union[ORMDescriptor[_T_co], SQLCoreOperations[_T_co], _T_co]: ...
+
+
+class _MappedAnnotationBase(Generic[_T_co], TypingOnly):
+ """common class for Mapped and similar ORM container classes.
+
+ these are classes that can appear on the left side of an ORM declarative
+ mapping, containing a mapped class or in some cases a collection
+ surrounding a mapped class.
+
+ """
+
+ __slots__ = ()
+
+
+class SQLORMExpression(
+ SQLORMOperations[_T_co], SQLColumnExpression[_T_co], TypingOnly
+):
+ """A type that may be used to indicate any ORM-level attribute or
+ object that acts in place of one, in the context of SQL expression
+ construction.
+
+ :class:`.SQLORMExpression` extends from the Core
+ :class:`.SQLColumnExpression` to add additional SQL methods that are ORM
+ specific, such as :meth:`.PropComparator.of_type`, and is part of the bases
+ for :class:`.InstrumentedAttribute`. It may be used in :pep:`484` typing to
+ indicate arguments or return values that should behave as ORM-level
+ attribute expressions.
+
+ .. versionadded:: 2.0.0b4
+
+
+ """
+
+ __slots__ = ()
+
+
+class Mapped(
+ SQLORMExpression[_T_co],
+ ORMDescriptor[_T_co],
+ _MappedAnnotationBase[_T_co],
+ roles.DDLConstraintColumnRole,
+):
+ """Represent an ORM mapped attribute on a mapped class.
+
+ This class represents the complete descriptor interface for any class
+ attribute that will have been :term:`instrumented` by the ORM
+ :class:`_orm.Mapper` class. Provides appropriate information to type
+ checkers such as pylance and mypy so that ORM-mapped attributes
+ are correctly typed.
+
+ The most prominent use of :class:`_orm.Mapped` is in
+ the :ref:`Declarative Mapping <orm_explicit_declarative_base>` form
+ of :class:`_orm.Mapper` configuration, where used explicitly it drives
+ the configuration of ORM attributes such as :func:`_orm.mapped_class`
+ and :func:`_orm.relationship`.
+
+ .. seealso::
+
+ :ref:`orm_explicit_declarative_base`
+
+ :ref:`orm_declarative_table`
+
+ .. tip::
+
+ The :class:`_orm.Mapped` class represents attributes that are handled
+ directly by the :class:`_orm.Mapper` class. It does not include other
+ Python descriptor classes that are provided as extensions, including
+ :ref:`hybrids_toplevel` and the :ref:`associationproxy_toplevel`.
+ While these systems still make use of ORM-specific superclasses
+ and structures, they are not :term:`instrumented` by the
+ :class:`_orm.Mapper` and instead provide their own functionality
+ when they are accessed on a class.
+
+ .. versionadded:: 1.4
+
+
+ """
+
+ __slots__ = ()
+
+ if typing.TYPE_CHECKING:
+
+ @overload
+ def __get__(
+ self, instance: None, owner: Any
+ ) -> InstrumentedAttribute[_T_co]: ...
+
+ @overload
+ def __get__(self, instance: object, owner: Any) -> _T_co: ...
+
+ def __get__(
+ self, instance: Optional[object], owner: Any
+ ) -> Union[InstrumentedAttribute[_T_co], _T_co]: ...
+
+ @classmethod
+ def _empty_constructor(cls, arg1: Any) -> Mapped[_T_co]: ...
+
+ def __set__(
+ self, instance: Any, value: Union[SQLCoreOperations[_T_co], _T_co]
+ ) -> None: ...
+
+ def __delete__(self, instance: Any) -> None: ...
+
+
+class _MappedAttribute(Generic[_T_co], TypingOnly):
+ """Mixin for attributes which should be replaced by mapper-assigned
+ attributes.
+
+ """
+
+ __slots__ = ()
+
+
+class _DeclarativeMapped(Mapped[_T_co], _MappedAttribute[_T_co]):
+ """Mixin for :class:`.MapperProperty` subclasses that allows them to
+ be compatible with ORM-annotated declarative mappings.
+
+ """
+
+ __slots__ = ()
+
+ # MappedSQLExpression, Relationship, Composite etc. dont actually do
+ # SQL expression behavior. yet there is code that compares them with
+ # __eq__(), __ne__(), etc. Since #8847 made Mapped even more full
+ # featured including ColumnOperators, we need to have those methods
+ # be no-ops for these objects, so return NotImplemented to fall back
+ # to normal comparison behavior.
+ def operate(self, op: OperatorType, *other: Any, **kwargs: Any) -> Any:
+ return NotImplemented
+
+ __sa_operate__ = operate
+
+ def reverse_operate(
+ self, op: OperatorType, other: Any, **kwargs: Any
+ ) -> Any:
+ return NotImplemented
+
+
+class DynamicMapped(_MappedAnnotationBase[_T_co]):
+ """Represent the ORM mapped attribute type for a "dynamic" relationship.
+
+ The :class:`_orm.DynamicMapped` type annotation may be used in an
+ :ref:`Annotated Declarative Table <orm_declarative_mapped_column>` mapping
+ to indicate that the ``lazy="dynamic"`` loader strategy should be used
+ for a particular :func:`_orm.relationship`.
+
+ .. legacy:: The "dynamic" lazy loader strategy is the legacy form of what
+ is now the "write_only" strategy described in the section
+ :ref:`write_only_relationship`.
+
+ E.g.::
+
+ class User(Base):
+ __tablename__ = "user"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ addresses: DynamicMapped[Address] = relationship(
+ cascade="all,delete-orphan"
+ )
+
+ See the section :ref:`dynamic_relationship` for background.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`dynamic_relationship` - complete background
+
+ :class:`.WriteOnlyMapped` - fully 2.0 style version
+
+ """
+
+ __slots__ = ()
+
+ if TYPE_CHECKING:
+
+ @overload
+ def __get__(
+ self, instance: None, owner: Any
+ ) -> InstrumentedAttribute[_T_co]: ...
+
+ @overload
+ def __get__(
+ self, instance: object, owner: Any
+ ) -> AppenderQuery[_T_co]: ...
+
+ def __get__(
+ self, instance: Optional[object], owner: Any
+ ) -> Union[InstrumentedAttribute[_T_co], AppenderQuery[_T_co]]: ...
+
+ def __set__(
+ self, instance: Any, value: typing.Collection[_T_co]
+ ) -> None: ...
+
+
+class WriteOnlyMapped(_MappedAnnotationBase[_T_co]):
+ """Represent the ORM mapped attribute type for a "write only" relationship.
+
+ The :class:`_orm.WriteOnlyMapped` type annotation may be used in an
+ :ref:`Annotated Declarative Table <orm_declarative_mapped_column>` mapping
+ to indicate that the ``lazy="write_only"`` loader strategy should be used
+ for a particular :func:`_orm.relationship`.
+
+ E.g.::
+
+ class User(Base):
+ __tablename__ = "user"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ addresses: WriteOnlyMapped[Address] = relationship(
+ cascade="all,delete-orphan"
+ )
+
+ See the section :ref:`write_only_relationship` for background.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`write_only_relationship` - complete background
+
+ :class:`.DynamicMapped` - includes legacy :class:`_orm.Query` support
+
+ """
+
+ __slots__ = ()
+
+ if TYPE_CHECKING:
+
+ @overload
+ def __get__(
+ self, instance: None, owner: Any
+ ) -> InstrumentedAttribute[_T_co]: ...
+
+ @overload
+ def __get__(
+ self, instance: object, owner: Any
+ ) -> WriteOnlyCollection[_T_co]: ...
+
+ def __get__(
+ self, instance: Optional[object], owner: Any
+ ) -> Union[
+ InstrumentedAttribute[_T_co], WriteOnlyCollection[_T_co]
+ ]: ...
+
+ def __set__(
+ self, instance: Any, value: typing.Collection[_T_co]
+ ) -> None: ...
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/bulk_persistence.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/bulk_persistence.py
new file mode 100644
index 0000000..5d2558d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/bulk_persistence.py
@@ -0,0 +1,2048 @@
+# orm/bulk_persistence.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: ignore-errors
+
+
+"""additional ORM persistence classes related to "bulk" operations,
+specifically outside of the flush() process.
+
+"""
+
+from __future__ import annotations
+
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Iterable
+from typing import Optional
+from typing import overload
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import attributes
+from . import context
+from . import evaluator
+from . import exc as orm_exc
+from . import loading
+from . import persistence
+from .base import NO_VALUE
+from .context import AbstractORMCompileState
+from .context import FromStatement
+from .context import ORMFromStatementCompileState
+from .context import QueryContext
+from .. import exc as sa_exc
+from .. import util
+from ..engine import Dialect
+from ..engine import result as _result
+from ..sql import coercions
+from ..sql import dml
+from ..sql import expression
+from ..sql import roles
+from ..sql import select
+from ..sql import sqltypes
+from ..sql.base import _entity_namespace_key
+from ..sql.base import CompileState
+from ..sql.base import Options
+from ..sql.dml import DeleteDMLState
+from ..sql.dml import InsertDMLState
+from ..sql.dml import UpdateDMLState
+from ..util import EMPTY_DICT
+from ..util.typing import Literal
+
+if TYPE_CHECKING:
+ from ._typing import DMLStrategyArgument
+ from ._typing import OrmExecuteOptionsParameter
+ from ._typing import SynchronizeSessionArgument
+ from .mapper import Mapper
+ from .session import _BindArguments
+ from .session import ORMExecuteState
+ from .session import Session
+ from .session import SessionTransaction
+ from .state import InstanceState
+ from ..engine import Connection
+ from ..engine import cursor
+ from ..engine.interfaces import _CoreAnyExecuteParams
+
+_O = TypeVar("_O", bound=object)
+
+
+@overload
+def _bulk_insert(
+ mapper: Mapper[_O],
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
+ session_transaction: SessionTransaction,
+ isstates: bool,
+ return_defaults: bool,
+ render_nulls: bool,
+ use_orm_insert_stmt: Literal[None] = ...,
+ execution_options: Optional[OrmExecuteOptionsParameter] = ...,
+) -> None: ...
+
+
+@overload
+def _bulk_insert(
+ mapper: Mapper[_O],
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
+ session_transaction: SessionTransaction,
+ isstates: bool,
+ return_defaults: bool,
+ render_nulls: bool,
+ use_orm_insert_stmt: Optional[dml.Insert] = ...,
+ execution_options: Optional[OrmExecuteOptionsParameter] = ...,
+) -> cursor.CursorResult[Any]: ...
+
+
+def _bulk_insert(
+ mapper: Mapper[_O],
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
+ session_transaction: SessionTransaction,
+ isstates: bool,
+ return_defaults: bool,
+ render_nulls: bool,
+ use_orm_insert_stmt: Optional[dml.Insert] = None,
+ execution_options: Optional[OrmExecuteOptionsParameter] = None,
+) -> Optional[cursor.CursorResult[Any]]:
+ base_mapper = mapper.base_mapper
+
+ if session_transaction.session.connection_callable:
+ raise NotImplementedError(
+ "connection_callable / per-instance sharding "
+ "not supported in bulk_insert()"
+ )
+
+ if isstates:
+ if return_defaults:
+ states = [(state, state.dict) for state in mappings]
+ mappings = [dict_ for (state, dict_) in states]
+ else:
+ mappings = [state.dict for state in mappings]
+ else:
+ mappings = [dict(m) for m in mappings]
+ _expand_composites(mapper, mappings)
+
+ connection = session_transaction.connection(base_mapper)
+
+ return_result: Optional[cursor.CursorResult[Any]] = None
+
+ mappers_to_run = [
+ (table, mp)
+ for table, mp in base_mapper._sorted_tables.items()
+ if table in mapper._pks_by_table
+ ]
+
+ if return_defaults:
+ # not used by new-style bulk inserts, only used for legacy
+ bookkeeping = True
+ elif len(mappers_to_run) > 1:
+ # if we have more than one table, mapper to run where we will be
+ # either horizontally splicing, or copying values between tables,
+ # we need the "bookkeeping" / deterministic returning order
+ bookkeeping = True
+ else:
+ bookkeeping = False
+
+ for table, super_mapper in mappers_to_run:
+ # find bindparams in the statement. For bulk, we don't really know if
+ # a key in the params applies to a different table since we are
+ # potentially inserting for multiple tables here; looking at the
+ # bindparam() is a lot more direct. in most cases this will
+ # use _generate_cache_key() which is memoized, although in practice
+ # the ultimate statement that's executed is probably not the same
+ # object so that memoization might not matter much.
+ extra_bp_names = (
+ [
+ b.key
+ for b in use_orm_insert_stmt._get_embedded_bindparams()
+ if b.key in mappings[0]
+ ]
+ if use_orm_insert_stmt is not None
+ else ()
+ )
+
+ records = (
+ (
+ None,
+ state_dict,
+ params,
+ mapper,
+ connection,
+ value_params,
+ has_all_pks,
+ has_all_defaults,
+ )
+ for (
+ state,
+ state_dict,
+ params,
+ mp,
+ conn,
+ value_params,
+ has_all_pks,
+ has_all_defaults,
+ ) in persistence._collect_insert_commands(
+ table,
+ ((None, mapping, mapper, connection) for mapping in mappings),
+ bulk=True,
+ return_defaults=bookkeeping,
+ render_nulls=render_nulls,
+ include_bulk_keys=extra_bp_names,
+ )
+ )
+
+ result = persistence._emit_insert_statements(
+ base_mapper,
+ None,
+ super_mapper,
+ table,
+ records,
+ bookkeeping=bookkeeping,
+ use_orm_insert_stmt=use_orm_insert_stmt,
+ execution_options=execution_options,
+ )
+ if use_orm_insert_stmt is not None:
+ if not use_orm_insert_stmt._returning or return_result is None:
+ return_result = result
+ elif result.returns_rows:
+ assert bookkeeping
+ return_result = return_result.splice_horizontally(result)
+
+ if return_defaults and isstates:
+ identity_cls = mapper._identity_class
+ identity_props = [p.key for p in mapper._identity_key_props]
+ for state, dict_ in states:
+ state.key = (
+ identity_cls,
+ tuple([dict_[key] for key in identity_props]),
+ )
+
+ if use_orm_insert_stmt is not None:
+ assert return_result is not None
+ return return_result
+
+
+@overload
+def _bulk_update(
+ mapper: Mapper[Any],
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
+ session_transaction: SessionTransaction,
+ isstates: bool,
+ update_changed_only: bool,
+ use_orm_update_stmt: Literal[None] = ...,
+ enable_check_rowcount: bool = True,
+) -> None: ...
+
+
+@overload
+def _bulk_update(
+ mapper: Mapper[Any],
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
+ session_transaction: SessionTransaction,
+ isstates: bool,
+ update_changed_only: bool,
+ use_orm_update_stmt: Optional[dml.Update] = ...,
+ enable_check_rowcount: bool = True,
+) -> _result.Result[Any]: ...
+
+
+def _bulk_update(
+ mapper: Mapper[Any],
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
+ session_transaction: SessionTransaction,
+ isstates: bool,
+ update_changed_only: bool,
+ use_orm_update_stmt: Optional[dml.Update] = None,
+ enable_check_rowcount: bool = True,
+) -> Optional[_result.Result[Any]]:
+ base_mapper = mapper.base_mapper
+
+ search_keys = mapper._primary_key_propkeys
+ if mapper._version_id_prop:
+ search_keys = {mapper._version_id_prop.key}.union(search_keys)
+
+ def _changed_dict(mapper, state):
+ return {
+ k: v
+ for k, v in state.dict.items()
+ if k in state.committed_state or k in search_keys
+ }
+
+ if isstates:
+ if update_changed_only:
+ mappings = [_changed_dict(mapper, state) for state in mappings]
+ else:
+ mappings = [state.dict for state in mappings]
+ else:
+ mappings = [dict(m) for m in mappings]
+ _expand_composites(mapper, mappings)
+
+ if session_transaction.session.connection_callable:
+ raise NotImplementedError(
+ "connection_callable / per-instance sharding "
+ "not supported in bulk_update()"
+ )
+
+ connection = session_transaction.connection(base_mapper)
+
+ # find bindparams in the statement. see _bulk_insert for similar
+ # notes for the insert case
+ extra_bp_names = (
+ [
+ b.key
+ for b in use_orm_update_stmt._get_embedded_bindparams()
+ if b.key in mappings[0]
+ ]
+ if use_orm_update_stmt is not None
+ else ()
+ )
+
+ for table, super_mapper in base_mapper._sorted_tables.items():
+ if not mapper.isa(super_mapper) or table not in mapper._pks_by_table:
+ continue
+
+ records = persistence._collect_update_commands(
+ None,
+ table,
+ (
+ (
+ None,
+ mapping,
+ mapper,
+ connection,
+ (
+ mapping[mapper._version_id_prop.key]
+ if mapper._version_id_prop
+ else None
+ ),
+ )
+ for mapping in mappings
+ ),
+ bulk=True,
+ use_orm_update_stmt=use_orm_update_stmt,
+ include_bulk_keys=extra_bp_names,
+ )
+ persistence._emit_update_statements(
+ base_mapper,
+ None,
+ super_mapper,
+ table,
+ records,
+ bookkeeping=False,
+ use_orm_update_stmt=use_orm_update_stmt,
+ enable_check_rowcount=enable_check_rowcount,
+ )
+
+ if use_orm_update_stmt is not None:
+ return _result.null_result()
+
+
+def _expand_composites(mapper, mappings):
+ composite_attrs = mapper.composites
+ if not composite_attrs:
+ return
+
+ composite_keys = set(composite_attrs.keys())
+ populators = {
+ key: composite_attrs[key]._populate_composite_bulk_save_mappings_fn()
+ for key in composite_keys
+ }
+ for mapping in mappings:
+ for key in composite_keys.intersection(mapping):
+ populators[key](mapping)
+
+
+class ORMDMLState(AbstractORMCompileState):
+ is_dml_returning = True
+ from_statement_ctx: Optional[ORMFromStatementCompileState] = None
+
+ @classmethod
+ def _get_orm_crud_kv_pairs(
+ cls, mapper, statement, kv_iterator, needs_to_be_cacheable
+ ):
+ core_get_crud_kv_pairs = UpdateDMLState._get_crud_kv_pairs
+
+ for k, v in kv_iterator:
+ k = coercions.expect(roles.DMLColumnRole, k)
+
+ if isinstance(k, str):
+ desc = _entity_namespace_key(mapper, k, default=NO_VALUE)
+ if desc is NO_VALUE:
+ yield (
+ coercions.expect(roles.DMLColumnRole, k),
+ (
+ coercions.expect(
+ roles.ExpressionElementRole,
+ v,
+ type_=sqltypes.NullType(),
+ is_crud=True,
+ )
+ if needs_to_be_cacheable
+ else v
+ ),
+ )
+ else:
+ yield from core_get_crud_kv_pairs(
+ statement,
+ desc._bulk_update_tuples(v),
+ needs_to_be_cacheable,
+ )
+ elif "entity_namespace" in k._annotations:
+ k_anno = k._annotations
+ attr = _entity_namespace_key(
+ k_anno["entity_namespace"], k_anno["proxy_key"]
+ )
+ yield from core_get_crud_kv_pairs(
+ statement,
+ attr._bulk_update_tuples(v),
+ needs_to_be_cacheable,
+ )
+ else:
+ yield (
+ k,
+ (
+ v
+ if not needs_to_be_cacheable
+ else coercions.expect(
+ roles.ExpressionElementRole,
+ v,
+ type_=sqltypes.NullType(),
+ is_crud=True,
+ )
+ ),
+ )
+
+ @classmethod
+ def _get_multi_crud_kv_pairs(cls, statement, kv_iterator):
+ plugin_subject = statement._propagate_attrs["plugin_subject"]
+
+ if not plugin_subject or not plugin_subject.mapper:
+ return UpdateDMLState._get_multi_crud_kv_pairs(
+ statement, kv_iterator
+ )
+
+ return [
+ dict(
+ cls._get_orm_crud_kv_pairs(
+ plugin_subject.mapper, statement, value_dict.items(), False
+ )
+ )
+ for value_dict in kv_iterator
+ ]
+
+ @classmethod
+ def _get_crud_kv_pairs(cls, statement, kv_iterator, needs_to_be_cacheable):
+ assert (
+ needs_to_be_cacheable
+ ), "no test coverage for needs_to_be_cacheable=False"
+
+ plugin_subject = statement._propagate_attrs["plugin_subject"]
+
+ if not plugin_subject or not plugin_subject.mapper:
+ return UpdateDMLState._get_crud_kv_pairs(
+ statement, kv_iterator, needs_to_be_cacheable
+ )
+
+ return list(
+ cls._get_orm_crud_kv_pairs(
+ plugin_subject.mapper,
+ statement,
+ kv_iterator,
+ needs_to_be_cacheable,
+ )
+ )
+
+ @classmethod
+ def get_entity_description(cls, statement):
+ ext_info = statement.table._annotations["parententity"]
+ mapper = ext_info.mapper
+ if ext_info.is_aliased_class:
+ _label_name = ext_info.name
+ else:
+ _label_name = mapper.class_.__name__
+
+ return {
+ "name": _label_name,
+ "type": mapper.class_,
+ "expr": ext_info.entity,
+ "entity": ext_info.entity,
+ "table": mapper.local_table,
+ }
+
+ @classmethod
+ def get_returning_column_descriptions(cls, statement):
+ def _ent_for_col(c):
+ return c._annotations.get("parententity", None)
+
+ def _attr_for_col(c, ent):
+ if ent is None:
+ return c
+ proxy_key = c._annotations.get("proxy_key", None)
+ if not proxy_key:
+ return c
+ else:
+ return getattr(ent.entity, proxy_key, c)
+
+ return [
+ {
+ "name": c.key,
+ "type": c.type,
+ "expr": _attr_for_col(c, ent),
+ "aliased": ent.is_aliased_class,
+ "entity": ent.entity,
+ }
+ for c, ent in [
+ (c, _ent_for_col(c)) for c in statement._all_selected_columns
+ ]
+ ]
+
+ def _setup_orm_returning(
+ self,
+ compiler,
+ orm_level_statement,
+ dml_level_statement,
+ dml_mapper,
+ *,
+ use_supplemental_cols=True,
+ ):
+ """establish ORM column handlers for an INSERT, UPDATE, or DELETE
+ which uses explicit returning().
+
+ called within compilation level create_for_statement.
+
+ The _return_orm_returning() method then receives the Result
+ after the statement was executed, and applies ORM loading to the
+ state that we first established here.
+
+ """
+
+ if orm_level_statement._returning:
+ fs = FromStatement(
+ orm_level_statement._returning,
+ dml_level_statement,
+ _adapt_on_names=False,
+ )
+ fs = fs.execution_options(**orm_level_statement._execution_options)
+ fs = fs.options(*orm_level_statement._with_options)
+ self.select_statement = fs
+ self.from_statement_ctx = fsc = (
+ ORMFromStatementCompileState.create_for_statement(fs, compiler)
+ )
+ fsc.setup_dml_returning_compile_state(dml_mapper)
+
+ dml_level_statement = dml_level_statement._generate()
+ dml_level_statement._returning = ()
+
+ cols_to_return = [c for c in fsc.primary_columns if c is not None]
+
+ # since we are splicing result sets together, make sure there
+ # are columns of some kind returned in each result set
+ if not cols_to_return:
+ cols_to_return.extend(dml_mapper.primary_key)
+
+ if use_supplemental_cols:
+ dml_level_statement = dml_level_statement.return_defaults(
+ # this is a little weird looking, but by passing
+ # primary key as the main list of cols, this tells
+ # return_defaults to omit server-default cols (and
+ # actually all cols, due to some weird thing we should
+ # clean up in crud.py).
+ # Since we have cols_to_return, just return what we asked
+ # for (plus primary key, which ORM persistence needs since
+ # we likely set bookkeeping=True here, which is another
+ # whole thing...). We dont want to clutter the
+ # statement up with lots of other cols the user didn't
+ # ask for. see #9685
+ *dml_mapper.primary_key,
+ supplemental_cols=cols_to_return,
+ )
+ else:
+ dml_level_statement = dml_level_statement.returning(
+ *cols_to_return
+ )
+
+ return dml_level_statement
+
+ @classmethod
+ def _return_orm_returning(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ result,
+ ):
+ execution_context = result.context
+ compile_state = execution_context.compiled.compile_state
+
+ if (
+ compile_state.from_statement_ctx
+ and not compile_state.from_statement_ctx.compile_options._is_star
+ ):
+ load_options = execution_options.get(
+ "_sa_orm_load_options", QueryContext.default_load_options
+ )
+
+ querycontext = QueryContext(
+ compile_state.from_statement_ctx,
+ compile_state.select_statement,
+ params,
+ session,
+ load_options,
+ execution_options,
+ bind_arguments,
+ )
+ return loading.instances(result, querycontext)
+ else:
+ return result
+
+
+class BulkUDCompileState(ORMDMLState):
+ class default_update_options(Options):
+ _dml_strategy: DMLStrategyArgument = "auto"
+ _synchronize_session: SynchronizeSessionArgument = "auto"
+ _can_use_returning: bool = False
+ _is_delete_using: bool = False
+ _is_update_from: bool = False
+ _autoflush: bool = True
+ _subject_mapper: Optional[Mapper[Any]] = None
+ _resolved_values = EMPTY_DICT
+ _eval_condition = None
+ _matched_rows = None
+ _identity_token = None
+
+ @classmethod
+ def can_use_returning(
+ cls,
+ dialect: Dialect,
+ mapper: Mapper[Any],
+ *,
+ is_multitable: bool = False,
+ is_update_from: bool = False,
+ is_delete_using: bool = False,
+ is_executemany: bool = False,
+ ) -> bool:
+ raise NotImplementedError()
+
+ @classmethod
+ def orm_pre_session_exec(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ is_pre_event,
+ ):
+ (
+ update_options,
+ execution_options,
+ ) = BulkUDCompileState.default_update_options.from_execution_options(
+ "_sa_orm_update_options",
+ {
+ "synchronize_session",
+ "autoflush",
+ "identity_token",
+ "is_delete_using",
+ "is_update_from",
+ "dml_strategy",
+ },
+ execution_options,
+ statement._execution_options,
+ )
+ bind_arguments["clause"] = statement
+ try:
+ plugin_subject = statement._propagate_attrs["plugin_subject"]
+ except KeyError:
+ assert False, "statement had 'orm' plugin but no plugin_subject"
+ else:
+ if plugin_subject:
+ bind_arguments["mapper"] = plugin_subject.mapper
+ update_options += {"_subject_mapper": plugin_subject.mapper}
+
+ if "parententity" not in statement.table._annotations:
+ update_options += {"_dml_strategy": "core_only"}
+ elif not isinstance(params, list):
+ if update_options._dml_strategy == "auto":
+ update_options += {"_dml_strategy": "orm"}
+ elif update_options._dml_strategy == "bulk":
+ raise sa_exc.InvalidRequestError(
+ 'Can\'t use "bulk" ORM insert strategy without '
+ "passing separate parameters"
+ )
+ else:
+ if update_options._dml_strategy == "auto":
+ update_options += {"_dml_strategy": "bulk"}
+
+ sync = update_options._synchronize_session
+ if sync is not None:
+ if sync not in ("auto", "evaluate", "fetch", False):
+ raise sa_exc.ArgumentError(
+ "Valid strategies for session synchronization "
+ "are 'auto', 'evaluate', 'fetch', False"
+ )
+ if update_options._dml_strategy == "bulk" and sync == "fetch":
+ raise sa_exc.InvalidRequestError(
+ "The 'fetch' synchronization strategy is not available "
+ "for 'bulk' ORM updates (i.e. multiple parameter sets)"
+ )
+
+ if not is_pre_event:
+ if update_options._autoflush:
+ session._autoflush()
+
+ if update_options._dml_strategy == "orm":
+ if update_options._synchronize_session == "auto":
+ update_options = cls._do_pre_synchronize_auto(
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ update_options,
+ )
+ elif update_options._synchronize_session == "evaluate":
+ update_options = cls._do_pre_synchronize_evaluate(
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ update_options,
+ )
+ elif update_options._synchronize_session == "fetch":
+ update_options = cls._do_pre_synchronize_fetch(
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ update_options,
+ )
+ elif update_options._dml_strategy == "bulk":
+ if update_options._synchronize_session == "auto":
+ update_options += {"_synchronize_session": "evaluate"}
+
+ # indicators from the "pre exec" step that are then
+ # added to the DML statement, which will also be part of the cache
+ # key. The compile level create_for_statement() method will then
+ # consume these at compiler time.
+ statement = statement._annotate(
+ {
+ "synchronize_session": update_options._synchronize_session,
+ "is_delete_using": update_options._is_delete_using,
+ "is_update_from": update_options._is_update_from,
+ "dml_strategy": update_options._dml_strategy,
+ "can_use_returning": update_options._can_use_returning,
+ }
+ )
+
+ return (
+ statement,
+ util.immutabledict(execution_options).union(
+ {"_sa_orm_update_options": update_options}
+ ),
+ )
+
+ @classmethod
+ def orm_setup_cursor_result(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ result,
+ ):
+ # this stage of the execution is called after the
+ # do_orm_execute event hook. meaning for an extension like
+ # horizontal sharding, this step happens *within* the horizontal
+ # sharding event handler which calls session.execute() re-entrantly
+ # and will occur for each backend individually.
+ # the sharding extension then returns its own merged result from the
+ # individual ones we return here.
+
+ update_options = execution_options["_sa_orm_update_options"]
+ if update_options._dml_strategy == "orm":
+ if update_options._synchronize_session == "evaluate":
+ cls._do_post_synchronize_evaluate(
+ session, statement, result, update_options
+ )
+ elif update_options._synchronize_session == "fetch":
+ cls._do_post_synchronize_fetch(
+ session, statement, result, update_options
+ )
+ elif update_options._dml_strategy == "bulk":
+ if update_options._synchronize_session == "evaluate":
+ cls._do_post_synchronize_bulk_evaluate(
+ session, params, result, update_options
+ )
+ return result
+
+ return cls._return_orm_returning(
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ result,
+ )
+
+ @classmethod
+ def _adjust_for_extra_criteria(cls, global_attributes, ext_info):
+ """Apply extra criteria filtering.
+
+ For all distinct single-table-inheritance mappers represented in the
+ table being updated or deleted, produce additional WHERE criteria such
+ that only the appropriate subtypes are selected from the total results.
+
+ Additionally, add WHERE criteria originating from LoaderCriteriaOptions
+ collected from the statement.
+
+ """
+
+ return_crit = ()
+
+ adapter = ext_info._adapter if ext_info.is_aliased_class else None
+
+ if (
+ "additional_entity_criteria",
+ ext_info.mapper,
+ ) in global_attributes:
+ return_crit += tuple(
+ ae._resolve_where_criteria(ext_info)
+ for ae in global_attributes[
+ ("additional_entity_criteria", ext_info.mapper)
+ ]
+ if ae.include_aliases or ae.entity is ext_info
+ )
+
+ if ext_info.mapper._single_table_criterion is not None:
+ return_crit += (ext_info.mapper._single_table_criterion,)
+
+ if adapter:
+ return_crit = tuple(adapter.traverse(crit) for crit in return_crit)
+
+ return return_crit
+
+ @classmethod
+ def _interpret_returning_rows(cls, mapper, rows):
+ """translate from local inherited table columns to base mapper
+ primary key columns.
+
+ Joined inheritance mappers always establish the primary key in terms of
+ the base table. When we UPDATE a sub-table, we can only get
+ RETURNING for the sub-table's columns.
+
+ Here, we create a lookup from the local sub table's primary key
+ columns to the base table PK columns so that we can get identity
+ key values from RETURNING that's against the joined inheritance
+ sub-table.
+
+ the complexity here is to support more than one level deep of
+ inheritance, where we have to link columns to each other across
+ the inheritance hierarchy.
+
+ """
+
+ if mapper.local_table is not mapper.base_mapper.local_table:
+ return rows
+
+ # this starts as a mapping of
+ # local_pk_col: local_pk_col.
+ # we will then iteratively rewrite the "value" of the dict with
+ # each successive superclass column
+ local_pk_to_base_pk = {pk: pk for pk in mapper.local_table.primary_key}
+
+ for mp in mapper.iterate_to_root():
+ if mp.inherits is None:
+ break
+ elif mp.local_table is mp.inherits.local_table:
+ continue
+
+ t_to_e = dict(mp._table_to_equated[mp.inherits.local_table])
+ col_to_col = {sub_pk: super_pk for super_pk, sub_pk in t_to_e[mp]}
+ for pk, super_ in local_pk_to_base_pk.items():
+ local_pk_to_base_pk[pk] = col_to_col[super_]
+
+ lookup = {
+ local_pk_to_base_pk[lpk]: idx
+ for idx, lpk in enumerate(mapper.local_table.primary_key)
+ }
+ primary_key_convert = [
+ lookup[bpk] for bpk in mapper.base_mapper.primary_key
+ ]
+ return [tuple(row[idx] for idx in primary_key_convert) for row in rows]
+
+ @classmethod
+ def _get_matched_objects_on_criteria(cls, update_options, states):
+ mapper = update_options._subject_mapper
+ eval_condition = update_options._eval_condition
+
+ raw_data = [
+ (state.obj(), state, state.dict)
+ for state in states
+ if state.mapper.isa(mapper) and not state.expired
+ ]
+
+ identity_token = update_options._identity_token
+ if identity_token is not None:
+ raw_data = [
+ (obj, state, dict_)
+ for obj, state, dict_ in raw_data
+ if state.identity_token == identity_token
+ ]
+
+ result = []
+ for obj, state, dict_ in raw_data:
+ evaled_condition = eval_condition(obj)
+
+ # caution: don't use "in ()" or == here, _EXPIRE_OBJECT
+ # evaluates as True for all comparisons
+ if (
+ evaled_condition is True
+ or evaled_condition is evaluator._EXPIRED_OBJECT
+ ):
+ result.append(
+ (
+ obj,
+ state,
+ dict_,
+ evaled_condition is evaluator._EXPIRED_OBJECT,
+ )
+ )
+ return result
+
+ @classmethod
+ def _eval_condition_from_statement(cls, update_options, statement):
+ mapper = update_options._subject_mapper
+ target_cls = mapper.class_
+
+ evaluator_compiler = evaluator._EvaluatorCompiler(target_cls)
+ crit = ()
+ if statement._where_criteria:
+ crit += statement._where_criteria
+
+ global_attributes = {}
+ for opt in statement._with_options:
+ if opt._is_criteria_option:
+ opt.get_global_criteria(global_attributes)
+
+ if global_attributes:
+ crit += cls._adjust_for_extra_criteria(global_attributes, mapper)
+
+ if crit:
+ eval_condition = evaluator_compiler.process(*crit)
+ else:
+ # workaround for mypy https://github.com/python/mypy/issues/14027
+ def _eval_condition(obj):
+ return True
+
+ eval_condition = _eval_condition
+
+ return eval_condition
+
+ @classmethod
+ def _do_pre_synchronize_auto(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ update_options,
+ ):
+ """setup auto sync strategy
+
+
+ "auto" checks if we can use "evaluate" first, then falls back
+ to "fetch"
+
+ evaluate is vastly more efficient for the common case
+ where session is empty, only has a few objects, and the UPDATE
+ statement can potentially match thousands/millions of rows.
+
+ OTOH more complex criteria that fails to work with "evaluate"
+ we would hope usually correlates with fewer net rows.
+
+ """
+
+ try:
+ eval_condition = cls._eval_condition_from_statement(
+ update_options, statement
+ )
+
+ except evaluator.UnevaluatableError:
+ pass
+ else:
+ return update_options + {
+ "_eval_condition": eval_condition,
+ "_synchronize_session": "evaluate",
+ }
+
+ update_options += {"_synchronize_session": "fetch"}
+ return cls._do_pre_synchronize_fetch(
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ update_options,
+ )
+
+ @classmethod
+ def _do_pre_synchronize_evaluate(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ update_options,
+ ):
+ try:
+ eval_condition = cls._eval_condition_from_statement(
+ update_options, statement
+ )
+
+ except evaluator.UnevaluatableError as err:
+ raise sa_exc.InvalidRequestError(
+ 'Could not evaluate current criteria in Python: "%s". '
+ "Specify 'fetch' or False for the "
+ "synchronize_session execution option." % err
+ ) from err
+
+ return update_options + {
+ "_eval_condition": eval_condition,
+ }
+
+ @classmethod
+ def _get_resolved_values(cls, mapper, statement):
+ if statement._multi_values:
+ return []
+ elif statement._ordered_values:
+ return list(statement._ordered_values)
+ elif statement._values:
+ return list(statement._values.items())
+ else:
+ return []
+
+ @classmethod
+ def _resolved_keys_as_propnames(cls, mapper, resolved_values):
+ values = []
+ for k, v in resolved_values:
+ if mapper and isinstance(k, expression.ColumnElement):
+ try:
+ attr = mapper._columntoproperty[k]
+ except orm_exc.UnmappedColumnError:
+ pass
+ else:
+ values.append((attr.key, v))
+ else:
+ raise sa_exc.InvalidRequestError(
+ "Attribute name not found, can't be "
+ "synchronized back to objects: %r" % k
+ )
+ return values
+
+ @classmethod
+ def _do_pre_synchronize_fetch(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ update_options,
+ ):
+ mapper = update_options._subject_mapper
+
+ select_stmt = (
+ select(*(mapper.primary_key + (mapper.select_identity_token,)))
+ .select_from(mapper)
+ .options(*statement._with_options)
+ )
+ select_stmt._where_criteria = statement._where_criteria
+
+ # conditionally run the SELECT statement for pre-fetch, testing the
+ # "bind" for if we can use RETURNING or not using the do_orm_execute
+ # event. If RETURNING is available, the do_orm_execute event
+ # will cancel the SELECT from being actually run.
+ #
+ # The way this is organized seems strange, why don't we just
+ # call can_use_returning() before invoking the statement and get
+ # answer?, why does this go through the whole execute phase using an
+ # event? Answer: because we are integrating with extensions such
+ # as the horizontal sharding extention that "multiplexes" an individual
+ # statement run through multiple engines, and it uses
+ # do_orm_execute() to do that.
+
+ can_use_returning = None
+
+ def skip_for_returning(orm_context: ORMExecuteState) -> Any:
+ bind = orm_context.session.get_bind(**orm_context.bind_arguments)
+ nonlocal can_use_returning
+
+ per_bind_result = cls.can_use_returning(
+ bind.dialect,
+ mapper,
+ is_update_from=update_options._is_update_from,
+ is_delete_using=update_options._is_delete_using,
+ is_executemany=orm_context.is_executemany,
+ )
+
+ if can_use_returning is not None:
+ if can_use_returning != per_bind_result:
+ raise sa_exc.InvalidRequestError(
+ "For synchronize_session='fetch', can't mix multiple "
+ "backends where some support RETURNING and others "
+ "don't"
+ )
+ elif orm_context.is_executemany and not per_bind_result:
+ raise sa_exc.InvalidRequestError(
+ "For synchronize_session='fetch', can't use multiple "
+ "parameter sets in ORM mode, which this backend does not "
+ "support with RETURNING"
+ )
+ else:
+ can_use_returning = per_bind_result
+
+ if per_bind_result:
+ return _result.null_result()
+ else:
+ return None
+
+ result = session.execute(
+ select_stmt,
+ params,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ _add_event=skip_for_returning,
+ )
+ matched_rows = result.fetchall()
+
+ return update_options + {
+ "_matched_rows": matched_rows,
+ "_can_use_returning": can_use_returning,
+ }
+
+
+@CompileState.plugin_for("orm", "insert")
+class BulkORMInsert(ORMDMLState, InsertDMLState):
+ class default_insert_options(Options):
+ _dml_strategy: DMLStrategyArgument = "auto"
+ _render_nulls: bool = False
+ _return_defaults: bool = False
+ _subject_mapper: Optional[Mapper[Any]] = None
+ _autoflush: bool = True
+ _populate_existing: bool = False
+
+ select_statement: Optional[FromStatement] = None
+
+ @classmethod
+ def orm_pre_session_exec(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ is_pre_event,
+ ):
+ (
+ insert_options,
+ execution_options,
+ ) = BulkORMInsert.default_insert_options.from_execution_options(
+ "_sa_orm_insert_options",
+ {"dml_strategy", "autoflush", "populate_existing", "render_nulls"},
+ execution_options,
+ statement._execution_options,
+ )
+ bind_arguments["clause"] = statement
+ try:
+ plugin_subject = statement._propagate_attrs["plugin_subject"]
+ except KeyError:
+ assert False, "statement had 'orm' plugin but no plugin_subject"
+ else:
+ if plugin_subject:
+ bind_arguments["mapper"] = plugin_subject.mapper
+ insert_options += {"_subject_mapper": plugin_subject.mapper}
+
+ if not params:
+ if insert_options._dml_strategy == "auto":
+ insert_options += {"_dml_strategy": "orm"}
+ elif insert_options._dml_strategy == "bulk":
+ raise sa_exc.InvalidRequestError(
+ 'Can\'t use "bulk" ORM insert strategy without '
+ "passing separate parameters"
+ )
+ else:
+ if insert_options._dml_strategy == "auto":
+ insert_options += {"_dml_strategy": "bulk"}
+
+ if insert_options._dml_strategy != "raw":
+ # for ORM object loading, like ORMContext, we have to disable
+ # result set adapt_to_context, because we will be generating a
+ # new statement with specific columns that's cached inside of
+ # an ORMFromStatementCompileState, which we will re-use for
+ # each result.
+ if not execution_options:
+ execution_options = context._orm_load_exec_options
+ else:
+ execution_options = execution_options.union(
+ context._orm_load_exec_options
+ )
+
+ if not is_pre_event and insert_options._autoflush:
+ session._autoflush()
+
+ statement = statement._annotate(
+ {"dml_strategy": insert_options._dml_strategy}
+ )
+
+ return (
+ statement,
+ util.immutabledict(execution_options).union(
+ {"_sa_orm_insert_options": insert_options}
+ ),
+ )
+
+ @classmethod
+ def orm_execute_statement(
+ cls,
+ session: Session,
+ statement: dml.Insert,
+ params: _CoreAnyExecuteParams,
+ execution_options: OrmExecuteOptionsParameter,
+ bind_arguments: _BindArguments,
+ conn: Connection,
+ ) -> _result.Result:
+ insert_options = execution_options.get(
+ "_sa_orm_insert_options", cls.default_insert_options
+ )
+
+ if insert_options._dml_strategy not in (
+ "raw",
+ "bulk",
+ "orm",
+ "auto",
+ ):
+ raise sa_exc.ArgumentError(
+ "Valid strategies for ORM insert strategy "
+ "are 'raw', 'orm', 'bulk', 'auto"
+ )
+
+ result: _result.Result[Any]
+
+ if insert_options._dml_strategy == "raw":
+ result = conn.execute(
+ statement, params or {}, execution_options=execution_options
+ )
+ return result
+
+ if insert_options._dml_strategy == "bulk":
+ mapper = insert_options._subject_mapper
+
+ if (
+ statement._post_values_clause is not None
+ and mapper._multiple_persistence_tables
+ ):
+ raise sa_exc.InvalidRequestError(
+ "bulk INSERT with a 'post values' clause "
+ "(typically upsert) not supported for multi-table "
+ f"mapper {mapper}"
+ )
+
+ assert mapper is not None
+ assert session._transaction is not None
+ result = _bulk_insert(
+ mapper,
+ cast(
+ "Iterable[Dict[str, Any]]",
+ [params] if isinstance(params, dict) else params,
+ ),
+ session._transaction,
+ isstates=False,
+ return_defaults=insert_options._return_defaults,
+ render_nulls=insert_options._render_nulls,
+ use_orm_insert_stmt=statement,
+ execution_options=execution_options,
+ )
+ elif insert_options._dml_strategy == "orm":
+ result = conn.execute(
+ statement, params or {}, execution_options=execution_options
+ )
+ else:
+ raise AssertionError()
+
+ if not bool(statement._returning):
+ return result
+
+ if insert_options._populate_existing:
+ load_options = execution_options.get(
+ "_sa_orm_load_options", QueryContext.default_load_options
+ )
+ load_options += {"_populate_existing": True}
+ execution_options = execution_options.union(
+ {"_sa_orm_load_options": load_options}
+ )
+
+ return cls._return_orm_returning(
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ result,
+ )
+
+ @classmethod
+ def create_for_statement(cls, statement, compiler, **kw) -> BulkORMInsert:
+ self = cast(
+ BulkORMInsert,
+ super().create_for_statement(statement, compiler, **kw),
+ )
+
+ if compiler is not None:
+ toplevel = not compiler.stack
+ else:
+ toplevel = True
+ if not toplevel:
+ return self
+
+ mapper = statement._propagate_attrs["plugin_subject"]
+ dml_strategy = statement._annotations.get("dml_strategy", "raw")
+ if dml_strategy == "bulk":
+ self._setup_for_bulk_insert(compiler)
+ elif dml_strategy == "orm":
+ self._setup_for_orm_insert(compiler, mapper)
+
+ return self
+
+ @classmethod
+ def _resolved_keys_as_col_keys(cls, mapper, resolved_value_dict):
+ return {
+ col.key if col is not None else k: v
+ for col, k, v in (
+ (mapper.c.get(k), k, v) for k, v in resolved_value_dict.items()
+ )
+ }
+
+ def _setup_for_orm_insert(self, compiler, mapper):
+ statement = orm_level_statement = cast(dml.Insert, self.statement)
+
+ statement = self._setup_orm_returning(
+ compiler,
+ orm_level_statement,
+ statement,
+ dml_mapper=mapper,
+ use_supplemental_cols=False,
+ )
+ self.statement = statement
+
+ def _setup_for_bulk_insert(self, compiler):
+ """establish an INSERT statement within the context of
+ bulk insert.
+
+ This method will be within the "conn.execute()" call that is invoked
+ by persistence._emit_insert_statement().
+
+ """
+ statement = orm_level_statement = cast(dml.Insert, self.statement)
+ an = statement._annotations
+
+ emit_insert_table, emit_insert_mapper = (
+ an["_emit_insert_table"],
+ an["_emit_insert_mapper"],
+ )
+
+ statement = statement._clone()
+
+ statement.table = emit_insert_table
+ if self._dict_parameters:
+ self._dict_parameters = {
+ col: val
+ for col, val in self._dict_parameters.items()
+ if col.table is emit_insert_table
+ }
+
+ statement = self._setup_orm_returning(
+ compiler,
+ orm_level_statement,
+ statement,
+ dml_mapper=emit_insert_mapper,
+ use_supplemental_cols=True,
+ )
+
+ if (
+ self.from_statement_ctx is not None
+ and self.from_statement_ctx.compile_options._is_star
+ ):
+ raise sa_exc.CompileError(
+ "Can't use RETURNING * with bulk ORM INSERT. "
+ "Please use a different INSERT form, such as INSERT..VALUES "
+ "or INSERT with a Core Connection"
+ )
+
+ self.statement = statement
+
+
+@CompileState.plugin_for("orm", "update")
+class BulkORMUpdate(BulkUDCompileState, UpdateDMLState):
+ @classmethod
+ def create_for_statement(cls, statement, compiler, **kw):
+ self = cls.__new__(cls)
+
+ dml_strategy = statement._annotations.get(
+ "dml_strategy", "unspecified"
+ )
+
+ toplevel = not compiler.stack
+
+ if toplevel and dml_strategy == "bulk":
+ self._setup_for_bulk_update(statement, compiler)
+ elif (
+ dml_strategy == "core_only"
+ or dml_strategy == "unspecified"
+ and "parententity" not in statement.table._annotations
+ ):
+ UpdateDMLState.__init__(self, statement, compiler, **kw)
+ elif not toplevel or dml_strategy in ("orm", "unspecified"):
+ self._setup_for_orm_update(statement, compiler)
+
+ return self
+
+ def _setup_for_orm_update(self, statement, compiler, **kw):
+ orm_level_statement = statement
+
+ toplevel = not compiler.stack
+
+ ext_info = statement.table._annotations["parententity"]
+
+ self.mapper = mapper = ext_info.mapper
+
+ self._resolved_values = self._get_resolved_values(mapper, statement)
+
+ self._init_global_attributes(
+ statement,
+ compiler,
+ toplevel=toplevel,
+ process_criteria_for_toplevel=toplevel,
+ )
+
+ if statement._values:
+ self._resolved_values = dict(self._resolved_values)
+
+ new_stmt = statement._clone()
+
+ # note if the statement has _multi_values, these
+ # are passed through to the new statement, which will then raise
+ # InvalidRequestError because UPDATE doesn't support multi_values
+ # right now.
+ if statement._ordered_values:
+ new_stmt._ordered_values = self._resolved_values
+ elif statement._values:
+ new_stmt._values = self._resolved_values
+
+ new_crit = self._adjust_for_extra_criteria(
+ self.global_attributes, mapper
+ )
+ if new_crit:
+ new_stmt = new_stmt.where(*new_crit)
+
+ # if we are against a lambda statement we might not be the
+ # topmost object that received per-execute annotations
+
+ # do this first as we need to determine if there is
+ # UPDATE..FROM
+
+ UpdateDMLState.__init__(self, new_stmt, compiler, **kw)
+
+ use_supplemental_cols = False
+
+ if not toplevel:
+ synchronize_session = None
+ else:
+ synchronize_session = compiler._annotations.get(
+ "synchronize_session", None
+ )
+ can_use_returning = compiler._annotations.get(
+ "can_use_returning", None
+ )
+ if can_use_returning is not False:
+ # even though pre_exec has determined basic
+ # can_use_returning for the dialect, if we are to use
+ # RETURNING we need to run can_use_returning() at this level
+ # unconditionally because is_delete_using was not known
+ # at the pre_exec level
+ can_use_returning = (
+ synchronize_session == "fetch"
+ and self.can_use_returning(
+ compiler.dialect, mapper, is_multitable=self.is_multitable
+ )
+ )
+
+ if synchronize_session == "fetch" and can_use_returning:
+ use_supplemental_cols = True
+
+ # NOTE: we might want to RETURNING the actual columns to be
+ # synchronized also. however this is complicated and difficult
+ # to align against the behavior of "evaluate". Additionally,
+ # in a large number (if not the majority) of cases, we have the
+ # "evaluate" answer, usually a fixed value, in memory already and
+ # there's no need to re-fetch the same value
+ # over and over again. so perhaps if it could be RETURNING just
+ # the elements that were based on a SQL expression and not
+ # a constant. For now it doesn't quite seem worth it
+ new_stmt = new_stmt.return_defaults(*new_stmt.table.primary_key)
+
+ if toplevel:
+ new_stmt = self._setup_orm_returning(
+ compiler,
+ orm_level_statement,
+ new_stmt,
+ dml_mapper=mapper,
+ use_supplemental_cols=use_supplemental_cols,
+ )
+
+ self.statement = new_stmt
+
+ def _setup_for_bulk_update(self, statement, compiler, **kw):
+ """establish an UPDATE statement within the context of
+ bulk insert.
+
+ This method will be within the "conn.execute()" call that is invoked
+ by persistence._emit_update_statement().
+
+ """
+ statement = cast(dml.Update, statement)
+ an = statement._annotations
+
+ emit_update_table, _ = (
+ an["_emit_update_table"],
+ an["_emit_update_mapper"],
+ )
+
+ statement = statement._clone()
+ statement.table = emit_update_table
+
+ UpdateDMLState.__init__(self, statement, compiler, **kw)
+
+ if self._ordered_values:
+ raise sa_exc.InvalidRequestError(
+ "bulk ORM UPDATE does not support ordered_values() for "
+ "custom UPDATE statements with bulk parameter sets. Use a "
+ "non-bulk UPDATE statement or use values()."
+ )
+
+ if self._dict_parameters:
+ self._dict_parameters = {
+ col: val
+ for col, val in self._dict_parameters.items()
+ if col.table is emit_update_table
+ }
+ self.statement = statement
+
+ @classmethod
+ def orm_execute_statement(
+ cls,
+ session: Session,
+ statement: dml.Update,
+ params: _CoreAnyExecuteParams,
+ execution_options: OrmExecuteOptionsParameter,
+ bind_arguments: _BindArguments,
+ conn: Connection,
+ ) -> _result.Result:
+ update_options = execution_options.get(
+ "_sa_orm_update_options", cls.default_update_options
+ )
+
+ if update_options._dml_strategy not in (
+ "orm",
+ "auto",
+ "bulk",
+ "core_only",
+ ):
+ raise sa_exc.ArgumentError(
+ "Valid strategies for ORM UPDATE strategy "
+ "are 'orm', 'auto', 'bulk', 'core_only'"
+ )
+
+ result: _result.Result[Any]
+
+ if update_options._dml_strategy == "bulk":
+ enable_check_rowcount = not statement._where_criteria
+
+ assert update_options._synchronize_session != "fetch"
+
+ if (
+ statement._where_criteria
+ and update_options._synchronize_session == "evaluate"
+ ):
+ raise sa_exc.InvalidRequestError(
+ "bulk synchronize of persistent objects not supported "
+ "when using bulk update with additional WHERE "
+ "criteria right now. add synchronize_session=None "
+ "execution option to bypass synchronize of persistent "
+ "objects."
+ )
+ mapper = update_options._subject_mapper
+ assert mapper is not None
+ assert session._transaction is not None
+ result = _bulk_update(
+ mapper,
+ cast(
+ "Iterable[Dict[str, Any]]",
+ [params] if isinstance(params, dict) else params,
+ ),
+ session._transaction,
+ isstates=False,
+ update_changed_only=False,
+ use_orm_update_stmt=statement,
+ enable_check_rowcount=enable_check_rowcount,
+ )
+ return cls.orm_setup_cursor_result(
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ result,
+ )
+ else:
+ return super().orm_execute_statement(
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ conn,
+ )
+
+ @classmethod
+ def can_use_returning(
+ cls,
+ dialect: Dialect,
+ mapper: Mapper[Any],
+ *,
+ is_multitable: bool = False,
+ is_update_from: bool = False,
+ is_delete_using: bool = False,
+ is_executemany: bool = False,
+ ) -> bool:
+ # normal answer for "should we use RETURNING" at all.
+ normal_answer = (
+ dialect.update_returning and mapper.local_table.implicit_returning
+ )
+ if not normal_answer:
+ return False
+
+ if is_executemany:
+ return dialect.update_executemany_returning
+
+ # these workarounds are currently hypothetical for UPDATE,
+ # unlike DELETE where they impact MariaDB
+ if is_update_from:
+ return dialect.update_returning_multifrom
+
+ elif is_multitable and not dialect.update_returning_multifrom:
+ raise sa_exc.CompileError(
+ f'Dialect "{dialect.name}" does not support RETURNING '
+ "with UPDATE..FROM; for synchronize_session='fetch', "
+ "please add the additional execution option "
+ "'is_update_from=True' to the statement to indicate that "
+ "a separate SELECT should be used for this backend."
+ )
+
+ return True
+
+ @classmethod
+ def _do_post_synchronize_bulk_evaluate(
+ cls, session, params, result, update_options
+ ):
+ if not params:
+ return
+
+ mapper = update_options._subject_mapper
+ pk_keys = [prop.key for prop in mapper._identity_key_props]
+
+ identity_map = session.identity_map
+
+ for param in params:
+ identity_key = mapper.identity_key_from_primary_key(
+ (param[key] for key in pk_keys),
+ update_options._identity_token,
+ )
+ state = identity_map.fast_get_state(identity_key)
+ if not state:
+ continue
+
+ evaluated_keys = set(param).difference(pk_keys)
+
+ dict_ = state.dict
+ # only evaluate unmodified attributes
+ to_evaluate = state.unmodified.intersection(evaluated_keys)
+ for key in to_evaluate:
+ if key in dict_:
+ dict_[key] = param[key]
+
+ state.manager.dispatch.refresh(state, None, to_evaluate)
+
+ state._commit(dict_, list(to_evaluate))
+
+ # attributes that were formerly modified instead get expired.
+ # this only gets hit if the session had pending changes
+ # and autoflush were set to False.
+ to_expire = evaluated_keys.intersection(dict_).difference(
+ to_evaluate
+ )
+ if to_expire:
+ state._expire_attributes(dict_, to_expire)
+
+ @classmethod
+ def _do_post_synchronize_evaluate(
+ cls, session, statement, result, update_options
+ ):
+ matched_objects = cls._get_matched_objects_on_criteria(
+ update_options,
+ session.identity_map.all_states(),
+ )
+
+ cls._apply_update_set_values_to_objects(
+ session,
+ update_options,
+ statement,
+ [(obj, state, dict_) for obj, state, dict_, _ in matched_objects],
+ )
+
+ @classmethod
+ def _do_post_synchronize_fetch(
+ cls, session, statement, result, update_options
+ ):
+ target_mapper = update_options._subject_mapper
+
+ returned_defaults_rows = result.returned_defaults_rows
+ if returned_defaults_rows:
+ pk_rows = cls._interpret_returning_rows(
+ target_mapper, returned_defaults_rows
+ )
+
+ matched_rows = [
+ tuple(row) + (update_options._identity_token,)
+ for row in pk_rows
+ ]
+ else:
+ matched_rows = update_options._matched_rows
+
+ objs = [
+ session.identity_map[identity_key]
+ for identity_key in [
+ target_mapper.identity_key_from_primary_key(
+ list(primary_key),
+ identity_token=identity_token,
+ )
+ for primary_key, identity_token in [
+ (row[0:-1], row[-1]) for row in matched_rows
+ ]
+ if update_options._identity_token is None
+ or identity_token == update_options._identity_token
+ ]
+ if identity_key in session.identity_map
+ ]
+
+ if not objs:
+ return
+
+ cls._apply_update_set_values_to_objects(
+ session,
+ update_options,
+ statement,
+ [
+ (
+ obj,
+ attributes.instance_state(obj),
+ attributes.instance_dict(obj),
+ )
+ for obj in objs
+ ],
+ )
+
+ @classmethod
+ def _apply_update_set_values_to_objects(
+ cls, session, update_options, statement, matched_objects
+ ):
+ """apply values to objects derived from an update statement, e.g.
+ UPDATE..SET <values>
+
+ """
+ mapper = update_options._subject_mapper
+ target_cls = mapper.class_
+ evaluator_compiler = evaluator._EvaluatorCompiler(target_cls)
+ resolved_values = cls._get_resolved_values(mapper, statement)
+ resolved_keys_as_propnames = cls._resolved_keys_as_propnames(
+ mapper, resolved_values
+ )
+ value_evaluators = {}
+ for key, value in resolved_keys_as_propnames:
+ try:
+ _evaluator = evaluator_compiler.process(
+ coercions.expect(roles.ExpressionElementRole, value)
+ )
+ except evaluator.UnevaluatableError:
+ pass
+ else:
+ value_evaluators[key] = _evaluator
+
+ evaluated_keys = list(value_evaluators.keys())
+ attrib = {k for k, v in resolved_keys_as_propnames}
+
+ states = set()
+ for obj, state, dict_ in matched_objects:
+ to_evaluate = state.unmodified.intersection(evaluated_keys)
+
+ for key in to_evaluate:
+ if key in dict_:
+ # only run eval for attributes that are present.
+ dict_[key] = value_evaluators[key](obj)
+
+ state.manager.dispatch.refresh(state, None, to_evaluate)
+
+ state._commit(dict_, list(to_evaluate))
+
+ # attributes that were formerly modified instead get expired.
+ # this only gets hit if the session had pending changes
+ # and autoflush were set to False.
+ to_expire = attrib.intersection(dict_).difference(to_evaluate)
+ if to_expire:
+ state._expire_attributes(dict_, to_expire)
+
+ states.add(state)
+ session._register_altered(states)
+
+
+@CompileState.plugin_for("orm", "delete")
+class BulkORMDelete(BulkUDCompileState, DeleteDMLState):
+ @classmethod
+ def create_for_statement(cls, statement, compiler, **kw):
+ self = cls.__new__(cls)
+
+ dml_strategy = statement._annotations.get(
+ "dml_strategy", "unspecified"
+ )
+
+ if (
+ dml_strategy == "core_only"
+ or dml_strategy == "unspecified"
+ and "parententity" not in statement.table._annotations
+ ):
+ DeleteDMLState.__init__(self, statement, compiler, **kw)
+ return self
+
+ toplevel = not compiler.stack
+
+ orm_level_statement = statement
+
+ ext_info = statement.table._annotations["parententity"]
+ self.mapper = mapper = ext_info.mapper
+
+ self._init_global_attributes(
+ statement,
+ compiler,
+ toplevel=toplevel,
+ process_criteria_for_toplevel=toplevel,
+ )
+
+ new_stmt = statement._clone()
+
+ new_crit = cls._adjust_for_extra_criteria(
+ self.global_attributes, mapper
+ )
+ if new_crit:
+ new_stmt = new_stmt.where(*new_crit)
+
+ # do this first as we need to determine if there is
+ # DELETE..FROM
+ DeleteDMLState.__init__(self, new_stmt, compiler, **kw)
+
+ use_supplemental_cols = False
+
+ if not toplevel:
+ synchronize_session = None
+ else:
+ synchronize_session = compiler._annotations.get(
+ "synchronize_session", None
+ )
+ can_use_returning = compiler._annotations.get(
+ "can_use_returning", None
+ )
+ if can_use_returning is not False:
+ # even though pre_exec has determined basic
+ # can_use_returning for the dialect, if we are to use
+ # RETURNING we need to run can_use_returning() at this level
+ # unconditionally because is_delete_using was not known
+ # at the pre_exec level
+ can_use_returning = (
+ synchronize_session == "fetch"
+ and self.can_use_returning(
+ compiler.dialect,
+ mapper,
+ is_multitable=self.is_multitable,
+ is_delete_using=compiler._annotations.get(
+ "is_delete_using", False
+ ),
+ )
+ )
+
+ if can_use_returning:
+ use_supplemental_cols = True
+
+ new_stmt = new_stmt.return_defaults(*new_stmt.table.primary_key)
+
+ if toplevel:
+ new_stmt = self._setup_orm_returning(
+ compiler,
+ orm_level_statement,
+ new_stmt,
+ dml_mapper=mapper,
+ use_supplemental_cols=use_supplemental_cols,
+ )
+
+ self.statement = new_stmt
+
+ return self
+
+ @classmethod
+ def orm_execute_statement(
+ cls,
+ session: Session,
+ statement: dml.Delete,
+ params: _CoreAnyExecuteParams,
+ execution_options: OrmExecuteOptionsParameter,
+ bind_arguments: _BindArguments,
+ conn: Connection,
+ ) -> _result.Result:
+ update_options = execution_options.get(
+ "_sa_orm_update_options", cls.default_update_options
+ )
+
+ if update_options._dml_strategy == "bulk":
+ raise sa_exc.InvalidRequestError(
+ "Bulk ORM DELETE not supported right now. "
+ "Statement may be invoked at the "
+ "Core level using "
+ "session.connection().execute(stmt, parameters)"
+ )
+
+ if update_options._dml_strategy not in ("orm", "auto", "core_only"):
+ raise sa_exc.ArgumentError(
+ "Valid strategies for ORM DELETE strategy are 'orm', 'auto', "
+ "'core_only'"
+ )
+
+ return super().orm_execute_statement(
+ session, statement, params, execution_options, bind_arguments, conn
+ )
+
+ @classmethod
+ def can_use_returning(
+ cls,
+ dialect: Dialect,
+ mapper: Mapper[Any],
+ *,
+ is_multitable: bool = False,
+ is_update_from: bool = False,
+ is_delete_using: bool = False,
+ is_executemany: bool = False,
+ ) -> bool:
+ # normal answer for "should we use RETURNING" at all.
+ normal_answer = (
+ dialect.delete_returning and mapper.local_table.implicit_returning
+ )
+ if not normal_answer:
+ return False
+
+ # now get into special workarounds because MariaDB supports
+ # DELETE...RETURNING but not DELETE...USING...RETURNING.
+ if is_delete_using:
+ # is_delete_using hint was passed. use
+ # additional dialect feature (True for PG, False for MariaDB)
+ return dialect.delete_returning_multifrom
+
+ elif is_multitable and not dialect.delete_returning_multifrom:
+ # is_delete_using hint was not passed, but we determined
+ # at compile time that this is in fact a DELETE..USING.
+ # it's too late to continue since we did not pre-SELECT.
+ # raise that we need that hint up front.
+
+ raise sa_exc.CompileError(
+ f'Dialect "{dialect.name}" does not support RETURNING '
+ "with DELETE..USING; for synchronize_session='fetch', "
+ "please add the additional execution option "
+ "'is_delete_using=True' to the statement to indicate that "
+ "a separate SELECT should be used for this backend."
+ )
+
+ return True
+
+ @classmethod
+ def _do_post_synchronize_evaluate(
+ cls, session, statement, result, update_options
+ ):
+ matched_objects = cls._get_matched_objects_on_criteria(
+ update_options,
+ session.identity_map.all_states(),
+ )
+
+ to_delete = []
+
+ for _, state, dict_, is_partially_expired in matched_objects:
+ if is_partially_expired:
+ state._expire(dict_, session.identity_map._modified)
+ else:
+ to_delete.append(state)
+
+ if to_delete:
+ session._remove_newly_deleted(to_delete)
+
+ @classmethod
+ def _do_post_synchronize_fetch(
+ cls, session, statement, result, update_options
+ ):
+ target_mapper = update_options._subject_mapper
+
+ returned_defaults_rows = result.returned_defaults_rows
+
+ if returned_defaults_rows:
+ pk_rows = cls._interpret_returning_rows(
+ target_mapper, returned_defaults_rows
+ )
+
+ matched_rows = [
+ tuple(row) + (update_options._identity_token,)
+ for row in pk_rows
+ ]
+ else:
+ matched_rows = update_options._matched_rows
+
+ for row in matched_rows:
+ primary_key = row[0:-1]
+ identity_token = row[-1]
+
+ # TODO: inline this and call remove_newly_deleted
+ # once
+ identity_key = target_mapper.identity_key_from_primary_key(
+ list(primary_key),
+ identity_token=identity_token,
+ )
+ if identity_key in session.identity_map:
+ session._remove_newly_deleted(
+ [
+ attributes.instance_state(
+ session.identity_map[identity_key]
+ )
+ ]
+ )
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/clsregistry.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/clsregistry.py
new file mode 100644
index 0000000..26113d8
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/clsregistry.py
@@ -0,0 +1,570 @@
+# orm/clsregistry.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""Routines to handle the string class registry used by declarative.
+
+This system allows specification of classes and expressions used in
+:func:`_orm.relationship` using strings.
+
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Generator
+from typing import Iterable
+from typing import List
+from typing import Mapping
+from typing import MutableMapping
+from typing import NoReturn
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import attributes
+from . import interfaces
+from .descriptor_props import SynonymProperty
+from .properties import ColumnProperty
+from .util import class_mapper
+from .. import exc
+from .. import inspection
+from .. import util
+from ..sql.schema import _get_table_key
+from ..util.typing import CallableReference
+
+if TYPE_CHECKING:
+ from .relationships import RelationshipProperty
+ from ..sql.schema import MetaData
+ from ..sql.schema import Table
+
+_T = TypeVar("_T", bound=Any)
+
+_ClsRegistryType = MutableMapping[str, Union[type, "ClsRegistryToken"]]
+
+# strong references to registries which we place in
+# the _decl_class_registry, which is usually weak referencing.
+# the internal registries here link to classes with weakrefs and remove
+# themselves when all references to contained classes are removed.
+_registries: Set[ClsRegistryToken] = set()
+
+
+def add_class(
+ classname: str, cls: Type[_T], decl_class_registry: _ClsRegistryType
+) -> None:
+ """Add a class to the _decl_class_registry associated with the
+ given declarative class.
+
+ """
+ if classname in decl_class_registry:
+ # class already exists.
+ existing = decl_class_registry[classname]
+ if not isinstance(existing, _MultipleClassMarker):
+ existing = decl_class_registry[classname] = _MultipleClassMarker(
+ [cls, cast("Type[Any]", existing)]
+ )
+ else:
+ decl_class_registry[classname] = cls
+
+ try:
+ root_module = cast(
+ _ModuleMarker, decl_class_registry["_sa_module_registry"]
+ )
+ except KeyError:
+ decl_class_registry["_sa_module_registry"] = root_module = (
+ _ModuleMarker("_sa_module_registry", None)
+ )
+
+ tokens = cls.__module__.split(".")
+
+ # build up a tree like this:
+ # modulename: myapp.snacks.nuts
+ #
+ # myapp->snack->nuts->(classes)
+ # snack->nuts->(classes)
+ # nuts->(classes)
+ #
+ # this allows partial token paths to be used.
+ while tokens:
+ token = tokens.pop(0)
+ module = root_module.get_module(token)
+ for token in tokens:
+ module = module.get_module(token)
+
+ try:
+ module.add_class(classname, cls)
+ except AttributeError as ae:
+ if not isinstance(module, _ModuleMarker):
+ raise exc.InvalidRequestError(
+ f'name "{classname}" matches both a '
+ "class name and a module name"
+ ) from ae
+ else:
+ raise
+
+
+def remove_class(
+ classname: str, cls: Type[Any], decl_class_registry: _ClsRegistryType
+) -> None:
+ if classname in decl_class_registry:
+ existing = decl_class_registry[classname]
+ if isinstance(existing, _MultipleClassMarker):
+ existing.remove_item(cls)
+ else:
+ del decl_class_registry[classname]
+
+ try:
+ root_module = cast(
+ _ModuleMarker, decl_class_registry["_sa_module_registry"]
+ )
+ except KeyError:
+ return
+
+ tokens = cls.__module__.split(".")
+
+ while tokens:
+ token = tokens.pop(0)
+ module = root_module.get_module(token)
+ for token in tokens:
+ module = module.get_module(token)
+ try:
+ module.remove_class(classname, cls)
+ except AttributeError:
+ if not isinstance(module, _ModuleMarker):
+ pass
+ else:
+ raise
+
+
+def _key_is_empty(
+ key: str,
+ decl_class_registry: _ClsRegistryType,
+ test: Callable[[Any], bool],
+) -> bool:
+ """test if a key is empty of a certain object.
+
+ used for unit tests against the registry to see if garbage collection
+ is working.
+
+ "test" is a callable that will be passed an object should return True
+ if the given object is the one we were looking for.
+
+ We can't pass the actual object itself b.c. this is for testing garbage
+ collection; the caller will have to have removed references to the
+ object itself.
+
+ """
+ if key not in decl_class_registry:
+ return True
+
+ thing = decl_class_registry[key]
+ if isinstance(thing, _MultipleClassMarker):
+ for sub_thing in thing.contents:
+ if test(sub_thing):
+ return False
+ else:
+ raise NotImplementedError("unknown codepath")
+ else:
+ return not test(thing)
+
+
+class ClsRegistryToken:
+ """an object that can be in the registry._class_registry as a value."""
+
+ __slots__ = ()
+
+
+class _MultipleClassMarker(ClsRegistryToken):
+ """refers to multiple classes of the same name
+ within _decl_class_registry.
+
+ """
+
+ __slots__ = "on_remove", "contents", "__weakref__"
+
+ contents: Set[weakref.ref[Type[Any]]]
+ on_remove: CallableReference[Optional[Callable[[], None]]]
+
+ def __init__(
+ self,
+ classes: Iterable[Type[Any]],
+ on_remove: Optional[Callable[[], None]] = None,
+ ):
+ self.on_remove = on_remove
+ self.contents = {
+ weakref.ref(item, self._remove_item) for item in classes
+ }
+ _registries.add(self)
+
+ def remove_item(self, cls: Type[Any]) -> None:
+ self._remove_item(weakref.ref(cls))
+
+ def __iter__(self) -> Generator[Optional[Type[Any]], None, None]:
+ return (ref() for ref in self.contents)
+
+ def attempt_get(self, path: List[str], key: str) -> Type[Any]:
+ if len(self.contents) > 1:
+ raise exc.InvalidRequestError(
+ 'Multiple classes found for path "%s" '
+ "in the registry of this declarative "
+ "base. Please use a fully module-qualified path."
+ % (".".join(path + [key]))
+ )
+ else:
+ ref = list(self.contents)[0]
+ cls = ref()
+ if cls is None:
+ raise NameError(key)
+ return cls
+
+ def _remove_item(self, ref: weakref.ref[Type[Any]]) -> None:
+ self.contents.discard(ref)
+ if not self.contents:
+ _registries.discard(self)
+ if self.on_remove:
+ self.on_remove()
+
+ def add_item(self, item: Type[Any]) -> None:
+ # protect against class registration race condition against
+ # asynchronous garbage collection calling _remove_item,
+ # [ticket:3208] and [ticket:10782]
+ modules = {
+ cls.__module__
+ for cls in [ref() for ref in list(self.contents)]
+ if cls is not None
+ }
+ if item.__module__ in modules:
+ util.warn(
+ "This declarative base already contains a class with the "
+ "same class name and module name as %s.%s, and will "
+ "be replaced in the string-lookup table."
+ % (item.__module__, item.__name__)
+ )
+ self.contents.add(weakref.ref(item, self._remove_item))
+
+
+class _ModuleMarker(ClsRegistryToken):
+ """Refers to a module name within
+ _decl_class_registry.
+
+ """
+
+ __slots__ = "parent", "name", "contents", "mod_ns", "path", "__weakref__"
+
+ parent: Optional[_ModuleMarker]
+ contents: Dict[str, Union[_ModuleMarker, _MultipleClassMarker]]
+ mod_ns: _ModNS
+ path: List[str]
+
+ def __init__(self, name: str, parent: Optional[_ModuleMarker]):
+ self.parent = parent
+ self.name = name
+ self.contents = {}
+ self.mod_ns = _ModNS(self)
+ if self.parent:
+ self.path = self.parent.path + [self.name]
+ else:
+ self.path = []
+ _registries.add(self)
+
+ def __contains__(self, name: str) -> bool:
+ return name in self.contents
+
+ def __getitem__(self, name: str) -> ClsRegistryToken:
+ return self.contents[name]
+
+ def _remove_item(self, name: str) -> None:
+ self.contents.pop(name, None)
+ if not self.contents and self.parent is not None:
+ self.parent._remove_item(self.name)
+ _registries.discard(self)
+
+ def resolve_attr(self, key: str) -> Union[_ModNS, Type[Any]]:
+ return self.mod_ns.__getattr__(key)
+
+ def get_module(self, name: str) -> _ModuleMarker:
+ if name not in self.contents:
+ marker = _ModuleMarker(name, self)
+ self.contents[name] = marker
+ else:
+ marker = cast(_ModuleMarker, self.contents[name])
+ return marker
+
+ def add_class(self, name: str, cls: Type[Any]) -> None:
+ if name in self.contents:
+ existing = cast(_MultipleClassMarker, self.contents[name])
+ try:
+ existing.add_item(cls)
+ except AttributeError as ae:
+ if not isinstance(existing, _MultipleClassMarker):
+ raise exc.InvalidRequestError(
+ f'name "{name}" matches both a '
+ "class name and a module name"
+ ) from ae
+ else:
+ raise
+ else:
+ existing = self.contents[name] = _MultipleClassMarker(
+ [cls], on_remove=lambda: self._remove_item(name)
+ )
+
+ def remove_class(self, name: str, cls: Type[Any]) -> None:
+ if name in self.contents:
+ existing = cast(_MultipleClassMarker, self.contents[name])
+ existing.remove_item(cls)
+
+
+class _ModNS:
+ __slots__ = ("__parent",)
+
+ __parent: _ModuleMarker
+
+ def __init__(self, parent: _ModuleMarker):
+ self.__parent = parent
+
+ def __getattr__(self, key: str) -> Union[_ModNS, Type[Any]]:
+ try:
+ value = self.__parent.contents[key]
+ except KeyError:
+ pass
+ else:
+ if value is not None:
+ if isinstance(value, _ModuleMarker):
+ return value.mod_ns
+ else:
+ assert isinstance(value, _MultipleClassMarker)
+ return value.attempt_get(self.__parent.path, key)
+ raise NameError(
+ "Module %r has no mapped classes "
+ "registered under the name %r" % (self.__parent.name, key)
+ )
+
+
+class _GetColumns:
+ __slots__ = ("cls",)
+
+ cls: Type[Any]
+
+ def __init__(self, cls: Type[Any]):
+ self.cls = cls
+
+ def __getattr__(self, key: str) -> Any:
+ mp = class_mapper(self.cls, configure=False)
+ if mp:
+ if key not in mp.all_orm_descriptors:
+ raise AttributeError(
+ "Class %r does not have a mapped column named %r"
+ % (self.cls, key)
+ )
+
+ desc = mp.all_orm_descriptors[key]
+ if desc.extension_type is interfaces.NotExtension.NOT_EXTENSION:
+ assert isinstance(desc, attributes.QueryableAttribute)
+ prop = desc.property
+ if isinstance(prop, SynonymProperty):
+ key = prop.name
+ elif not isinstance(prop, ColumnProperty):
+ raise exc.InvalidRequestError(
+ "Property %r is not an instance of"
+ " ColumnProperty (i.e. does not correspond"
+ " directly to a Column)." % key
+ )
+ return getattr(self.cls, key)
+
+
+inspection._inspects(_GetColumns)(
+ lambda target: inspection.inspect(target.cls)
+)
+
+
+class _GetTable:
+ __slots__ = "key", "metadata"
+
+ key: str
+ metadata: MetaData
+
+ def __init__(self, key: str, metadata: MetaData):
+ self.key = key
+ self.metadata = metadata
+
+ def __getattr__(self, key: str) -> Table:
+ return self.metadata.tables[_get_table_key(key, self.key)]
+
+
+def _determine_container(key: str, value: Any) -> _GetColumns:
+ if isinstance(value, _MultipleClassMarker):
+ value = value.attempt_get([], key)
+ return _GetColumns(value)
+
+
+class _class_resolver:
+ __slots__ = (
+ "cls",
+ "prop",
+ "arg",
+ "fallback",
+ "_dict",
+ "_resolvers",
+ "favor_tables",
+ )
+
+ cls: Type[Any]
+ prop: RelationshipProperty[Any]
+ fallback: Mapping[str, Any]
+ arg: str
+ favor_tables: bool
+ _resolvers: Tuple[Callable[[str], Any], ...]
+
+ def __init__(
+ self,
+ cls: Type[Any],
+ prop: RelationshipProperty[Any],
+ fallback: Mapping[str, Any],
+ arg: str,
+ favor_tables: bool = False,
+ ):
+ self.cls = cls
+ self.prop = prop
+ self.arg = arg
+ self.fallback = fallback
+ self._dict = util.PopulateDict(self._access_cls)
+ self._resolvers = ()
+ self.favor_tables = favor_tables
+
+ def _access_cls(self, key: str) -> Any:
+ cls = self.cls
+
+ manager = attributes.manager_of_class(cls)
+ decl_base = manager.registry
+ assert decl_base is not None
+ decl_class_registry = decl_base._class_registry
+ metadata = decl_base.metadata
+
+ if self.favor_tables:
+ if key in metadata.tables:
+ return metadata.tables[key]
+ elif key in metadata._schemas:
+ return _GetTable(key, getattr(cls, "metadata", metadata))
+
+ if key in decl_class_registry:
+ return _determine_container(key, decl_class_registry[key])
+
+ if not self.favor_tables:
+ if key in metadata.tables:
+ return metadata.tables[key]
+ elif key in metadata._schemas:
+ return _GetTable(key, getattr(cls, "metadata", metadata))
+
+ if "_sa_module_registry" in decl_class_registry and key in cast(
+ _ModuleMarker, decl_class_registry["_sa_module_registry"]
+ ):
+ registry = cast(
+ _ModuleMarker, decl_class_registry["_sa_module_registry"]
+ )
+ return registry.resolve_attr(key)
+ elif self._resolvers:
+ for resolv in self._resolvers:
+ value = resolv(key)
+ if value is not None:
+ return value
+
+ return self.fallback[key]
+
+ def _raise_for_name(self, name: str, err: Exception) -> NoReturn:
+ generic_match = re.match(r"(.+)\[(.+)\]", name)
+
+ if generic_match:
+ clsarg = generic_match.group(2).strip("'")
+ raise exc.InvalidRequestError(
+ f"When initializing mapper {self.prop.parent}, "
+ f'expression "relationship({self.arg!r})" seems to be '
+ "using a generic class as the argument to relationship(); "
+ "please state the generic argument "
+ "using an annotation, e.g. "
+ f'"{self.prop.key}: Mapped[{generic_match.group(1)}'
+ f"['{clsarg}']] = relationship()\""
+ ) from err
+ else:
+ raise exc.InvalidRequestError(
+ "When initializing mapper %s, expression %r failed to "
+ "locate a name (%r). If this is a class name, consider "
+ "adding this relationship() to the %r class after "
+ "both dependent classes have been defined."
+ % (self.prop.parent, self.arg, name, self.cls)
+ ) from err
+
+ def _resolve_name(self) -> Union[Table, Type[Any], _ModNS]:
+ name = self.arg
+ d = self._dict
+ rval = None
+ try:
+ for token in name.split("."):
+ if rval is None:
+ rval = d[token]
+ else:
+ rval = getattr(rval, token)
+ except KeyError as err:
+ self._raise_for_name(name, err)
+ except NameError as n:
+ self._raise_for_name(n.args[0], n)
+ else:
+ if isinstance(rval, _GetColumns):
+ return rval.cls
+ else:
+ if TYPE_CHECKING:
+ assert isinstance(rval, (type, Table, _ModNS))
+ return rval
+
+ def __call__(self) -> Any:
+ try:
+ x = eval(self.arg, globals(), self._dict)
+
+ if isinstance(x, _GetColumns):
+ return x.cls
+ else:
+ return x
+ except NameError as n:
+ self._raise_for_name(n.args[0], n)
+
+
+_fallback_dict: Mapping[str, Any] = None # type: ignore
+
+
+def _resolver(cls: Type[Any], prop: RelationshipProperty[Any]) -> Tuple[
+ Callable[[str], Callable[[], Union[Type[Any], Table, _ModNS]]],
+ Callable[[str, bool], _class_resolver],
+]:
+ global _fallback_dict
+
+ if _fallback_dict is None:
+ import sqlalchemy
+ from . import foreign
+ from . import remote
+
+ _fallback_dict = util.immutabledict(sqlalchemy.__dict__).union(
+ {"foreign": foreign, "remote": remote}
+ )
+
+ def resolve_arg(arg: str, favor_tables: bool = False) -> _class_resolver:
+ return _class_resolver(
+ cls, prop, _fallback_dict, arg, favor_tables=favor_tables
+ )
+
+ def resolve_name(
+ arg: str,
+ ) -> Callable[[], Union[Type[Any], Table, _ModNS]]:
+ return _class_resolver(cls, prop, _fallback_dict, arg)._resolve_name
+
+ return resolve_name, resolve_arg
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/collections.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/collections.py
new file mode 100644
index 0000000..6fefd78
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/collections.py
@@ -0,0 +1,1618 @@
+# orm/collections.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: allow-untyped-defs, allow-untyped-calls
+
+"""Support for collections of mapped entities.
+
+The collections package supplies the machinery used to inform the ORM of
+collection membership changes. An instrumentation via decoration approach is
+used, allowing arbitrary types (including built-ins) to be used as entity
+collections without requiring inheritance from a base class.
+
+Instrumentation decoration relays membership change events to the
+:class:`.CollectionAttributeImpl` that is currently managing the collection.
+The decorators observe function call arguments and return values, tracking
+entities entering or leaving the collection. Two decorator approaches are
+provided. One is a bundle of generic decorators that map function arguments
+and return values to events::
+
+ from sqlalchemy.orm.collections import collection
+ class MyClass:
+ # ...
+
+ @collection.adds(1)
+ def store(self, item):
+ self.data.append(item)
+
+ @collection.removes_return()
+ def pop(self):
+ return self.data.pop()
+
+
+The second approach is a bundle of targeted decorators that wrap appropriate
+append and remove notifiers around the mutation methods present in the
+standard Python ``list``, ``set`` and ``dict`` interfaces. These could be
+specified in terms of generic decorator recipes, but are instead hand-tooled
+for increased efficiency. The targeted decorators occasionally implement
+adapter-like behavior, such as mapping bulk-set methods (``extend``,
+``update``, ``__setslice__``, etc.) into the series of atomic mutation events
+that the ORM requires.
+
+The targeted decorators are used internally for automatic instrumentation of
+entity collection classes. Every collection class goes through a
+transformation process roughly like so:
+
+1. If the class is a built-in, substitute a trivial sub-class
+2. Is this class already instrumented?
+3. Add in generic decorators
+4. Sniff out the collection interface through duck-typing
+5. Add targeted decoration to any undecorated interface method
+
+This process modifies the class at runtime, decorating methods and adding some
+bookkeeping properties. This isn't possible (or desirable) for built-in
+classes like ``list``, so trivial sub-classes are substituted to hold
+decoration::
+
+ class InstrumentedList(list):
+ pass
+
+Collection classes can be specified in ``relationship(collection_class=)`` as
+types or a function that returns an instance. Collection classes are
+inspected and instrumented during the mapper compilation phase. The
+collection_class callable will be executed once to produce a specimen
+instance, and the type of that specimen will be instrumented. Functions that
+return built-in types like ``lists`` will be adapted to produce instrumented
+instances.
+
+When extending a known type like ``list``, additional decorations are not
+generally not needed. Odds are, the extension method will delegate to a
+method that's already instrumented. For example::
+
+ class QueueIsh(list):
+ def push(self, item):
+ self.append(item)
+ def shift(self):
+ return self.pop(0)
+
+There's no need to decorate these methods. ``append`` and ``pop`` are already
+instrumented as part of the ``list`` interface. Decorating them would fire
+duplicate events, which should be avoided.
+
+The targeted decoration tries not to rely on other methods in the underlying
+collection class, but some are unavoidable. Many depend on 'read' methods
+being present to properly instrument a 'write', for example, ``__setitem__``
+needs ``__getitem__``. "Bulk" methods like ``update`` and ``extend`` may also
+reimplemented in terms of atomic appends and removes, so the ``extend``
+decoration will actually perform many ``append`` operations and not call the
+underlying method at all.
+
+Tight control over bulk operation and the firing of events is also possible by
+implementing the instrumentation internally in your methods. The basic
+instrumentation package works under the general assumption that collection
+mutation will not raise unusual exceptions. If you want to closely
+orchestrate append and remove events with exception management, internal
+instrumentation may be the answer. Within your method,
+``collection_adapter(self)`` will retrieve an object that you can use for
+explicit control over triggering append and remove events.
+
+The owning object and :class:`.CollectionAttributeImpl` are also reachable
+through the adapter, allowing for some very sophisticated behavior.
+
+"""
+from __future__ import annotations
+
+import operator
+import threading
+import typing
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Collection
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import NoReturn
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from .base import NO_KEY
+from .. import exc as sa_exc
+from .. import util
+from ..sql.base import NO_ARG
+from ..util.compat import inspect_getfullargspec
+from ..util.typing import Protocol
+
+if typing.TYPE_CHECKING:
+ from .attributes import AttributeEventToken
+ from .attributes import CollectionAttributeImpl
+ from .mapped_collection import attribute_keyed_dict
+ from .mapped_collection import column_keyed_dict
+ from .mapped_collection import keyfunc_mapping
+ from .mapped_collection import KeyFuncDict # noqa: F401
+ from .state import InstanceState
+
+
+__all__ = [
+ "collection",
+ "collection_adapter",
+ "keyfunc_mapping",
+ "column_keyed_dict",
+ "attribute_keyed_dict",
+ "column_keyed_dict",
+ "attribute_keyed_dict",
+ "MappedCollection",
+ "KeyFuncDict",
+]
+
+__instrumentation_mutex = threading.Lock()
+
+
+_CollectionFactoryType = Callable[[], "_AdaptedCollectionProtocol"]
+
+_T = TypeVar("_T", bound=Any)
+_KT = TypeVar("_KT", bound=Any)
+_VT = TypeVar("_VT", bound=Any)
+_COL = TypeVar("_COL", bound="Collection[Any]")
+_FN = TypeVar("_FN", bound="Callable[..., Any]")
+
+
+class _CollectionConverterProtocol(Protocol):
+ def __call__(self, collection: _COL) -> _COL: ...
+
+
+class _AdaptedCollectionProtocol(Protocol):
+ _sa_adapter: CollectionAdapter
+ _sa_appender: Callable[..., Any]
+ _sa_remover: Callable[..., Any]
+ _sa_iterator: Callable[..., Iterable[Any]]
+ _sa_converter: _CollectionConverterProtocol
+
+
+class collection:
+ """Decorators for entity collection classes.
+
+ The decorators fall into two groups: annotations and interception recipes.
+
+ The annotating decorators (appender, remover, iterator, converter,
+ internally_instrumented) indicate the method's purpose and take no
+ arguments. They are not written with parens::
+
+ @collection.appender
+ def append(self, append): ...
+
+ The recipe decorators all require parens, even those that take no
+ arguments::
+
+ @collection.adds('entity')
+ def insert(self, position, entity): ...
+
+ @collection.removes_return()
+ def popitem(self): ...
+
+ """
+
+ # Bundled as a class solely for ease of use: packaging, doc strings,
+ # importability.
+
+ @staticmethod
+ def appender(fn):
+ """Tag the method as the collection appender.
+
+ The appender method is called with one positional argument: the value
+ to append. The method will be automatically decorated with 'adds(1)'
+ if not already decorated::
+
+ @collection.appender
+ def add(self, append): ...
+
+ # or, equivalently
+ @collection.appender
+ @collection.adds(1)
+ def add(self, append): ...
+
+ # for mapping type, an 'append' may kick out a previous value
+ # that occupies that slot. consider d['a'] = 'foo'- any previous
+ # value in d['a'] is discarded.
+ @collection.appender
+ @collection.replaces(1)
+ def add(self, entity):
+ key = some_key_func(entity)
+ previous = None
+ if key in self:
+ previous = self[key]
+ self[key] = entity
+ return previous
+
+ If the value to append is not allowed in the collection, you may
+ raise an exception. Something to remember is that the appender
+ will be called for each object mapped by a database query. If the
+ database contains rows that violate your collection semantics, you
+ will need to get creative to fix the problem, as access via the
+ collection will not work.
+
+ If the appender method is internally instrumented, you must also
+ receive the keyword argument '_sa_initiator' and ensure its
+ promulgation to collection events.
+
+ """
+ fn._sa_instrument_role = "appender"
+ return fn
+
+ @staticmethod
+ def remover(fn):
+ """Tag the method as the collection remover.
+
+ The remover method is called with one positional argument: the value
+ to remove. The method will be automatically decorated with
+ :meth:`removes_return` if not already decorated::
+
+ @collection.remover
+ def zap(self, entity): ...
+
+ # or, equivalently
+ @collection.remover
+ @collection.removes_return()
+ def zap(self, ): ...
+
+ If the value to remove is not present in the collection, you may
+ raise an exception or return None to ignore the error.
+
+ If the remove method is internally instrumented, you must also
+ receive the keyword argument '_sa_initiator' and ensure its
+ promulgation to collection events.
+
+ """
+ fn._sa_instrument_role = "remover"
+ return fn
+
+ @staticmethod
+ def iterator(fn):
+ """Tag the method as the collection remover.
+
+ The iterator method is called with no arguments. It is expected to
+ return an iterator over all collection members::
+
+ @collection.iterator
+ def __iter__(self): ...
+
+ """
+ fn._sa_instrument_role = "iterator"
+ return fn
+
+ @staticmethod
+ def internally_instrumented(fn):
+ """Tag the method as instrumented.
+
+ This tag will prevent any decoration from being applied to the
+ method. Use this if you are orchestrating your own calls to
+ :func:`.collection_adapter` in one of the basic SQLAlchemy
+ interface methods, or to prevent an automatic ABC method
+ decoration from wrapping your implementation::
+
+ # normally an 'extend' method on a list-like class would be
+ # automatically intercepted and re-implemented in terms of
+ # SQLAlchemy events and append(). your implementation will
+ # never be called, unless:
+ @collection.internally_instrumented
+ def extend(self, items): ...
+
+ """
+ fn._sa_instrumented = True
+ return fn
+
+ @staticmethod
+ @util.deprecated(
+ "1.3",
+ "The :meth:`.collection.converter` handler is deprecated and will "
+ "be removed in a future release. Please refer to the "
+ ":class:`.AttributeEvents.bulk_replace` listener interface in "
+ "conjunction with the :func:`.event.listen` function.",
+ )
+ def converter(fn):
+ """Tag the method as the collection converter.
+
+ This optional method will be called when a collection is being
+ replaced entirely, as in::
+
+ myobj.acollection = [newvalue1, newvalue2]
+
+ The converter method will receive the object being assigned and should
+ return an iterable of values suitable for use by the ``appender``
+ method. A converter must not assign values or mutate the collection,
+ its sole job is to adapt the value the user provides into an iterable
+ of values for the ORM's use.
+
+ The default converter implementation will use duck-typing to do the
+ conversion. A dict-like collection will be convert into an iterable
+ of dictionary values, and other types will simply be iterated::
+
+ @collection.converter
+ def convert(self, other): ...
+
+ If the duck-typing of the object does not match the type of this
+ collection, a TypeError is raised.
+
+ Supply an implementation of this method if you want to expand the
+ range of possible types that can be assigned in bulk or perform
+ validation on the values about to be assigned.
+
+ """
+ fn._sa_instrument_role = "converter"
+ return fn
+
+ @staticmethod
+ def adds(arg):
+ """Mark the method as adding an entity to the collection.
+
+ Adds "add to collection" handling to the method. The decorator
+ argument indicates which method argument holds the SQLAlchemy-relevant
+ value. Arguments can be specified positionally (i.e. integer) or by
+ name::
+
+ @collection.adds(1)
+ def push(self, item): ...
+
+ @collection.adds('entity')
+ def do_stuff(self, thing, entity=None): ...
+
+ """
+
+ def decorator(fn):
+ fn._sa_instrument_before = ("fire_append_event", arg)
+ return fn
+
+ return decorator
+
+ @staticmethod
+ def replaces(arg):
+ """Mark the method as replacing an entity in the collection.
+
+ Adds "add to collection" and "remove from collection" handling to
+ the method. The decorator argument indicates which method argument
+ holds the SQLAlchemy-relevant value to be added, and return value, if
+ any will be considered the value to remove.
+
+ Arguments can be specified positionally (i.e. integer) or by name::
+
+ @collection.replaces(2)
+ def __setitem__(self, index, item): ...
+
+ """
+
+ def decorator(fn):
+ fn._sa_instrument_before = ("fire_append_event", arg)
+ fn._sa_instrument_after = "fire_remove_event"
+ return fn
+
+ return decorator
+
+ @staticmethod
+ def removes(arg):
+ """Mark the method as removing an entity in the collection.
+
+ Adds "remove from collection" handling to the method. The decorator
+ argument indicates which method argument holds the SQLAlchemy-relevant
+ value to be removed. Arguments can be specified positionally (i.e.
+ integer) or by name::
+
+ @collection.removes(1)
+ def zap(self, item): ...
+
+ For methods where the value to remove is not known at call-time, use
+ collection.removes_return.
+
+ """
+
+ def decorator(fn):
+ fn._sa_instrument_before = ("fire_remove_event", arg)
+ return fn
+
+ return decorator
+
+ @staticmethod
+ def removes_return():
+ """Mark the method as removing an entity in the collection.
+
+ Adds "remove from collection" handling to the method. The return
+ value of the method, if any, is considered the value to remove. The
+ method arguments are not inspected::
+
+ @collection.removes_return()
+ def pop(self): ...
+
+ For methods where the value to remove is known at call-time, use
+ collection.remove.
+
+ """
+
+ def decorator(fn):
+ fn._sa_instrument_after = "fire_remove_event"
+ return fn
+
+ return decorator
+
+
+if TYPE_CHECKING:
+
+ def collection_adapter(collection: Collection[Any]) -> CollectionAdapter:
+ """Fetch the :class:`.CollectionAdapter` for a collection."""
+
+else:
+ collection_adapter = operator.attrgetter("_sa_adapter")
+
+
+class CollectionAdapter:
+ """Bridges between the ORM and arbitrary Python collections.
+
+ Proxies base-level collection operations (append, remove, iterate)
+ to the underlying Python collection, and emits add/remove events for
+ entities entering or leaving the collection.
+
+ The ORM uses :class:`.CollectionAdapter` exclusively for interaction with
+ entity collections.
+
+
+ """
+
+ __slots__ = (
+ "attr",
+ "_key",
+ "_data",
+ "owner_state",
+ "_converter",
+ "invalidated",
+ "empty",
+ )
+
+ attr: CollectionAttributeImpl
+ _key: str
+
+ # this is actually a weakref; see note in constructor
+ _data: Callable[..., _AdaptedCollectionProtocol]
+
+ owner_state: InstanceState[Any]
+ _converter: _CollectionConverterProtocol
+ invalidated: bool
+ empty: bool
+
+ def __init__(
+ self,
+ attr: CollectionAttributeImpl,
+ owner_state: InstanceState[Any],
+ data: _AdaptedCollectionProtocol,
+ ):
+ self.attr = attr
+ self._key = attr.key
+
+ # this weakref stays referenced throughout the lifespan of
+ # CollectionAdapter. so while the weakref can return None, this
+ # is realistically only during garbage collection of this object, so
+ # we type this as a callable that returns _AdaptedCollectionProtocol
+ # in all cases.
+ self._data = weakref.ref(data) # type: ignore
+
+ self.owner_state = owner_state
+ data._sa_adapter = self
+ self._converter = data._sa_converter
+ self.invalidated = False
+ self.empty = False
+
+ def _warn_invalidated(self) -> None:
+ util.warn("This collection has been invalidated.")
+
+ @property
+ def data(self) -> _AdaptedCollectionProtocol:
+ "The entity collection being adapted."
+ return self._data()
+
+ @property
+ def _referenced_by_owner(self) -> bool:
+ """return True if the owner state still refers to this collection.
+
+ This will return False within a bulk replace operation,
+ where this collection is the one being replaced.
+
+ """
+ return self.owner_state.dict[self._key] is self._data()
+
+ def bulk_appender(self):
+ return self._data()._sa_appender
+
+ def append_with_event(
+ self, item: Any, initiator: Optional[AttributeEventToken] = None
+ ) -> None:
+ """Add an entity to the collection, firing mutation events."""
+
+ self._data()._sa_appender(item, _sa_initiator=initiator)
+
+ def _set_empty(self, user_data):
+ assert (
+ not self.empty
+ ), "This collection adapter is already in the 'empty' state"
+ self.empty = True
+ self.owner_state._empty_collections[self._key] = user_data
+
+ def _reset_empty(self) -> None:
+ assert (
+ self.empty
+ ), "This collection adapter is not in the 'empty' state"
+ self.empty = False
+ self.owner_state.dict[self._key] = (
+ self.owner_state._empty_collections.pop(self._key)
+ )
+
+ def _refuse_empty(self) -> NoReturn:
+ raise sa_exc.InvalidRequestError(
+ "This is a special 'empty' collection which cannot accommodate "
+ "internal mutation operations"
+ )
+
+ def append_without_event(self, item: Any) -> None:
+ """Add or restore an entity to the collection, firing no events."""
+
+ if self.empty:
+ self._refuse_empty()
+ self._data()._sa_appender(item, _sa_initiator=False)
+
+ def append_multiple_without_event(self, items: Iterable[Any]) -> None:
+ """Add or restore an entity to the collection, firing no events."""
+ if self.empty:
+ self._refuse_empty()
+ appender = self._data()._sa_appender
+ for item in items:
+ appender(item, _sa_initiator=False)
+
+ def bulk_remover(self):
+ return self._data()._sa_remover
+
+ def remove_with_event(
+ self, item: Any, initiator: Optional[AttributeEventToken] = None
+ ) -> None:
+ """Remove an entity from the collection, firing mutation events."""
+ self._data()._sa_remover(item, _sa_initiator=initiator)
+
+ def remove_without_event(self, item: Any) -> None:
+ """Remove an entity from the collection, firing no events."""
+ if self.empty:
+ self._refuse_empty()
+ self._data()._sa_remover(item, _sa_initiator=False)
+
+ def clear_with_event(
+ self, initiator: Optional[AttributeEventToken] = None
+ ) -> None:
+ """Empty the collection, firing a mutation event for each entity."""
+
+ if self.empty:
+ self._refuse_empty()
+ remover = self._data()._sa_remover
+ for item in list(self):
+ remover(item, _sa_initiator=initiator)
+
+ def clear_without_event(self) -> None:
+ """Empty the collection, firing no events."""
+
+ if self.empty:
+ self._refuse_empty()
+ remover = self._data()._sa_remover
+ for item in list(self):
+ remover(item, _sa_initiator=False)
+
+ def __iter__(self):
+ """Iterate over entities in the collection."""
+
+ return iter(self._data()._sa_iterator())
+
+ def __len__(self):
+ """Count entities in the collection."""
+ return len(list(self._data()._sa_iterator()))
+
+ def __bool__(self):
+ return True
+
+ def _fire_append_wo_mutation_event_bulk(
+ self, items, initiator=None, key=NO_KEY
+ ):
+ if not items:
+ return
+
+ if initiator is not False:
+ if self.invalidated:
+ self._warn_invalidated()
+
+ if self.empty:
+ self._reset_empty()
+
+ for item in items:
+ self.attr.fire_append_wo_mutation_event(
+ self.owner_state,
+ self.owner_state.dict,
+ item,
+ initiator,
+ key,
+ )
+
+ def fire_append_wo_mutation_event(self, item, initiator=None, key=NO_KEY):
+ """Notify that a entity is entering the collection but is already
+ present.
+
+
+ Initiator is a token owned by the InstrumentedAttribute that
+ initiated the membership mutation, and should be left as None
+ unless you are passing along an initiator value from a chained
+ operation.
+
+ .. versionadded:: 1.4.15
+
+ """
+ if initiator is not False:
+ if self.invalidated:
+ self._warn_invalidated()
+
+ if self.empty:
+ self._reset_empty()
+
+ return self.attr.fire_append_wo_mutation_event(
+ self.owner_state, self.owner_state.dict, item, initiator, key
+ )
+ else:
+ return item
+
+ def fire_append_event(self, item, initiator=None, key=NO_KEY):
+ """Notify that a entity has entered the collection.
+
+ Initiator is a token owned by the InstrumentedAttribute that
+ initiated the membership mutation, and should be left as None
+ unless you are passing along an initiator value from a chained
+ operation.
+
+ """
+ if initiator is not False:
+ if self.invalidated:
+ self._warn_invalidated()
+
+ if self.empty:
+ self._reset_empty()
+
+ return self.attr.fire_append_event(
+ self.owner_state, self.owner_state.dict, item, initiator, key
+ )
+ else:
+ return item
+
+ def _fire_remove_event_bulk(self, items, initiator=None, key=NO_KEY):
+ if not items:
+ return
+
+ if initiator is not False:
+ if self.invalidated:
+ self._warn_invalidated()
+
+ if self.empty:
+ self._reset_empty()
+
+ for item in items:
+ self.attr.fire_remove_event(
+ self.owner_state,
+ self.owner_state.dict,
+ item,
+ initiator,
+ key,
+ )
+
+ def fire_remove_event(self, item, initiator=None, key=NO_KEY):
+ """Notify that a entity has been removed from the collection.
+
+ Initiator is the InstrumentedAttribute that initiated the membership
+ mutation, and should be left as None unless you are passing along
+ an initiator value from a chained operation.
+
+ """
+ if initiator is not False:
+ if self.invalidated:
+ self._warn_invalidated()
+
+ if self.empty:
+ self._reset_empty()
+
+ self.attr.fire_remove_event(
+ self.owner_state, self.owner_state.dict, item, initiator, key
+ )
+
+ def fire_pre_remove_event(self, initiator=None, key=NO_KEY):
+ """Notify that an entity is about to be removed from the collection.
+
+ Only called if the entity cannot be removed after calling
+ fire_remove_event().
+
+ """
+ if self.invalidated:
+ self._warn_invalidated()
+ self.attr.fire_pre_remove_event(
+ self.owner_state,
+ self.owner_state.dict,
+ initiator=initiator,
+ key=key,
+ )
+
+ def __getstate__(self):
+ return {
+ "key": self._key,
+ "owner_state": self.owner_state,
+ "owner_cls": self.owner_state.class_,
+ "data": self.data,
+ "invalidated": self.invalidated,
+ "empty": self.empty,
+ }
+
+ def __setstate__(self, d):
+ self._key = d["key"]
+ self.owner_state = d["owner_state"]
+
+ # see note in constructor regarding this type: ignore
+ self._data = weakref.ref(d["data"]) # type: ignore
+
+ self._converter = d["data"]._sa_converter
+ d["data"]._sa_adapter = self
+ self.invalidated = d["invalidated"]
+ self.attr = getattr(d["owner_cls"], self._key).impl
+ self.empty = d.get("empty", False)
+
+
+def bulk_replace(values, existing_adapter, new_adapter, initiator=None):
+ """Load a new collection, firing events based on prior like membership.
+
+ Appends instances in ``values`` onto the ``new_adapter``. Events will be
+ fired for any instance not present in the ``existing_adapter``. Any
+ instances in ``existing_adapter`` not present in ``values`` will have
+ remove events fired upon them.
+
+ :param values: An iterable of collection member instances
+
+ :param existing_adapter: A :class:`.CollectionAdapter` of
+ instances to be replaced
+
+ :param new_adapter: An empty :class:`.CollectionAdapter`
+ to load with ``values``
+
+
+ """
+
+ assert isinstance(values, list)
+
+ idset = util.IdentitySet
+ existing_idset = idset(existing_adapter or ())
+ constants = existing_idset.intersection(values or ())
+ additions = idset(values or ()).difference(constants)
+ removals = existing_idset.difference(constants)
+
+ appender = new_adapter.bulk_appender()
+
+ for member in values or ():
+ if member in additions:
+ appender(member, _sa_initiator=initiator)
+ elif member in constants:
+ appender(member, _sa_initiator=False)
+
+ if existing_adapter:
+ existing_adapter._fire_append_wo_mutation_event_bulk(
+ constants, initiator=initiator
+ )
+ existing_adapter._fire_remove_event_bulk(removals, initiator=initiator)
+
+
+def prepare_instrumentation(
+ factory: Union[Type[Collection[Any]], _CollectionFactoryType],
+) -> _CollectionFactoryType:
+ """Prepare a callable for future use as a collection class factory.
+
+ Given a collection class factory (either a type or no-arg callable),
+ return another factory that will produce compatible instances when
+ called.
+
+ This function is responsible for converting collection_class=list
+ into the run-time behavior of collection_class=InstrumentedList.
+
+ """
+
+ impl_factory: _CollectionFactoryType
+
+ # Convert a builtin to 'Instrumented*'
+ if factory in __canned_instrumentation:
+ impl_factory = __canned_instrumentation[factory]
+ else:
+ impl_factory = cast(_CollectionFactoryType, factory)
+
+ cls: Union[_CollectionFactoryType, Type[Collection[Any]]]
+
+ # Create a specimen
+ cls = type(impl_factory())
+
+ # Did factory callable return a builtin?
+ if cls in __canned_instrumentation:
+ # if so, just convert.
+ # in previous major releases, this codepath wasn't working and was
+ # not covered by tests. prior to that it supplied a "wrapper"
+ # function that would return the class, though the rationale for this
+ # case is not known
+ impl_factory = __canned_instrumentation[cls]
+ cls = type(impl_factory())
+
+ # Instrument the class if needed.
+ if __instrumentation_mutex.acquire():
+ try:
+ if getattr(cls, "_sa_instrumented", None) != id(cls):
+ _instrument_class(cls)
+ finally:
+ __instrumentation_mutex.release()
+
+ return impl_factory
+
+
+def _instrument_class(cls):
+ """Modify methods in a class and install instrumentation."""
+
+ # In the normal call flow, a request for any of the 3 basic collection
+ # types is transformed into one of our trivial subclasses
+ # (e.g. InstrumentedList). Catch anything else that sneaks in here...
+ if cls.__module__ == "__builtin__":
+ raise sa_exc.ArgumentError(
+ "Can not instrument a built-in type. Use a "
+ "subclass, even a trivial one."
+ )
+
+ roles, methods = _locate_roles_and_methods(cls)
+
+ _setup_canned_roles(cls, roles, methods)
+
+ _assert_required_roles(cls, roles, methods)
+
+ _set_collection_attributes(cls, roles, methods)
+
+
+def _locate_roles_and_methods(cls):
+ """search for _sa_instrument_role-decorated methods in
+ method resolution order, assign to roles.
+
+ """
+
+ roles: Dict[str, str] = {}
+ methods: Dict[str, Tuple[Optional[str], Optional[int], Optional[str]]] = {}
+
+ for supercls in cls.__mro__:
+ for name, method in vars(supercls).items():
+ if not callable(method):
+ continue
+
+ # note role declarations
+ if hasattr(method, "_sa_instrument_role"):
+ role = method._sa_instrument_role
+ assert role in (
+ "appender",
+ "remover",
+ "iterator",
+ "converter",
+ )
+ roles.setdefault(role, name)
+
+ # transfer instrumentation requests from decorated function
+ # to the combined queue
+ before: Optional[Tuple[str, int]] = None
+ after: Optional[str] = None
+
+ if hasattr(method, "_sa_instrument_before"):
+ op, argument = method._sa_instrument_before
+ assert op in ("fire_append_event", "fire_remove_event")
+ before = op, argument
+ if hasattr(method, "_sa_instrument_after"):
+ op = method._sa_instrument_after
+ assert op in ("fire_append_event", "fire_remove_event")
+ after = op
+ if before:
+ methods[name] = before + (after,)
+ elif after:
+ methods[name] = None, None, after
+ return roles, methods
+
+
+def _setup_canned_roles(cls, roles, methods):
+ """see if this class has "canned" roles based on a known
+ collection type (dict, set, list). Apply those roles
+ as needed to the "roles" dictionary, and also
+ prepare "decorator" methods
+
+ """
+ collection_type = util.duck_type_collection(cls)
+ if collection_type in __interfaces:
+ assert collection_type is not None
+ canned_roles, decorators = __interfaces[collection_type]
+ for role, name in canned_roles.items():
+ roles.setdefault(role, name)
+
+ # apply ABC auto-decoration to methods that need it
+ for method, decorator in decorators.items():
+ fn = getattr(cls, method, None)
+ if (
+ fn
+ and method not in methods
+ and not hasattr(fn, "_sa_instrumented")
+ ):
+ setattr(cls, method, decorator(fn))
+
+
+def _assert_required_roles(cls, roles, methods):
+ """ensure all roles are present, and apply implicit instrumentation if
+ needed
+
+ """
+ if "appender" not in roles or not hasattr(cls, roles["appender"]):
+ raise sa_exc.ArgumentError(
+ "Type %s must elect an appender method to be "
+ "a collection class" % cls.__name__
+ )
+ elif roles["appender"] not in methods and not hasattr(
+ getattr(cls, roles["appender"]), "_sa_instrumented"
+ ):
+ methods[roles["appender"]] = ("fire_append_event", 1, None)
+
+ if "remover" not in roles or not hasattr(cls, roles["remover"]):
+ raise sa_exc.ArgumentError(
+ "Type %s must elect a remover method to be "
+ "a collection class" % cls.__name__
+ )
+ elif roles["remover"] not in methods and not hasattr(
+ getattr(cls, roles["remover"]), "_sa_instrumented"
+ ):
+ methods[roles["remover"]] = ("fire_remove_event", 1, None)
+
+ if "iterator" not in roles or not hasattr(cls, roles["iterator"]):
+ raise sa_exc.ArgumentError(
+ "Type %s must elect an iterator method to be "
+ "a collection class" % cls.__name__
+ )
+
+
+def _set_collection_attributes(cls, roles, methods):
+ """apply ad-hoc instrumentation from decorators, class-level defaults
+ and implicit role declarations
+
+ """
+ for method_name, (before, argument, after) in methods.items():
+ setattr(
+ cls,
+ method_name,
+ _instrument_membership_mutator(
+ getattr(cls, method_name), before, argument, after
+ ),
+ )
+ # intern the role map
+ for role, method_name in roles.items():
+ setattr(cls, "_sa_%s" % role, getattr(cls, method_name))
+
+ cls._sa_adapter = None
+
+ if not hasattr(cls, "_sa_converter"):
+ cls._sa_converter = None
+ cls._sa_instrumented = id(cls)
+
+
+def _instrument_membership_mutator(method, before, argument, after):
+ """Route method args and/or return value through the collection
+ adapter."""
+ # This isn't smart enough to handle @adds(1) for 'def fn(self, (a, b))'
+ if before:
+ fn_args = list(
+ util.flatten_iterator(inspect_getfullargspec(method)[0])
+ )
+ if isinstance(argument, int):
+ pos_arg = argument
+ named_arg = len(fn_args) > argument and fn_args[argument] or None
+ else:
+ if argument in fn_args:
+ pos_arg = fn_args.index(argument)
+ else:
+ pos_arg = None
+ named_arg = argument
+ del fn_args
+
+ def wrapper(*args, **kw):
+ if before:
+ if pos_arg is None:
+ if named_arg not in kw:
+ raise sa_exc.ArgumentError(
+ "Missing argument %s" % argument
+ )
+ value = kw[named_arg]
+ else:
+ if len(args) > pos_arg:
+ value = args[pos_arg]
+ elif named_arg in kw:
+ value = kw[named_arg]
+ else:
+ raise sa_exc.ArgumentError(
+ "Missing argument %s" % argument
+ )
+
+ initiator = kw.pop("_sa_initiator", None)
+ if initiator is False:
+ executor = None
+ else:
+ executor = args[0]._sa_adapter
+
+ if before and executor:
+ getattr(executor, before)(value, initiator)
+
+ if not after or not executor:
+ return method(*args, **kw)
+ else:
+ res = method(*args, **kw)
+ if res is not None:
+ getattr(executor, after)(res, initiator)
+ return res
+
+ wrapper._sa_instrumented = True # type: ignore[attr-defined]
+ if hasattr(method, "_sa_instrument_role"):
+ wrapper._sa_instrument_role = method._sa_instrument_role # type: ignore[attr-defined] # noqa: E501
+ wrapper.__name__ = method.__name__
+ wrapper.__doc__ = method.__doc__
+ return wrapper
+
+
+def __set_wo_mutation(collection, item, _sa_initiator=None):
+ """Run set wo mutation events.
+
+ The collection is not mutated.
+
+ """
+ if _sa_initiator is not False:
+ executor = collection._sa_adapter
+ if executor:
+ executor.fire_append_wo_mutation_event(
+ item, _sa_initiator, key=None
+ )
+
+
+def __set(collection, item, _sa_initiator, key):
+ """Run set events.
+
+ This event always occurs before the collection is actually mutated.
+
+ """
+
+ if _sa_initiator is not False:
+ executor = collection._sa_adapter
+ if executor:
+ item = executor.fire_append_event(item, _sa_initiator, key=key)
+ return item
+
+
+def __del(collection, item, _sa_initiator, key):
+ """Run del events.
+
+ This event occurs before the collection is actually mutated, *except*
+ in the case of a pop operation, in which case it occurs afterwards.
+ For pop operations, the __before_pop hook is called before the
+ operation occurs.
+
+ """
+ if _sa_initiator is not False:
+ executor = collection._sa_adapter
+ if executor:
+ executor.fire_remove_event(item, _sa_initiator, key=key)
+
+
+def __before_pop(collection, _sa_initiator=None):
+ """An event which occurs on a before a pop() operation occurs."""
+ executor = collection._sa_adapter
+ if executor:
+ executor.fire_pre_remove_event(_sa_initiator)
+
+
+def _list_decorators() -> Dict[str, Callable[[_FN], _FN]]:
+ """Tailored instrumentation wrappers for any list-like class."""
+
+ def _tidy(fn):
+ fn._sa_instrumented = True
+ fn.__doc__ = getattr(list, fn.__name__).__doc__
+
+ def append(fn):
+ def append(self, item, _sa_initiator=None):
+ item = __set(self, item, _sa_initiator, NO_KEY)
+ fn(self, item)
+
+ _tidy(append)
+ return append
+
+ def remove(fn):
+ def remove(self, value, _sa_initiator=None):
+ __del(self, value, _sa_initiator, NO_KEY)
+ # testlib.pragma exempt:__eq__
+ fn(self, value)
+
+ _tidy(remove)
+ return remove
+
+ def insert(fn):
+ def insert(self, index, value):
+ value = __set(self, value, None, index)
+ fn(self, index, value)
+
+ _tidy(insert)
+ return insert
+
+ def __setitem__(fn):
+ def __setitem__(self, index, value):
+ if not isinstance(index, slice):
+ existing = self[index]
+ if existing is not None:
+ __del(self, existing, None, index)
+ value = __set(self, value, None, index)
+ fn(self, index, value)
+ else:
+ # slice assignment requires __delitem__, insert, __len__
+ step = index.step or 1
+ start = index.start or 0
+ if start < 0:
+ start += len(self)
+ if index.stop is not None:
+ stop = index.stop
+ else:
+ stop = len(self)
+ if stop < 0:
+ stop += len(self)
+
+ if step == 1:
+ if value is self:
+ return
+ for i in range(start, stop, step):
+ if len(self) > start:
+ del self[start]
+
+ for i, item in enumerate(value):
+ self.insert(i + start, item)
+ else:
+ rng = list(range(start, stop, step))
+ if len(value) != len(rng):
+ raise ValueError(
+ "attempt to assign sequence of size %s to "
+ "extended slice of size %s"
+ % (len(value), len(rng))
+ )
+ for i, item in zip(rng, value):
+ self.__setitem__(i, item)
+
+ _tidy(__setitem__)
+ return __setitem__
+
+ def __delitem__(fn):
+ def __delitem__(self, index):
+ if not isinstance(index, slice):
+ item = self[index]
+ __del(self, item, None, index)
+ fn(self, index)
+ else:
+ # slice deletion requires __getslice__ and a slice-groking
+ # __getitem__ for stepped deletion
+ # note: not breaking this into atomic dels
+ for item in self[index]:
+ __del(self, item, None, index)
+ fn(self, index)
+
+ _tidy(__delitem__)
+ return __delitem__
+
+ def extend(fn):
+ def extend(self, iterable):
+ for value in list(iterable):
+ self.append(value)
+
+ _tidy(extend)
+ return extend
+
+ def __iadd__(fn):
+ def __iadd__(self, iterable):
+ # list.__iadd__ takes any iterable and seems to let TypeError
+ # raise as-is instead of returning NotImplemented
+ for value in list(iterable):
+ self.append(value)
+ return self
+
+ _tidy(__iadd__)
+ return __iadd__
+
+ def pop(fn):
+ def pop(self, index=-1):
+ __before_pop(self)
+ item = fn(self, index)
+ __del(self, item, None, index)
+ return item
+
+ _tidy(pop)
+ return pop
+
+ def clear(fn):
+ def clear(self, index=-1):
+ for item in self:
+ __del(self, item, None, index)
+ fn(self)
+
+ _tidy(clear)
+ return clear
+
+ # __imul__ : not wrapping this. all members of the collection are already
+ # present, so no need to fire appends... wrapping it with an explicit
+ # decorator is still possible, so events on *= can be had if they're
+ # desired. hard to imagine a use case for __imul__, though.
+
+ l = locals().copy()
+ l.pop("_tidy")
+ return l
+
+
+def _dict_decorators() -> Dict[str, Callable[[_FN], _FN]]:
+ """Tailored instrumentation wrappers for any dict-like mapping class."""
+
+ def _tidy(fn):
+ fn._sa_instrumented = True
+ fn.__doc__ = getattr(dict, fn.__name__).__doc__
+
+ def __setitem__(fn):
+ def __setitem__(self, key, value, _sa_initiator=None):
+ if key in self:
+ __del(self, self[key], _sa_initiator, key)
+ value = __set(self, value, _sa_initiator, key)
+ fn(self, key, value)
+
+ _tidy(__setitem__)
+ return __setitem__
+
+ def __delitem__(fn):
+ def __delitem__(self, key, _sa_initiator=None):
+ if key in self:
+ __del(self, self[key], _sa_initiator, key)
+ fn(self, key)
+
+ _tidy(__delitem__)
+ return __delitem__
+
+ def clear(fn):
+ def clear(self):
+ for key in self:
+ __del(self, self[key], None, key)
+ fn(self)
+
+ _tidy(clear)
+ return clear
+
+ def pop(fn):
+ def pop(self, key, default=NO_ARG):
+ __before_pop(self)
+ _to_del = key in self
+ if default is NO_ARG:
+ item = fn(self, key)
+ else:
+ item = fn(self, key, default)
+ if _to_del:
+ __del(self, item, None, key)
+ return item
+
+ _tidy(pop)
+ return pop
+
+ def popitem(fn):
+ def popitem(self):
+ __before_pop(self)
+ item = fn(self)
+ __del(self, item[1], None, 1)
+ return item
+
+ _tidy(popitem)
+ return popitem
+
+ def setdefault(fn):
+ def setdefault(self, key, default=None):
+ if key not in self:
+ self.__setitem__(key, default)
+ return default
+ else:
+ value = self.__getitem__(key)
+ if value is default:
+ __set_wo_mutation(self, value, None)
+
+ return value
+
+ _tidy(setdefault)
+ return setdefault
+
+ def update(fn):
+ def update(self, __other=NO_ARG, **kw):
+ if __other is not NO_ARG:
+ if hasattr(__other, "keys"):
+ for key in list(__other):
+ if key not in self or self[key] is not __other[key]:
+ self[key] = __other[key]
+ else:
+ __set_wo_mutation(self, __other[key], None)
+ else:
+ for key, value in __other:
+ if key not in self or self[key] is not value:
+ self[key] = value
+ else:
+ __set_wo_mutation(self, value, None)
+ for key in kw:
+ if key not in self or self[key] is not kw[key]:
+ self[key] = kw[key]
+ else:
+ __set_wo_mutation(self, kw[key], None)
+
+ _tidy(update)
+ return update
+
+ l = locals().copy()
+ l.pop("_tidy")
+ return l
+
+
+_set_binop_bases = (set, frozenset)
+
+
+def _set_binops_check_strict(self: Any, obj: Any) -> bool:
+ """Allow only set, frozenset and self.__class__-derived
+ objects in binops."""
+ return isinstance(obj, _set_binop_bases + (self.__class__,))
+
+
+def _set_binops_check_loose(self: Any, obj: Any) -> bool:
+ """Allow anything set-like to participate in set binops."""
+ return (
+ isinstance(obj, _set_binop_bases + (self.__class__,))
+ or util.duck_type_collection(obj) == set
+ )
+
+
+def _set_decorators() -> Dict[str, Callable[[_FN], _FN]]:
+ """Tailored instrumentation wrappers for any set-like class."""
+
+ def _tidy(fn):
+ fn._sa_instrumented = True
+ fn.__doc__ = getattr(set, fn.__name__).__doc__
+
+ def add(fn):
+ def add(self, value, _sa_initiator=None):
+ if value not in self:
+ value = __set(self, value, _sa_initiator, NO_KEY)
+ else:
+ __set_wo_mutation(self, value, _sa_initiator)
+ # testlib.pragma exempt:__hash__
+ fn(self, value)
+
+ _tidy(add)
+ return add
+
+ def discard(fn):
+ def discard(self, value, _sa_initiator=None):
+ # testlib.pragma exempt:__hash__
+ if value in self:
+ __del(self, value, _sa_initiator, NO_KEY)
+ # testlib.pragma exempt:__hash__
+ fn(self, value)
+
+ _tidy(discard)
+ return discard
+
+ def remove(fn):
+ def remove(self, value, _sa_initiator=None):
+ # testlib.pragma exempt:__hash__
+ if value in self:
+ __del(self, value, _sa_initiator, NO_KEY)
+ # testlib.pragma exempt:__hash__
+ fn(self, value)
+
+ _tidy(remove)
+ return remove
+
+ def pop(fn):
+ def pop(self):
+ __before_pop(self)
+ item = fn(self)
+ # for set in particular, we have no way to access the item
+ # that will be popped before pop is called.
+ __del(self, item, None, NO_KEY)
+ return item
+
+ _tidy(pop)
+ return pop
+
+ def clear(fn):
+ def clear(self):
+ for item in list(self):
+ self.remove(item)
+
+ _tidy(clear)
+ return clear
+
+ def update(fn):
+ def update(self, value):
+ for item in value:
+ self.add(item)
+
+ _tidy(update)
+ return update
+
+ def __ior__(fn):
+ def __ior__(self, value):
+ if not _set_binops_check_strict(self, value):
+ return NotImplemented
+ for item in value:
+ self.add(item)
+ return self
+
+ _tidy(__ior__)
+ return __ior__
+
+ def difference_update(fn):
+ def difference_update(self, value):
+ for item in value:
+ self.discard(item)
+
+ _tidy(difference_update)
+ return difference_update
+
+ def __isub__(fn):
+ def __isub__(self, value):
+ if not _set_binops_check_strict(self, value):
+ return NotImplemented
+ for item in value:
+ self.discard(item)
+ return self
+
+ _tidy(__isub__)
+ return __isub__
+
+ def intersection_update(fn):
+ def intersection_update(self, other):
+ want, have = self.intersection(other), set(self)
+ remove, add = have - want, want - have
+
+ for item in remove:
+ self.remove(item)
+ for item in add:
+ self.add(item)
+
+ _tidy(intersection_update)
+ return intersection_update
+
+ def __iand__(fn):
+ def __iand__(self, other):
+ if not _set_binops_check_strict(self, other):
+ return NotImplemented
+ want, have = self.intersection(other), set(self)
+ remove, add = have - want, want - have
+
+ for item in remove:
+ self.remove(item)
+ for item in add:
+ self.add(item)
+ return self
+
+ _tidy(__iand__)
+ return __iand__
+
+ def symmetric_difference_update(fn):
+ def symmetric_difference_update(self, other):
+ want, have = self.symmetric_difference(other), set(self)
+ remove, add = have - want, want - have
+
+ for item in remove:
+ self.remove(item)
+ for item in add:
+ self.add(item)
+
+ _tidy(symmetric_difference_update)
+ return symmetric_difference_update
+
+ def __ixor__(fn):
+ def __ixor__(self, other):
+ if not _set_binops_check_strict(self, other):
+ return NotImplemented
+ want, have = self.symmetric_difference(other), set(self)
+ remove, add = have - want, want - have
+
+ for item in remove:
+ self.remove(item)
+ for item in add:
+ self.add(item)
+ return self
+
+ _tidy(__ixor__)
+ return __ixor__
+
+ l = locals().copy()
+ l.pop("_tidy")
+ return l
+
+
+class InstrumentedList(List[_T]):
+ """An instrumented version of the built-in list."""
+
+
+class InstrumentedSet(Set[_T]):
+ """An instrumented version of the built-in set."""
+
+
+class InstrumentedDict(Dict[_KT, _VT]):
+ """An instrumented version of the built-in dict."""
+
+
+__canned_instrumentation: util.immutabledict[Any, _CollectionFactoryType] = (
+ util.immutabledict(
+ {
+ list: InstrumentedList,
+ set: InstrumentedSet,
+ dict: InstrumentedDict,
+ }
+ )
+)
+
+__interfaces: util.immutabledict[
+ Any,
+ Tuple[
+ Dict[str, str],
+ Dict[str, Callable[..., Any]],
+ ],
+] = util.immutabledict(
+ {
+ list: (
+ {
+ "appender": "append",
+ "remover": "remove",
+ "iterator": "__iter__",
+ },
+ _list_decorators(),
+ ),
+ set: (
+ {"appender": "add", "remover": "remove", "iterator": "__iter__"},
+ _set_decorators(),
+ ),
+ # decorators are required for dicts and object collections.
+ dict: ({"iterator": "values"}, _dict_decorators()),
+ }
+)
+
+
+def __go(lcls):
+ global keyfunc_mapping, mapped_collection
+ global column_keyed_dict, column_mapped_collection
+ global MappedCollection, KeyFuncDict
+ global attribute_keyed_dict, attribute_mapped_collection
+
+ from .mapped_collection import keyfunc_mapping
+ from .mapped_collection import column_keyed_dict
+ from .mapped_collection import attribute_keyed_dict
+ from .mapped_collection import KeyFuncDict
+
+ from .mapped_collection import mapped_collection
+ from .mapped_collection import column_mapped_collection
+ from .mapped_collection import attribute_mapped_collection
+ from .mapped_collection import MappedCollection
+
+ # ensure instrumentation is associated with
+ # these built-in classes; if a user-defined class
+ # subclasses these and uses @internally_instrumented,
+ # the superclass is otherwise not instrumented.
+ # see [ticket:2406].
+ _instrument_class(InstrumentedList)
+ _instrument_class(InstrumentedSet)
+ _instrument_class(KeyFuncDict)
+
+
+__go(locals())
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/context.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/context.py
new file mode 100644
index 0000000..3056016
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/context.py
@@ -0,0 +1,3243 @@
+# orm/context.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: ignore-errors
+
+from __future__ import annotations
+
+import itertools
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import attributes
+from . import interfaces
+from . import loading
+from .base import _is_aliased_class
+from .interfaces import ORMColumnDescription
+from .interfaces import ORMColumnsClauseRole
+from .path_registry import PathRegistry
+from .util import _entity_corresponds_to
+from .util import _ORMJoin
+from .util import _TraceAdaptRole
+from .util import AliasedClass
+from .util import Bundle
+from .util import ORMAdapter
+from .util import ORMStatementAdapter
+from .. import exc as sa_exc
+from .. import future
+from .. import inspect
+from .. import sql
+from .. import util
+from ..sql import coercions
+from ..sql import expression
+from ..sql import roles
+from ..sql import util as sql_util
+from ..sql import visitors
+from ..sql._typing import _TP
+from ..sql._typing import is_dml
+from ..sql._typing import is_insert_update
+from ..sql._typing import is_select_base
+from ..sql.base import _select_iterables
+from ..sql.base import CacheableOptions
+from ..sql.base import CompileState
+from ..sql.base import Executable
+from ..sql.base import Generative
+from ..sql.base import Options
+from ..sql.dml import UpdateBase
+from ..sql.elements import GroupedElement
+from ..sql.elements import TextClause
+from ..sql.selectable import CompoundSelectState
+from ..sql.selectable import LABEL_STYLE_DISAMBIGUATE_ONLY
+from ..sql.selectable import LABEL_STYLE_NONE
+from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
+from ..sql.selectable import Select
+from ..sql.selectable import SelectLabelStyle
+from ..sql.selectable import SelectState
+from ..sql.selectable import TypedReturnsRows
+from ..sql.visitors import InternalTraversal
+
+if TYPE_CHECKING:
+ from ._typing import _InternalEntityType
+ from ._typing import OrmExecuteOptionsParameter
+ from .loading import PostLoad
+ from .mapper import Mapper
+ from .query import Query
+ from .session import _BindArguments
+ from .session import Session
+ from ..engine import Result
+ from ..engine.interfaces import _CoreSingleExecuteParams
+ from ..sql._typing import _ColumnsClauseArgument
+ from ..sql.compiler import SQLCompiler
+ from ..sql.dml import _DMLTableElement
+ from ..sql.elements import ColumnElement
+ from ..sql.selectable import _JoinTargetElement
+ from ..sql.selectable import _LabelConventionCallable
+ from ..sql.selectable import _SetupJoinsElement
+ from ..sql.selectable import ExecutableReturnsRows
+ from ..sql.selectable import SelectBase
+ from ..sql.type_api import TypeEngine
+
+_T = TypeVar("_T", bound=Any)
+_path_registry = PathRegistry.root
+
+_EMPTY_DICT = util.immutabledict()
+
+
+LABEL_STYLE_LEGACY_ORM = SelectLabelStyle.LABEL_STYLE_LEGACY_ORM
+
+
+class QueryContext:
+ __slots__ = (
+ "top_level_context",
+ "compile_state",
+ "query",
+ "params",
+ "load_options",
+ "bind_arguments",
+ "execution_options",
+ "session",
+ "autoflush",
+ "populate_existing",
+ "invoke_all_eagers",
+ "version_check",
+ "refresh_state",
+ "create_eager_joins",
+ "propagated_loader_options",
+ "attributes",
+ "runid",
+ "partials",
+ "post_load_paths",
+ "identity_token",
+ "yield_per",
+ "loaders_require_buffering",
+ "loaders_require_uniquing",
+ )
+
+ runid: int
+ post_load_paths: Dict[PathRegistry, PostLoad]
+ compile_state: ORMCompileState
+
+ class default_load_options(Options):
+ _only_return_tuples = False
+ _populate_existing = False
+ _version_check = False
+ _invoke_all_eagers = True
+ _autoflush = True
+ _identity_token = None
+ _yield_per = None
+ _refresh_state = None
+ _lazy_loaded_from = None
+ _legacy_uniquing = False
+ _sa_top_level_orm_context = None
+ _is_user_refresh = False
+
+ def __init__(
+ self,
+ compile_state: CompileState,
+ statement: Union[Select[Any], FromStatement[Any]],
+ params: _CoreSingleExecuteParams,
+ session: Session,
+ load_options: Union[
+ Type[QueryContext.default_load_options],
+ QueryContext.default_load_options,
+ ],
+ execution_options: Optional[OrmExecuteOptionsParameter] = None,
+ bind_arguments: Optional[_BindArguments] = None,
+ ):
+ self.load_options = load_options
+ self.execution_options = execution_options or _EMPTY_DICT
+ self.bind_arguments = bind_arguments or _EMPTY_DICT
+ self.compile_state = compile_state
+ self.query = statement
+ self.session = session
+ self.loaders_require_buffering = False
+ self.loaders_require_uniquing = False
+ self.params = params
+ self.top_level_context = load_options._sa_top_level_orm_context
+
+ cached_options = compile_state.select_statement._with_options
+ uncached_options = statement._with_options
+
+ # see issue #7447 , #8399 for some background
+ # propagated loader options will be present on loaded InstanceState
+ # objects under state.load_options and are typically used by
+ # LazyLoader to apply options to the SELECT statement it emits.
+ # For compile state options (i.e. loader strategy options), these
+ # need to line up with the ".load_path" attribute which in
+ # loader.py is pulled from context.compile_state.current_path.
+ # so, this means these options have to be the ones from the
+ # *cached* statement that's travelling with compile_state, not the
+ # *current* statement which won't match up for an ad-hoc
+ # AliasedClass
+ self.propagated_loader_options = tuple(
+ opt._adapt_cached_option_to_uncached_option(self, uncached_opt)
+ for opt, uncached_opt in zip(cached_options, uncached_options)
+ if opt.propagate_to_loaders
+ )
+
+ self.attributes = dict(compile_state.attributes)
+
+ self.autoflush = load_options._autoflush
+ self.populate_existing = load_options._populate_existing
+ self.invoke_all_eagers = load_options._invoke_all_eagers
+ self.version_check = load_options._version_check
+ self.refresh_state = load_options._refresh_state
+ self.yield_per = load_options._yield_per
+ self.identity_token = load_options._identity_token
+
+ def _get_top_level_context(self) -> QueryContext:
+ return self.top_level_context or self
+
+
+_orm_load_exec_options = util.immutabledict(
+ {"_result_disable_adapt_to_context": True}
+)
+
+
+class AbstractORMCompileState(CompileState):
+ is_dml_returning = False
+
+ def _init_global_attributes(
+ self, statement, compiler, *, toplevel, process_criteria_for_toplevel
+ ):
+ self.attributes = {}
+
+ if compiler is None:
+ # this is the legacy / testing only ORM _compile_state() use case.
+ # there is no need to apply criteria options for this.
+ self.global_attributes = ga = {}
+ assert toplevel
+ return
+ else:
+ self.global_attributes = ga = compiler._global_attributes
+
+ if toplevel:
+ ga["toplevel_orm"] = True
+
+ if process_criteria_for_toplevel:
+ for opt in statement._with_options:
+ if opt._is_criteria_option:
+ opt.process_compile_state(self)
+
+ return
+ elif ga.get("toplevel_orm", False):
+ return
+
+ stack_0 = compiler.stack[0]
+
+ try:
+ toplevel_stmt = stack_0["selectable"]
+ except KeyError:
+ pass
+ else:
+ for opt in toplevel_stmt._with_options:
+ if opt._is_compile_state and opt._is_criteria_option:
+ opt.process_compile_state(self)
+
+ ga["toplevel_orm"] = True
+
+ @classmethod
+ def create_for_statement(
+ cls,
+ statement: Union[Select, FromStatement],
+ compiler: Optional[SQLCompiler],
+ **kw: Any,
+ ) -> AbstractORMCompileState:
+ """Create a context for a statement given a :class:`.Compiler`.
+
+ This method is always invoked in the context of SQLCompiler.process().
+
+ For a Select object, this would be invoked from
+ SQLCompiler.visit_select(). For the special FromStatement object used
+ by Query to indicate "Query.from_statement()", this is called by
+ FromStatement._compiler_dispatch() that would be called by
+ SQLCompiler.process().
+ """
+ return super().create_for_statement(statement, compiler, **kw)
+
+ @classmethod
+ def orm_pre_session_exec(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ is_pre_event,
+ ):
+ raise NotImplementedError()
+
+ @classmethod
+ def orm_execute_statement(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ conn,
+ ) -> Result:
+ result = conn.execute(
+ statement, params or {}, execution_options=execution_options
+ )
+ return cls.orm_setup_cursor_result(
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ result,
+ )
+
+ @classmethod
+ def orm_setup_cursor_result(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ result,
+ ):
+ raise NotImplementedError()
+
+
+class AutoflushOnlyORMCompileState(AbstractORMCompileState):
+ """ORM compile state that is a passthrough, except for autoflush."""
+
+ @classmethod
+ def orm_pre_session_exec(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ is_pre_event,
+ ):
+ # consume result-level load_options. These may have been set up
+ # in an ORMExecuteState hook
+ (
+ load_options,
+ execution_options,
+ ) = QueryContext.default_load_options.from_execution_options(
+ "_sa_orm_load_options",
+ {
+ "autoflush",
+ },
+ execution_options,
+ statement._execution_options,
+ )
+
+ if not is_pre_event and load_options._autoflush:
+ session._autoflush()
+
+ return statement, execution_options
+
+ @classmethod
+ def orm_setup_cursor_result(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ result,
+ ):
+ return result
+
+
+class ORMCompileState(AbstractORMCompileState):
+ class default_compile_options(CacheableOptions):
+ _cache_key_traversal = [
+ ("_use_legacy_query_style", InternalTraversal.dp_boolean),
+ ("_for_statement", InternalTraversal.dp_boolean),
+ ("_bake_ok", InternalTraversal.dp_boolean),
+ ("_current_path", InternalTraversal.dp_has_cache_key),
+ ("_enable_single_crit", InternalTraversal.dp_boolean),
+ ("_enable_eagerloads", InternalTraversal.dp_boolean),
+ ("_only_load_props", InternalTraversal.dp_plain_obj),
+ ("_set_base_alias", InternalTraversal.dp_boolean),
+ ("_for_refresh_state", InternalTraversal.dp_boolean),
+ ("_render_for_subquery", InternalTraversal.dp_boolean),
+ ("_is_star", InternalTraversal.dp_boolean),
+ ]
+
+ # set to True by default from Query._statement_20(), to indicate
+ # the rendered query should look like a legacy ORM query. right
+ # now this basically indicates we should use tablename_columnname
+ # style labels. Generally indicates the statement originated
+ # from a Query object.
+ _use_legacy_query_style = False
+
+ # set *only* when we are coming from the Query.statement
+ # accessor, or a Query-level equivalent such as
+ # query.subquery(). this supersedes "toplevel".
+ _for_statement = False
+
+ _bake_ok = True
+ _current_path = _path_registry
+ _enable_single_crit = True
+ _enable_eagerloads = True
+ _only_load_props = None
+ _set_base_alias = False
+ _for_refresh_state = False
+ _render_for_subquery = False
+ _is_star = False
+
+ attributes: Dict[Any, Any]
+ global_attributes: Dict[Any, Any]
+
+ statement: Union[Select[Any], FromStatement[Any]]
+ select_statement: Union[Select[Any], FromStatement[Any]]
+ _entities: List[_QueryEntity]
+ _polymorphic_adapters: Dict[_InternalEntityType, ORMAdapter]
+ compile_options: Union[
+ Type[default_compile_options], default_compile_options
+ ]
+ _primary_entity: Optional[_QueryEntity]
+ use_legacy_query_style: bool
+ _label_convention: _LabelConventionCallable
+ primary_columns: List[ColumnElement[Any]]
+ secondary_columns: List[ColumnElement[Any]]
+ dedupe_columns: Set[ColumnElement[Any]]
+ create_eager_joins: List[
+ # TODO: this structure is set up by JoinedLoader
+ Tuple[Any, ...]
+ ]
+ current_path: PathRegistry = _path_registry
+ _has_mapper_entities = False
+
+ def __init__(self, *arg, **kw):
+ raise NotImplementedError()
+
+ if TYPE_CHECKING:
+
+ @classmethod
+ def create_for_statement(
+ cls,
+ statement: Union[Select, FromStatement],
+ compiler: Optional[SQLCompiler],
+ **kw: Any,
+ ) -> ORMCompileState: ...
+
+ def _append_dedupe_col_collection(self, obj, col_collection):
+ dedupe = self.dedupe_columns
+ if obj not in dedupe:
+ dedupe.add(obj)
+ col_collection.append(obj)
+
+ @classmethod
+ def _column_naming_convention(
+ cls, label_style: SelectLabelStyle, legacy: bool
+ ) -> _LabelConventionCallable:
+ if legacy:
+
+ def name(col, col_name=None):
+ if col_name:
+ return col_name
+ else:
+ return getattr(col, "key")
+
+ return name
+ else:
+ return SelectState._column_naming_convention(label_style)
+
+ @classmethod
+ def get_column_descriptions(cls, statement):
+ return _column_descriptions(statement)
+
+ @classmethod
+ def orm_pre_session_exec(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ is_pre_event,
+ ):
+ # consume result-level load_options. These may have been set up
+ # in an ORMExecuteState hook
+ (
+ load_options,
+ execution_options,
+ ) = QueryContext.default_load_options.from_execution_options(
+ "_sa_orm_load_options",
+ {
+ "populate_existing",
+ "autoflush",
+ "yield_per",
+ "identity_token",
+ "sa_top_level_orm_context",
+ },
+ execution_options,
+ statement._execution_options,
+ )
+
+ # default execution options for ORM results:
+ # 1. _result_disable_adapt_to_context=True
+ # this will disable the ResultSetMetadata._adapt_to_context()
+ # step which we don't need, as we have result processors cached
+ # against the original SELECT statement before caching.
+
+ if "sa_top_level_orm_context" in execution_options:
+ ctx = execution_options["sa_top_level_orm_context"]
+ execution_options = ctx.query._execution_options.merge_with(
+ ctx.execution_options, execution_options
+ )
+
+ if not execution_options:
+ execution_options = _orm_load_exec_options
+ else:
+ execution_options = execution_options.union(_orm_load_exec_options)
+
+ # would have been placed here by legacy Query only
+ if load_options._yield_per:
+ execution_options = execution_options.union(
+ {"yield_per": load_options._yield_per}
+ )
+
+ if (
+ getattr(statement._compile_options, "_current_path", None)
+ and len(statement._compile_options._current_path) > 10
+ and execution_options.get("compiled_cache", True) is not None
+ ):
+ execution_options: util.immutabledict[str, Any] = (
+ execution_options.union(
+ {
+ "compiled_cache": None,
+ "_cache_disable_reason": "excess depth for "
+ "ORM loader options",
+ }
+ )
+ )
+
+ bind_arguments["clause"] = statement
+
+ # new in 1.4 - the coercions system is leveraged to allow the
+ # "subject" mapper of a statement be propagated to the top
+ # as the statement is built. "subject" mapper is the generally
+ # standard object used as an identifier for multi-database schemes.
+
+ # we are here based on the fact that _propagate_attrs contains
+ # "compile_state_plugin": "orm". The "plugin_subject"
+ # needs to be present as well.
+
+ try:
+ plugin_subject = statement._propagate_attrs["plugin_subject"]
+ except KeyError:
+ assert False, "statement had 'orm' plugin but no plugin_subject"
+ else:
+ if plugin_subject:
+ bind_arguments["mapper"] = plugin_subject.mapper
+
+ if not is_pre_event and load_options._autoflush:
+ session._autoflush()
+
+ return statement, execution_options
+
+ @classmethod
+ def orm_setup_cursor_result(
+ cls,
+ session,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ result,
+ ):
+ execution_context = result.context
+ compile_state = execution_context.compiled.compile_state
+
+ # cover edge case where ORM entities used in legacy select
+ # were passed to session.execute:
+ # session.execute(legacy_select([User.id, User.name]))
+ # see test_query->test_legacy_tuple_old_select
+
+ load_options = execution_options.get(
+ "_sa_orm_load_options", QueryContext.default_load_options
+ )
+
+ if compile_state.compile_options._is_star:
+ return result
+
+ querycontext = QueryContext(
+ compile_state,
+ statement,
+ params,
+ session,
+ load_options,
+ execution_options,
+ bind_arguments,
+ )
+ return loading.instances(result, querycontext)
+
+ @property
+ def _lead_mapper_entities(self):
+ """return all _MapperEntity objects in the lead entities collection.
+
+ Does **not** include entities that have been replaced by
+ with_entities(), with_only_columns()
+
+ """
+ return [
+ ent for ent in self._entities if isinstance(ent, _MapperEntity)
+ ]
+
+ def _create_with_polymorphic_adapter(self, ext_info, selectable):
+ """given MapperEntity or ORMColumnEntity, setup polymorphic loading
+ if called for by the Mapper.
+
+ As of #8168 in 2.0.0rc1, polymorphic adapters, which greatly increase
+ the complexity of the query creation process, are not used at all
+ except in the quasi-legacy cases of with_polymorphic referring to an
+ alias and/or subquery. This would apply to concrete polymorphic
+ loading, and joined inheritance where a subquery is
+ passed to with_polymorphic (which is completely unnecessary in modern
+ use).
+
+ """
+ if (
+ not ext_info.is_aliased_class
+ and ext_info.mapper.persist_selectable
+ not in self._polymorphic_adapters
+ ):
+ for mp in ext_info.mapper.iterate_to_root():
+ self._mapper_loads_polymorphically_with(
+ mp,
+ ORMAdapter(
+ _TraceAdaptRole.WITH_POLYMORPHIC_ADAPTER,
+ mp,
+ equivalents=mp._equivalent_columns,
+ selectable=selectable,
+ ),
+ )
+
+ def _mapper_loads_polymorphically_with(self, mapper, adapter):
+ for m2 in mapper._with_polymorphic_mappers or [mapper]:
+ self._polymorphic_adapters[m2] = adapter
+
+ for m in m2.iterate_to_root():
+ self._polymorphic_adapters[m.local_table] = adapter
+
+ @classmethod
+ def _create_entities_collection(cls, query, legacy):
+ raise NotImplementedError(
+ "this method only works for ORMSelectCompileState"
+ )
+
+
+class DMLReturningColFilter:
+ """an adapter used for the DML RETURNING case.
+
+ Has a subset of the interface used by
+ :class:`.ORMAdapter` and is used for :class:`._QueryEntity`
+ instances to set up their columns as used in RETURNING for a
+ DML statement.
+
+ """
+
+ __slots__ = ("mapper", "columns", "__weakref__")
+
+ def __init__(self, target_mapper, immediate_dml_mapper):
+ if (
+ immediate_dml_mapper is not None
+ and target_mapper.local_table
+ is not immediate_dml_mapper.local_table
+ ):
+ # joined inh, or in theory other kinds of multi-table mappings
+ self.mapper = immediate_dml_mapper
+ else:
+ # single inh, normal mappings, etc.
+ self.mapper = target_mapper
+ self.columns = self.columns = util.WeakPopulateDict(
+ self.adapt_check_present # type: ignore
+ )
+
+ def __call__(self, col, as_filter):
+ for cc in sql_util._find_columns(col):
+ c2 = self.adapt_check_present(cc)
+ if c2 is not None:
+ return col
+ else:
+ return None
+
+ def adapt_check_present(self, col):
+ mapper = self.mapper
+ prop = mapper._columntoproperty.get(col, None)
+ if prop is None:
+ return None
+ return mapper.local_table.c.corresponding_column(col)
+
+
+@sql.base.CompileState.plugin_for("orm", "orm_from_statement")
+class ORMFromStatementCompileState(ORMCompileState):
+ _from_obj_alias = None
+ _has_mapper_entities = False
+
+ statement_container: FromStatement
+ requested_statement: Union[SelectBase, TextClause, UpdateBase]
+ dml_table: Optional[_DMLTableElement] = None
+
+ _has_orm_entities = False
+ multi_row_eager_loaders = False
+ eager_adding_joins = False
+ compound_eager_adapter = None
+
+ extra_criteria_entities = _EMPTY_DICT
+ eager_joins = _EMPTY_DICT
+
+ @classmethod
+ def create_for_statement(
+ cls,
+ statement_container: Union[Select, FromStatement],
+ compiler: Optional[SQLCompiler],
+ **kw: Any,
+ ) -> ORMFromStatementCompileState:
+ assert isinstance(statement_container, FromStatement)
+
+ if compiler is not None and compiler.stack:
+ raise sa_exc.CompileError(
+ "The ORM FromStatement construct only supports being "
+ "invoked as the topmost statement, as it is only intended to "
+ "define how result rows should be returned."
+ )
+
+ self = cls.__new__(cls)
+ self._primary_entity = None
+
+ self.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
+
+ if statement.is_dml:
+ self.dml_table = statement.table
+ self.is_dml_returning = True
+
+ self._entities = []
+ self._polymorphic_adapters = {}
+
+ self.compile_options = statement_container._compile_options
+
+ if (
+ self.use_legacy_query_style
+ and isinstance(statement, expression.SelectBase)
+ and not statement._is_textual
+ and not statement.is_dml
+ and statement._label_style is LABEL_STYLE_NONE
+ ):
+ self.statement = statement.set_label_style(
+ LABEL_STYLE_TABLENAME_PLUS_COL
+ )
+ else:
+ self.statement = statement
+
+ self._label_convention = self._column_naming_convention(
+ (
+ statement._label_style
+ if not statement._is_textual and not statement.is_dml
+ else LABEL_STYLE_NONE
+ ),
+ self.use_legacy_query_style,
+ )
+
+ _QueryEntity.to_compile_state(
+ self,
+ statement_container._raw_columns,
+ self._entities,
+ is_current_entities=True,
+ )
+
+ self.current_path = statement_container._compile_options._current_path
+
+ self._init_global_attributes(
+ statement_container,
+ compiler,
+ process_criteria_for_toplevel=False,
+ toplevel=True,
+ )
+
+ if statement_container._with_options:
+ for opt in statement_container._with_options:
+ if opt._is_compile_state:
+ opt.process_compile_state(self)
+
+ if statement_container._with_context_options:
+ for fn, key in statement_container._with_context_options:
+ fn(self)
+
+ self.primary_columns = []
+ self.secondary_columns = []
+ self.dedupe_columns = set()
+ self.create_eager_joins = []
+ self._fallback_from_clauses = []
+
+ self.order_by = None
+
+ if isinstance(self.statement, expression.TextClause):
+ # TextClause has no "column" objects at all. for this case,
+ # we generate columns from our _QueryEntity objects, then
+ # flip on all the "please match no matter what" parameters.
+ self.extra_criteria_entities = {}
+
+ for entity in self._entities:
+ entity.setup_compile_state(self)
+
+ compiler._ordered_columns = compiler._textual_ordered_columns = (
+ False
+ )
+
+ # enable looser result column matching. this is shown to be
+ # needed by test_query.py::TextTest
+ compiler._loose_column_name_matching = True
+
+ for c in self.primary_columns:
+ compiler.process(
+ c,
+ within_columns_clause=True,
+ add_to_result_map=compiler._add_to_result_map,
+ )
+ else:
+ # for everyone else, Select, Insert, Update, TextualSelect, they
+ # have column objects already. After much
+ # experimentation here, the best approach seems to be, use
+ # those columns completely, don't interfere with the compiler
+ # at all; just in ORM land, use an adapter to convert from
+ # our ORM columns to whatever columns are in the statement,
+ # before we look in the result row. Adapt on names
+ # to accept cases such as issue #9217, however also allow
+ # this to be overridden for cases such as #9273.
+ self._from_obj_alias = ORMStatementAdapter(
+ _TraceAdaptRole.ADAPT_FROM_STATEMENT,
+ self.statement,
+ adapt_on_names=statement_container._adapt_on_names,
+ )
+
+ return self
+
+ def _adapt_col_list(self, cols, current_adapter):
+ return cols
+
+ def _get_current_adapter(self):
+ return None
+
+ def setup_dml_returning_compile_state(self, dml_mapper):
+ """used by BulkORMInsert (and Update / Delete?) to set up a handler
+ for RETURNING to return ORM objects and expressions
+
+ """
+ target_mapper = self.statement._propagate_attrs.get(
+ "plugin_subject", None
+ )
+ adapter = DMLReturningColFilter(target_mapper, dml_mapper)
+
+ if self.compile_options._is_star and (len(self._entities) != 1):
+ raise sa_exc.CompileError(
+ "Can't generate ORM query that includes multiple expressions "
+ "at the same time as '*'; query for '*' alone if present"
+ )
+
+ for entity in self._entities:
+ entity.setup_dml_returning_compile_state(self, adapter)
+
+
+class FromStatement(GroupedElement, Generative, TypedReturnsRows[_TP]):
+ """Core construct that represents a load of ORM objects from various
+ :class:`.ReturnsRows` and other classes including:
+
+ :class:`.Select`, :class:`.TextClause`, :class:`.TextualSelect`,
+ :class:`.CompoundSelect`, :class`.Insert`, :class:`.Update`,
+ and in theory, :class:`.Delete`.
+
+ """
+
+ __visit_name__ = "orm_from_statement"
+
+ _compile_options = ORMFromStatementCompileState.default_compile_options
+
+ _compile_state_factory = ORMFromStatementCompileState.create_for_statement
+
+ _for_update_arg = None
+
+ element: Union[ExecutableReturnsRows, TextClause]
+
+ _adapt_on_names: bool
+
+ _traverse_internals = [
+ ("_raw_columns", InternalTraversal.dp_clauseelement_list),
+ ("element", InternalTraversal.dp_clauseelement),
+ ] + Executable._executable_traverse_internals
+
+ _cache_key_traversal = _traverse_internals + [
+ ("_compile_options", InternalTraversal.dp_has_cache_key)
+ ]
+
+ def __init__(
+ self,
+ entities: Iterable[_ColumnsClauseArgument[Any]],
+ element: Union[ExecutableReturnsRows, TextClause],
+ _adapt_on_names: bool = True,
+ ):
+ self._raw_columns = [
+ coercions.expect(
+ roles.ColumnsClauseRole,
+ ent,
+ apply_propagate_attrs=self,
+ post_inspect=True,
+ )
+ for ent in util.to_list(entities)
+ ]
+ self.element = element
+ self.is_dml = element.is_dml
+ self._label_style = (
+ element._label_style if is_select_base(element) else None
+ )
+ self._adapt_on_names = _adapt_on_names
+
+ def _compiler_dispatch(self, compiler, **kw):
+ """provide a fixed _compiler_dispatch method.
+
+ This is roughly similar to using the sqlalchemy.ext.compiler
+ ``@compiles`` extension.
+
+ """
+
+ compile_state = self._compile_state_factory(self, compiler, **kw)
+
+ toplevel = not compiler.stack
+
+ if toplevel:
+ compiler.compile_state = compile_state
+
+ return compiler.process(compile_state.statement, **kw)
+
+ @property
+ def column_descriptions(self):
+ """Return a :term:`plugin-enabled` 'column descriptions' structure
+ referring to the columns which are SELECTed by this statement.
+
+ See the section :ref:`queryguide_inspection` for an overview
+ of this feature.
+
+ .. seealso::
+
+ :ref:`queryguide_inspection` - ORM background
+
+ """
+ meth = cast(
+ ORMSelectCompileState, SelectState.get_plugin_class(self)
+ ).get_column_descriptions
+ return meth(self)
+
+ def _ensure_disambiguated_names(self):
+ return self
+
+ def get_children(self, **kw):
+ yield from itertools.chain.from_iterable(
+ element._from_objects for element in self._raw_columns
+ )
+ yield from super().get_children(**kw)
+
+ @property
+ def _all_selected_columns(self):
+ return self.element._all_selected_columns
+
+ @property
+ def _return_defaults(self):
+ return self.element._return_defaults if is_dml(self.element) else None
+
+ @property
+ def _returning(self):
+ return self.element._returning if is_dml(self.element) else None
+
+ @property
+ def _inline(self):
+ return self.element._inline if is_insert_update(self.element) else None
+
+
+@sql.base.CompileState.plugin_for("orm", "compound_select")
+class CompoundSelectCompileState(
+ AutoflushOnlyORMCompileState, CompoundSelectState
+):
+ pass
+
+
+@sql.base.CompileState.plugin_for("orm", "select")
+class ORMSelectCompileState(ORMCompileState, SelectState):
+ _already_joined_edges = ()
+
+ _memoized_entities = _EMPTY_DICT
+
+ _from_obj_alias = None
+ _has_mapper_entities = False
+
+ _has_orm_entities = False
+ multi_row_eager_loaders = False
+ eager_adding_joins = False
+ compound_eager_adapter = None
+
+ correlate = None
+ correlate_except = None
+ _where_criteria = ()
+ _having_criteria = ()
+
+ @classmethod
+ def create_for_statement(
+ cls,
+ statement: Union[Select, FromStatement],
+ compiler: Optional[SQLCompiler],
+ **kw: Any,
+ ) -> ORMSelectCompileState:
+ """compiler hook, we arrive here from compiler.visit_select() only."""
+
+ self = cls.__new__(cls)
+
+ if compiler is not None:
+ toplevel = not compiler.stack
+ else:
+ toplevel = True
+
+ select_statement = statement
+
+ # 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
+ )
+
+ if select_statement._execution_options:
+ # execution options should not impact the compilation of a
+ # query, and at the moment subqueryloader is putting some things
+ # in here that we explicitly don't want stuck in a cache.
+ self.select_statement = select_statement._clone()
+ self.select_statement._execution_options = util.immutabledict()
+ else:
+ self.select_statement = select_statement
+
+ # indicates this select() came from Query.statement
+ self.for_statement = select_statement._compile_options._for_statement
+
+ # generally if we are from Query or directly from a select()
+ self.use_legacy_query_style = (
+ select_statement._compile_options._use_legacy_query_style
+ )
+
+ self._entities = []
+ self._primary_entity = None
+ self._polymorphic_adapters = {}
+
+ self.compile_options = select_statement._compile_options
+
+ if not toplevel:
+ # for subqueries, turn off eagerloads and set
+ # "render_for_subquery".
+ self.compile_options += {
+ "_enable_eagerloads": False,
+ "_render_for_subquery": True,
+ }
+
+ # determine label style. we can make different decisions here.
+ # at the moment, trying to see if we can always use DISAMBIGUATE_ONLY
+ # rather than LABEL_STYLE_NONE, and if we can use disambiguate style
+ # for new style ORM selects too.
+ if (
+ self.use_legacy_query_style
+ and self.select_statement._label_style is LABEL_STYLE_LEGACY_ORM
+ ):
+ if not self.for_statement:
+ self.label_style = LABEL_STYLE_TABLENAME_PLUS_COL
+ else:
+ self.label_style = LABEL_STYLE_DISAMBIGUATE_ONLY
+ else:
+ self.label_style = self.select_statement._label_style
+
+ if select_statement._memoized_select_entities:
+ self._memoized_entities = {
+ memoized_entities: _QueryEntity.to_compile_state(
+ self,
+ memoized_entities._raw_columns,
+ [],
+ is_current_entities=False,
+ )
+ for memoized_entities in (
+ select_statement._memoized_select_entities
+ )
+ }
+
+ # label_convention is stateful and will yield deduping keys if it
+ # sees the same key twice. therefore it's important that it is not
+ # invoked for the above "memoized" entities that aren't actually
+ # in the columns clause
+ self._label_convention = self._column_naming_convention(
+ statement._label_style, self.use_legacy_query_style
+ )
+
+ _QueryEntity.to_compile_state(
+ self,
+ select_statement._raw_columns,
+ self._entities,
+ is_current_entities=True,
+ )
+
+ self.current_path = select_statement._compile_options._current_path
+
+ self.eager_order_by = ()
+
+ self._init_global_attributes(
+ select_statement,
+ compiler,
+ toplevel=toplevel,
+ process_criteria_for_toplevel=False,
+ )
+
+ if toplevel and (
+ select_statement._with_options
+ or select_statement._memoized_select_entities
+ ):
+ for (
+ memoized_entities
+ ) in select_statement._memoized_select_entities:
+ for opt in memoized_entities._with_options:
+ if opt._is_compile_state:
+ opt.process_compile_state_replaced_entities(
+ self,
+ [
+ ent
+ for ent in self._memoized_entities[
+ memoized_entities
+ ]
+ if isinstance(ent, _MapperEntity)
+ ],
+ )
+
+ for opt in self.select_statement._with_options:
+ if opt._is_compile_state:
+ opt.process_compile_state(self)
+
+ # uncomment to print out the context.attributes structure
+ # after it's been set up above
+ # self._dump_option_struct()
+
+ if select_statement._with_context_options:
+ for fn, key in select_statement._with_context_options:
+ fn(self)
+
+ self.primary_columns = []
+ self.secondary_columns = []
+ self.dedupe_columns = set()
+ self.eager_joins = {}
+ self.extra_criteria_entities = {}
+ self.create_eager_joins = []
+ self._fallback_from_clauses = []
+
+ # normalize the FROM clauses early by themselves, as this makes
+ # it an easier job when we need to assemble a JOIN onto these,
+ # for select.join() as well as joinedload(). As of 1.4 there are now
+ # potentially more complex sets of FROM objects here as the use
+ # of lambda statements for lazyload, load_on_pk etc. uses more
+ # cloning of the select() construct. See #6495
+ self.from_clauses = self._normalize_froms(
+ info.selectable for info in select_statement._from_obj
+ )
+
+ # this is a fairly arbitrary break into a second method,
+ # so it might be nicer to break up create_for_statement()
+ # and _setup_for_generate into three or four logical sections
+ self._setup_for_generate()
+
+ SelectState.__init__(self, self.statement, compiler, **kw)
+ return self
+
+ def _dump_option_struct(self):
+ print("\n---------------------------------------------------\n")
+ print(f"current path: {self.current_path}")
+ for key in self.attributes:
+ if isinstance(key, tuple) and key[0] == "loader":
+ print(f"\nLoader: {PathRegistry.coerce(key[1])}")
+ print(f" {self.attributes[key]}")
+ print(f" {self.attributes[key].__dict__}")
+ elif isinstance(key, tuple) and key[0] == "path_with_polymorphic":
+ print(f"\nWith Polymorphic: {PathRegistry.coerce(key[1])}")
+ print(f" {self.attributes[key]}")
+
+ def _setup_for_generate(self):
+ query = self.select_statement
+
+ self.statement = None
+ self._join_entities = ()
+
+ if self.compile_options._set_base_alias:
+ # legacy Query only
+ self._set_select_from_alias()
+
+ for memoized_entities in query._memoized_select_entities:
+ if memoized_entities._setup_joins:
+ self._join(
+ memoized_entities._setup_joins,
+ self._memoized_entities[memoized_entities],
+ )
+
+ if query._setup_joins:
+ self._join(query._setup_joins, self._entities)
+
+ current_adapter = self._get_current_adapter()
+
+ if query._where_criteria:
+ self._where_criteria = query._where_criteria
+
+ if current_adapter:
+ self._where_criteria = tuple(
+ current_adapter(crit, True)
+ for crit in self._where_criteria
+ )
+
+ # TODO: some complexity with order_by here was due to mapper.order_by.
+ # now that this is removed we can hopefully make order_by /
+ # group_by act identically to how they are in Core select.
+ self.order_by = (
+ self._adapt_col_list(query._order_by_clauses, current_adapter)
+ if current_adapter and query._order_by_clauses not in (None, False)
+ else query._order_by_clauses
+ )
+
+ if query._having_criteria:
+ self._having_criteria = tuple(
+ current_adapter(crit, True) if current_adapter else crit
+ for crit in query._having_criteria
+ )
+
+ self.group_by = (
+ self._adapt_col_list(
+ util.flatten_iterator(query._group_by_clauses), current_adapter
+ )
+ if current_adapter and query._group_by_clauses not in (None, False)
+ else query._group_by_clauses or None
+ )
+
+ if self.eager_order_by:
+ adapter = self.from_clauses[0]._target_adapter
+ self.eager_order_by = adapter.copy_and_process(self.eager_order_by)
+
+ if query._distinct_on:
+ self.distinct_on = self._adapt_col_list(
+ query._distinct_on, current_adapter
+ )
+ else:
+ self.distinct_on = ()
+
+ self.distinct = query._distinct
+
+ if query._correlate:
+ # ORM mapped entities that are mapped to joins can be passed
+ # to .correlate, so here they are broken into their component
+ # tables.
+ self.correlate = tuple(
+ util.flatten_iterator(
+ sql_util.surface_selectables(s) if s is not None else None
+ for s in query._correlate
+ )
+ )
+ elif query._correlate_except is not None:
+ self.correlate_except = tuple(
+ util.flatten_iterator(
+ sql_util.surface_selectables(s) if s is not None else None
+ for s in query._correlate_except
+ )
+ )
+ elif not query._auto_correlate:
+ self.correlate = (None,)
+
+ # PART II
+
+ self._for_update_arg = query._for_update_arg
+
+ if self.compile_options._is_star and (len(self._entities) != 1):
+ raise sa_exc.CompileError(
+ "Can't generate ORM query that includes multiple expressions "
+ "at the same time as '*'; query for '*' alone if present"
+ )
+ for entity in self._entities:
+ entity.setup_compile_state(self)
+
+ for rec in self.create_eager_joins:
+ strategy = rec[0]
+ strategy(self, *rec[1:])
+
+ # else "load from discrete FROMs" mode,
+ # i.e. when each _MappedEntity has its own FROM
+
+ if self.compile_options._enable_single_crit:
+ self._adjust_for_extra_criteria()
+
+ if not self.primary_columns:
+ if self.compile_options._only_load_props:
+ assert False, "no columns were included in _only_load_props"
+
+ raise sa_exc.InvalidRequestError(
+ "Query contains no columns with which to SELECT from."
+ )
+
+ if not self.from_clauses:
+ self.from_clauses = list(self._fallback_from_clauses)
+
+ if self.order_by is False:
+ self.order_by = None
+
+ if (
+ self.multi_row_eager_loaders
+ and self.eager_adding_joins
+ and self._should_nest_selectable
+ ):
+ self.statement = self._compound_eager_statement()
+ else:
+ self.statement = self._simple_statement()
+
+ if self.for_statement:
+ ezero = self._mapper_zero()
+ if ezero is not None:
+ # TODO: this goes away once we get rid of the deep entity
+ # thing
+ self.statement = self.statement._annotate(
+ {"deepentity": ezero}
+ )
+
+ @classmethod
+ def _create_entities_collection(cls, query, legacy):
+ """Creates a partial ORMSelectCompileState that includes
+ the full collection of _MapperEntity and other _QueryEntity objects.
+
+ Supports a few remaining use cases that are pre-compilation
+ but still need to gather some of the column / adaption information.
+
+ """
+ self = cls.__new__(cls)
+
+ self._entities = []
+ self._primary_entity = None
+ self._polymorphic_adapters = {}
+
+ self._label_convention = self._column_naming_convention(
+ query._label_style, legacy
+ )
+
+ # entities will also set up polymorphic adapters for mappers
+ # that have with_polymorphic configured
+ _QueryEntity.to_compile_state(
+ self, query._raw_columns, self._entities, is_current_entities=True
+ )
+ return self
+
+ @classmethod
+ def determine_last_joined_entity(cls, statement):
+ setup_joins = statement._setup_joins
+
+ return _determine_last_joined_entity(setup_joins, None)
+
+ @classmethod
+ def all_selected_columns(cls, statement):
+ for element in statement._raw_columns:
+ if (
+ element.is_selectable
+ and "entity_namespace" in element._annotations
+ ):
+ ens = element._annotations["entity_namespace"]
+ if not ens.is_mapper and not ens.is_aliased_class:
+ yield from _select_iterables([element])
+ else:
+ yield from _select_iterables(ens._all_column_expressions)
+ else:
+ yield from _select_iterables([element])
+
+ @classmethod
+ def get_columns_clause_froms(cls, statement):
+ return cls._normalize_froms(
+ itertools.chain.from_iterable(
+ (
+ element._from_objects
+ if "parententity" not in element._annotations
+ else [
+ element._annotations[
+ "parententity"
+ ].__clause_element__()
+ ]
+ )
+ for element in statement._raw_columns
+ )
+ )
+
+ @classmethod
+ def from_statement(cls, statement, from_statement):
+ from_statement = coercions.expect(
+ roles.ReturnsRowsRole,
+ from_statement,
+ apply_propagate_attrs=statement,
+ )
+
+ stmt = 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 _set_select_from_alias(self):
+ """used only for legacy Query cases"""
+
+ query = self.select_statement # query
+
+ assert self.compile_options._set_base_alias
+ assert len(query._from_obj) == 1
+
+ adapter = self._get_select_from_alias_from_obj(query._from_obj[0])
+ if adapter:
+ self.compile_options += {"_enable_single_crit": False}
+ self._from_obj_alias = adapter
+
+ def _get_select_from_alias_from_obj(self, from_obj):
+ """used only for legacy Query cases"""
+
+ info = from_obj
+
+ if "parententity" in info._annotations:
+ info = info._annotations["parententity"]
+
+ if hasattr(info, "mapper"):
+ if not info.is_aliased_class:
+ raise sa_exc.ArgumentError(
+ "A selectable (FromClause) instance is "
+ "expected when the base alias is being set."
+ )
+ else:
+ return info._adapter
+
+ elif isinstance(info.selectable, sql.selectable.AliasedReturnsRows):
+ equivs = self._all_equivs()
+ assert info is info.selectable
+ return ORMStatementAdapter(
+ _TraceAdaptRole.LEGACY_SELECT_FROM_ALIAS,
+ info.selectable,
+ equivalents=equivs,
+ )
+ else:
+ return None
+
+ def _mapper_zero(self):
+ """return the Mapper associated with the first QueryEntity."""
+ return self._entities[0].mapper
+
+ def _entity_zero(self):
+ """Return the 'entity' (mapper or AliasedClass) associated
+ with the first QueryEntity, or alternatively the 'select from'
+ entity if specified."""
+
+ for ent in self.from_clauses:
+ if "parententity" in ent._annotations:
+ return ent._annotations["parententity"]
+ for qent in self._entities:
+ if qent.entity_zero:
+ return qent.entity_zero
+
+ return None
+
+ def _only_full_mapper_zero(self, methname):
+ if self._entities != [self._primary_entity]:
+ raise sa_exc.InvalidRequestError(
+ "%s() can only be used against "
+ "a single mapped class." % methname
+ )
+ return self._primary_entity.entity_zero
+
+ def _only_entity_zero(self, rationale=None):
+ if len(self._entities) > 1:
+ raise sa_exc.InvalidRequestError(
+ rationale
+ or "This operation requires a Query "
+ "against a single mapper."
+ )
+ return self._entity_zero()
+
+ def _all_equivs(self):
+ equivs = {}
+
+ for memoized_entities in self._memoized_entities.values():
+ for ent in [
+ ent
+ for ent in memoized_entities
+ if isinstance(ent, _MapperEntity)
+ ]:
+ equivs.update(ent.mapper._equivalent_columns)
+
+ for ent in [
+ ent for ent in self._entities if isinstance(ent, _MapperEntity)
+ ]:
+ equivs.update(ent.mapper._equivalent_columns)
+ return equivs
+
+ def _compound_eager_statement(self):
+ # for eager joins present and LIMIT/OFFSET/DISTINCT,
+ # wrap the query inside a select,
+ # then append eager joins onto that
+
+ if self.order_by:
+ # the default coercion for ORDER BY is now the OrderByRole,
+ # which adds an additional post coercion to ByOfRole in that
+ # elements are converted into label references. For the
+ # eager load / subquery wrapping case, we need to un-coerce
+ # the original expressions outside of the label references
+ # in order to have them render.
+ unwrapped_order_by = [
+ (
+ elem.element
+ if isinstance(elem, sql.elements._label_reference)
+ else elem
+ )
+ for elem in self.order_by
+ ]
+
+ order_by_col_expr = sql_util.expand_column_list_from_order_by(
+ self.primary_columns, unwrapped_order_by
+ )
+ else:
+ order_by_col_expr = []
+ unwrapped_order_by = None
+
+ # put FOR UPDATE on the inner query, where MySQL will honor it,
+ # as well as if it has an OF so PostgreSQL can use it.
+ inner = self._select_statement(
+ self.primary_columns
+ + [c for c in order_by_col_expr if c not in self.dedupe_columns],
+ self.from_clauses,
+ self._where_criteria,
+ self._having_criteria,
+ self.label_style,
+ self.order_by,
+ for_update=self._for_update_arg,
+ hints=self.select_statement._hints,
+ statement_hints=self.select_statement._statement_hints,
+ correlate=self.correlate,
+ correlate_except=self.correlate_except,
+ **self._select_args,
+ )
+
+ inner = inner.alias()
+
+ equivs = self._all_equivs()
+
+ self.compound_eager_adapter = ORMStatementAdapter(
+ _TraceAdaptRole.COMPOUND_EAGER_STATEMENT, inner, equivalents=equivs
+ )
+
+ statement = future.select(
+ *([inner] + self.secondary_columns) # use_labels=self.labels
+ )
+ statement._label_style = self.label_style
+
+ # Oracle however does not allow FOR UPDATE on the subquery,
+ # and the Oracle dialect ignores it, plus for PostgreSQL, MySQL
+ # we expect that all elements of the row are locked, so also put it
+ # on the outside (except in the case of PG when OF is used)
+ if (
+ self._for_update_arg is not None
+ and self._for_update_arg.of is None
+ ):
+ statement._for_update_arg = self._for_update_arg
+
+ from_clause = inner
+ for eager_join in self.eager_joins.values():
+ # EagerLoader places a 'stop_on' attribute on the join,
+ # giving us a marker as to where the "splice point" of
+ # the join should be
+ from_clause = sql_util.splice_joins(
+ from_clause, eager_join, eager_join.stop_on
+ )
+
+ statement.select_from.non_generative(statement, from_clause)
+
+ if unwrapped_order_by:
+ statement.order_by.non_generative(
+ statement,
+ *self.compound_eager_adapter.copy_and_process(
+ unwrapped_order_by
+ ),
+ )
+
+ statement.order_by.non_generative(statement, *self.eager_order_by)
+ return statement
+
+ def _simple_statement(self):
+ statement = self._select_statement(
+ self.primary_columns + self.secondary_columns,
+ tuple(self.from_clauses) + tuple(self.eager_joins.values()),
+ self._where_criteria,
+ self._having_criteria,
+ self.label_style,
+ self.order_by,
+ for_update=self._for_update_arg,
+ hints=self.select_statement._hints,
+ statement_hints=self.select_statement._statement_hints,
+ correlate=self.correlate,
+ correlate_except=self.correlate_except,
+ **self._select_args,
+ )
+
+ if self.eager_order_by:
+ statement.order_by.non_generative(statement, *self.eager_order_by)
+ return statement
+
+ def _select_statement(
+ self,
+ raw_columns,
+ from_obj,
+ where_criteria,
+ having_criteria,
+ label_style,
+ order_by,
+ for_update,
+ hints,
+ statement_hints,
+ correlate,
+ correlate_except,
+ limit_clause,
+ offset_clause,
+ fetch_clause,
+ fetch_clause_options,
+ distinct,
+ distinct_on,
+ prefixes,
+ suffixes,
+ group_by,
+ independent_ctes,
+ independent_ctes_opts,
+ ):
+ statement = Select._create_raw_select(
+ _raw_columns=raw_columns,
+ _from_obj=from_obj,
+ _label_style=label_style,
+ )
+
+ if where_criteria:
+ statement._where_criteria = where_criteria
+ if having_criteria:
+ statement._having_criteria = having_criteria
+
+ if order_by:
+ statement._order_by_clauses += tuple(order_by)
+
+ if distinct_on:
+ statement.distinct.non_generative(statement, *distinct_on)
+ elif distinct:
+ statement.distinct.non_generative(statement)
+
+ if group_by:
+ statement._group_by_clauses += tuple(group_by)
+
+ statement._limit_clause = limit_clause
+ statement._offset_clause = offset_clause
+ statement._fetch_clause = fetch_clause
+ statement._fetch_clause_options = fetch_clause_options
+ statement._independent_ctes = independent_ctes
+ statement._independent_ctes_opts = independent_ctes_opts
+
+ if prefixes:
+ statement._prefixes = prefixes
+
+ if suffixes:
+ statement._suffixes = suffixes
+
+ statement._for_update_arg = for_update
+
+ if hints:
+ statement._hints = hints
+ if statement_hints:
+ statement._statement_hints = statement_hints
+
+ if correlate:
+ statement.correlate.non_generative(statement, *correlate)
+
+ if correlate_except is not None:
+ statement.correlate_except.non_generative(
+ statement, *correlate_except
+ )
+
+ return statement
+
+ def _adapt_polymorphic_element(self, element):
+ if "parententity" in element._annotations:
+ search = element._annotations["parententity"]
+ alias = self._polymorphic_adapters.get(search, None)
+ if alias:
+ return alias.adapt_clause(element)
+
+ if isinstance(element, expression.FromClause):
+ search = element
+ elif hasattr(element, "table"):
+ search = element.table
+ else:
+ return None
+
+ alias = self._polymorphic_adapters.get(search, None)
+ if alias:
+ return alias.adapt_clause(element)
+
+ def _adapt_col_list(self, cols, current_adapter):
+ if current_adapter:
+ return [current_adapter(o, True) for o in cols]
+ else:
+ return cols
+
+ def _get_current_adapter(self):
+ adapters = []
+
+ if self._from_obj_alias:
+ # used for legacy going forward for query set_ops, e.g.
+ # union(), union_all(), etc.
+ # 1.4 and previously, also used for from_self(),
+ # select_entity_from()
+ #
+ # for the "from obj" alias, apply extra rule to the
+ # 'ORM only' check, if this query were generated from a
+ # subquery of itself, i.e. _from_selectable(), apply adaption
+ # to all SQL constructs.
+ adapters.append(
+ (
+ True,
+ self._from_obj_alias.replace,
+ )
+ )
+
+ # this was *hopefully* the only adapter we were going to need
+ # going forward...however, we unfortunately need _from_obj_alias
+ # for query.union(), which we can't drop
+ if self._polymorphic_adapters:
+ adapters.append((False, self._adapt_polymorphic_element))
+
+ if not adapters:
+ return None
+
+ def _adapt_clause(clause, as_filter):
+ # do we adapt all expression elements or only those
+ # tagged as 'ORM' constructs ?
+
+ def replace(elem):
+ is_orm_adapt = (
+ "_orm_adapt" in elem._annotations
+ or "parententity" in elem._annotations
+ )
+ for always_adapt, adapter in adapters:
+ if is_orm_adapt or always_adapt:
+ e = adapter(elem)
+ if e is not None:
+ return e
+
+ return visitors.replacement_traverse(clause, {}, replace)
+
+ return _adapt_clause
+
+ def _join(self, args, entities_collection):
+ for right, onclause, from_, flags in args:
+ isouter = flags["isouter"]
+ full = flags["full"]
+
+ right = inspect(right)
+ if onclause is not None:
+ onclause = inspect(onclause)
+
+ if isinstance(right, interfaces.PropComparator):
+ if onclause is not None:
+ raise sa_exc.InvalidRequestError(
+ "No 'on clause' argument may be passed when joining "
+ "to a relationship path as a target"
+ )
+
+ onclause = right
+ right = None
+ elif "parententity" in right._annotations:
+ right = right._annotations["parententity"]
+
+ if onclause is None:
+ if not right.is_selectable and not hasattr(right, "mapper"):
+ raise sa_exc.ArgumentError(
+ "Expected mapped entity or "
+ "selectable/table as join target"
+ )
+
+ of_type = None
+
+ if isinstance(onclause, interfaces.PropComparator):
+ # descriptor/property given (or determined); this tells us
+ # explicitly what the expected "left" side of the join is.
+
+ of_type = getattr(onclause, "_of_type", None)
+
+ if right is None:
+ if of_type:
+ right = of_type
+ else:
+ right = onclause.property
+
+ try:
+ right = right.entity
+ except AttributeError as err:
+ raise sa_exc.ArgumentError(
+ "Join target %s does not refer to a "
+ "mapped entity" % right
+ ) from err
+
+ left = onclause._parententity
+
+ prop = onclause.property
+ if not isinstance(onclause, attributes.QueryableAttribute):
+ onclause = prop
+
+ # check for this path already present. don't render in that
+ # case.
+ if (left, right, prop.key) in self._already_joined_edges:
+ continue
+
+ if from_ is not None:
+ if (
+ from_ is not left
+ and from_._annotations.get("parententity", None)
+ is not left
+ ):
+ raise sa_exc.InvalidRequestError(
+ "explicit from clause %s does not match left side "
+ "of relationship attribute %s"
+ % (
+ from_._annotations.get("parententity", from_),
+ onclause,
+ )
+ )
+ elif from_ is not None:
+ prop = None
+ left = from_
+ else:
+ # no descriptor/property given; we will need to figure out
+ # what the effective "left" side is
+ prop = left = None
+
+ # figure out the final "left" and "right" sides and create an
+ # ORMJoin to add to our _from_obj tuple
+ self._join_left_to_right(
+ entities_collection,
+ left,
+ right,
+ onclause,
+ prop,
+ isouter,
+ full,
+ )
+
+ def _join_left_to_right(
+ self,
+ entities_collection,
+ left,
+ right,
+ onclause,
+ prop,
+ outerjoin,
+ full,
+ ):
+ """given raw "left", "right", "onclause" parameters consumed from
+ a particular key within _join(), add a real ORMJoin object to
+ our _from_obj list (or augment an existing one)
+
+ """
+
+ if left is None:
+ # left not given (e.g. no relationship object/name specified)
+ # figure out the best "left" side based on our existing froms /
+ # entities
+ assert prop is None
+ (
+ left,
+ replace_from_obj_index,
+ use_entity_index,
+ ) = self._join_determine_implicit_left_side(
+ entities_collection, left, right, onclause
+ )
+ else:
+ # left is given via a relationship/name, or as explicit left side.
+ # Determine where in our
+ # "froms" list it should be spliced/appended as well as what
+ # existing entity it corresponds to.
+ (
+ replace_from_obj_index,
+ use_entity_index,
+ ) = self._join_place_explicit_left_side(entities_collection, left)
+
+ if left is right:
+ raise sa_exc.InvalidRequestError(
+ "Can't construct a join from %s to %s, they "
+ "are the same entity" % (left, right)
+ )
+
+ # the right side as given often needs to be adapted. additionally
+ # a lot of things can be wrong with it. handle all that and
+ # get back the new effective "right" side
+ r_info, right, onclause = self._join_check_and_adapt_right_side(
+ left, right, onclause, prop
+ )
+
+ if not r_info.is_selectable:
+ extra_criteria = self._get_extra_criteria(r_info)
+ else:
+ extra_criteria = ()
+
+ if replace_from_obj_index is not None:
+ # splice into an existing element in the
+ # self._from_obj list
+ left_clause = self.from_clauses[replace_from_obj_index]
+
+ self.from_clauses = (
+ self.from_clauses[:replace_from_obj_index]
+ + [
+ _ORMJoin(
+ left_clause,
+ right,
+ onclause,
+ isouter=outerjoin,
+ full=full,
+ _extra_criteria=extra_criteria,
+ )
+ ]
+ + self.from_clauses[replace_from_obj_index + 1 :]
+ )
+ else:
+ # add a new element to the self._from_obj list
+ if use_entity_index is not None:
+ # make use of _MapperEntity selectable, which is usually
+ # entity_zero.selectable, but if with_polymorphic() were used
+ # might be distinct
+ assert isinstance(
+ entities_collection[use_entity_index], _MapperEntity
+ )
+ left_clause = entities_collection[use_entity_index].selectable
+ else:
+ left_clause = left
+
+ self.from_clauses = self.from_clauses + [
+ _ORMJoin(
+ left_clause,
+ r_info,
+ onclause,
+ isouter=outerjoin,
+ full=full,
+ _extra_criteria=extra_criteria,
+ )
+ ]
+
+ def _join_determine_implicit_left_side(
+ self, entities_collection, left, right, onclause
+ ):
+ """When join conditions don't express the left side explicitly,
+ determine if an existing FROM or entity in this query
+ can serve as the left hand side.
+
+ """
+
+ # when we are here, it means join() was called without an ORM-
+ # specific way of telling us what the "left" side is, e.g.:
+ #
+ # join(RightEntity)
+ #
+ # or
+ #
+ # join(RightEntity, RightEntity.foo == LeftEntity.bar)
+ #
+
+ r_info = inspect(right)
+
+ replace_from_obj_index = use_entity_index = None
+
+ if self.from_clauses:
+ # we have a list of FROMs already. So by definition this
+ # join has to connect to one of those FROMs.
+
+ indexes = sql_util.find_left_clause_to_join_from(
+ self.from_clauses, r_info.selectable, onclause
+ )
+
+ if len(indexes) == 1:
+ replace_from_obj_index = indexes[0]
+ left = self.from_clauses[replace_from_obj_index]
+ elif len(indexes) > 1:
+ raise sa_exc.InvalidRequestError(
+ "Can't determine which FROM clause to join "
+ "from, there are multiple FROMS which can "
+ "join to this entity. Please use the .select_from() "
+ "method to establish an explicit left side, as well as "
+ "providing an explicit ON clause if not present already "
+ "to help resolve the ambiguity."
+ )
+ else:
+ raise sa_exc.InvalidRequestError(
+ "Don't know how to join to %r. "
+ "Please use the .select_from() "
+ "method to establish an explicit left side, as well as "
+ "providing an explicit ON clause if not present already "
+ "to help resolve the ambiguity." % (right,)
+ )
+
+ elif entities_collection:
+ # we have no explicit FROMs, so the implicit left has to
+ # come from our list of entities.
+
+ potential = {}
+ for entity_index, ent in enumerate(entities_collection):
+ entity = ent.entity_zero_or_selectable
+ if entity is None:
+ continue
+ ent_info = inspect(entity)
+ if ent_info is r_info: # left and right are the same, skip
+ continue
+
+ # by using a dictionary with the selectables as keys this
+ # de-duplicates those selectables as occurs when the query is
+ # against a series of columns from the same selectable
+ if isinstance(ent, _MapperEntity):
+ potential[ent.selectable] = (entity_index, entity)
+ else:
+ potential[ent_info.selectable] = (None, entity)
+
+ all_clauses = list(potential.keys())
+ indexes = sql_util.find_left_clause_to_join_from(
+ all_clauses, r_info.selectable, onclause
+ )
+
+ if len(indexes) == 1:
+ use_entity_index, left = potential[all_clauses[indexes[0]]]
+ elif len(indexes) > 1:
+ raise sa_exc.InvalidRequestError(
+ "Can't determine which FROM clause to join "
+ "from, there are multiple FROMS which can "
+ "join to this entity. Please use the .select_from() "
+ "method to establish an explicit left side, as well as "
+ "providing an explicit ON clause if not present already "
+ "to help resolve the ambiguity."
+ )
+ else:
+ raise sa_exc.InvalidRequestError(
+ "Don't know how to join to %r. "
+ "Please use the .select_from() "
+ "method to establish an explicit left side, as well as "
+ "providing an explicit ON clause if not present already "
+ "to help resolve the ambiguity." % (right,)
+ )
+ else:
+ raise sa_exc.InvalidRequestError(
+ "No entities to join from; please use "
+ "select_from() to establish the left "
+ "entity/selectable of this join"
+ )
+
+ return left, replace_from_obj_index, use_entity_index
+
+ def _join_place_explicit_left_side(self, entities_collection, left):
+ """When join conditions express a left side explicitly, determine
+ where in our existing list of FROM clauses we should join towards,
+ or if we need to make a new join, and if so is it from one of our
+ existing entities.
+
+ """
+
+ # when we are here, it means join() was called with an indicator
+ # as to an exact left side, which means a path to a
+ # Relationship was given, e.g.:
+ #
+ # join(RightEntity, LeftEntity.right)
+ #
+ # or
+ #
+ # join(LeftEntity.right)
+ #
+ # as well as string forms:
+ #
+ # join(RightEntity, "right")
+ #
+ # etc.
+ #
+
+ replace_from_obj_index = use_entity_index = None
+
+ l_info = inspect(left)
+ if self.from_clauses:
+ indexes = sql_util.find_left_clause_that_matches_given(
+ self.from_clauses, l_info.selectable
+ )
+
+ if len(indexes) > 1:
+ raise sa_exc.InvalidRequestError(
+ "Can't identify which entity in which to assign the "
+ "left side of this join. Please use a more specific "
+ "ON clause."
+ )
+
+ # have an index, means the left side is already present in
+ # an existing FROM in the self._from_obj tuple
+ if indexes:
+ replace_from_obj_index = indexes[0]
+
+ # no index, means we need to add a new element to the
+ # self._from_obj tuple
+
+ # no from element present, so we will have to add to the
+ # self._from_obj tuple. Determine if this left side matches up
+ # with existing mapper entities, in which case we want to apply the
+ # aliasing / adaptation rules present on that entity if any
+ if (
+ replace_from_obj_index is None
+ and entities_collection
+ and hasattr(l_info, "mapper")
+ ):
+ for idx, ent in enumerate(entities_collection):
+ # TODO: should we be checking for multiple mapper entities
+ # matching?
+ if isinstance(ent, _MapperEntity) and ent.corresponds_to(left):
+ use_entity_index = idx
+ break
+
+ return replace_from_obj_index, use_entity_index
+
+ def _join_check_and_adapt_right_side(self, left, right, onclause, prop):
+ """transform the "right" side of the join as well as the onclause
+ according to polymorphic mapping translations, aliasing on the query
+ or on the join, special cases where the right and left side have
+ overlapping tables.
+
+ """
+
+ l_info = inspect(left)
+ r_info = inspect(right)
+
+ overlap = False
+
+ right_mapper = getattr(r_info, "mapper", None)
+ # if the target is a joined inheritance mapping,
+ # be more liberal about auto-aliasing.
+ if right_mapper and (
+ right_mapper.with_polymorphic
+ or isinstance(right_mapper.persist_selectable, expression.Join)
+ ):
+ for from_obj in self.from_clauses or [l_info.selectable]:
+ if sql_util.selectables_overlap(
+ l_info.selectable, from_obj
+ ) and sql_util.selectables_overlap(
+ from_obj, r_info.selectable
+ ):
+ overlap = True
+ break
+
+ if overlap and l_info.selectable is r_info.selectable:
+ raise sa_exc.InvalidRequestError(
+ "Can't join table/selectable '%s' to itself"
+ % l_info.selectable
+ )
+
+ right_mapper, right_selectable, right_is_aliased = (
+ getattr(r_info, "mapper", None),
+ r_info.selectable,
+ getattr(r_info, "is_aliased_class", False),
+ )
+
+ if (
+ right_mapper
+ and prop
+ and not right_mapper.common_parent(prop.mapper)
+ ):
+ raise sa_exc.InvalidRequestError(
+ "Join target %s does not correspond to "
+ "the right side of join condition %s" % (right, onclause)
+ )
+
+ # _join_entities is used as a hint for single-table inheritance
+ # purposes at the moment
+ if hasattr(r_info, "mapper"):
+ self._join_entities += (r_info,)
+
+ need_adapter = False
+
+ # test for joining to an unmapped selectable as the target
+ if r_info.is_clause_element:
+ if prop:
+ right_mapper = prop.mapper
+
+ if right_selectable._is_lateral:
+ # orm_only is disabled to suit the case where we have to
+ # adapt an explicit correlate(Entity) - the select() loses
+ # the ORM-ness in this case right now, ideally it would not
+ current_adapter = self._get_current_adapter()
+ if current_adapter is not None:
+ # TODO: we had orm_only=False here before, removing
+ # it didn't break things. if we identify the rationale,
+ # may need to apply "_orm_only" annotation here.
+ right = current_adapter(right, True)
+
+ elif prop:
+ # joining to selectable with a mapper property given
+ # as the ON clause
+
+ if not right_selectable.is_derived_from(
+ right_mapper.persist_selectable
+ ):
+ raise sa_exc.InvalidRequestError(
+ "Selectable '%s' is not derived from '%s'"
+ % (
+ right_selectable.description,
+ right_mapper.persist_selectable.description,
+ )
+ )
+
+ # if the destination selectable is a plain select(),
+ # turn it into an alias().
+ if isinstance(right_selectable, expression.SelectBase):
+ right_selectable = coercions.expect(
+ roles.FromClauseRole, right_selectable
+ )
+ need_adapter = True
+
+ # make the right hand side target into an ORM entity
+ right = AliasedClass(right_mapper, right_selectable)
+
+ util.warn_deprecated(
+ "An alias is being generated automatically against "
+ "joined entity %s for raw clauseelement, which is "
+ "deprecated and will be removed in a later release. "
+ "Use the aliased() "
+ "construct explicitly, see the linked example."
+ % right_mapper,
+ "1.4",
+ code="xaj1",
+ )
+
+ # test for overlap:
+ # orm/inheritance/relationships.py
+ # SelfReferentialM2MTest
+ aliased_entity = right_mapper and not right_is_aliased and overlap
+
+ if not need_adapter and aliased_entity:
+ # there are a few places in the ORM that automatic aliasing
+ # is still desirable, and can't be automatic with a Core
+ # only approach. For illustrations of "overlaps" see
+ # test/orm/inheritance/test_relationships.py. There are also
+ # general overlap cases with many-to-many tables where automatic
+ # aliasing is desirable.
+ right = AliasedClass(right, flat=True)
+ need_adapter = True
+
+ util.warn(
+ "An alias is being generated automatically against "
+ "joined entity %s due to overlapping tables. This is a "
+ "legacy pattern which may be "
+ "deprecated in a later release. Use the "
+ "aliased(<entity>, flat=True) "
+ "construct explicitly, see the linked example." % right_mapper,
+ code="xaj2",
+ )
+
+ if need_adapter:
+ # if need_adapter is True, we are in a deprecated case and
+ # a warning has been emitted.
+ assert right_mapper
+
+ adapter = ORMAdapter(
+ _TraceAdaptRole.DEPRECATED_JOIN_ADAPT_RIGHT_SIDE,
+ inspect(right),
+ equivalents=right_mapper._equivalent_columns,
+ )
+
+ # if an alias() on the right side was generated,
+ # which is intended to wrap a the right side in a subquery,
+ # ensure that columns retrieved from this target in the result
+ # set are also adapted.
+ self._mapper_loads_polymorphically_with(right_mapper, adapter)
+ elif (
+ not r_info.is_clause_element
+ and not right_is_aliased
+ and right_mapper._has_aliased_polymorphic_fromclause
+ ):
+ # for the case where the target mapper has a with_polymorphic
+ # set up, ensure an adapter is set up for criteria that works
+ # against this mapper. Previously, this logic used to
+ # use the "create_aliases or aliased_entity" case to generate
+ # an aliased() object, but this creates an alias that isn't
+ # strictly necessary.
+ # see test/orm/test_core_compilation.py
+ # ::RelNaturalAliasedJoinsTest::test_straight
+ # and similar
+ self._mapper_loads_polymorphically_with(
+ right_mapper,
+ ORMAdapter(
+ _TraceAdaptRole.WITH_POLYMORPHIC_ADAPTER_RIGHT_JOIN,
+ right_mapper,
+ selectable=right_mapper.selectable,
+ equivalents=right_mapper._equivalent_columns,
+ ),
+ )
+ # if the onclause is a ClauseElement, adapt it with any
+ # adapters that are in place right now
+ if isinstance(onclause, expression.ClauseElement):
+ current_adapter = self._get_current_adapter()
+ if current_adapter:
+ onclause = current_adapter(onclause, True)
+
+ # if joining on a MapperProperty path,
+ # track the path to prevent redundant joins
+ if prop:
+ self._already_joined_edges += ((left, right, prop.key),)
+
+ return inspect(right), right, onclause
+
+ @property
+ def _select_args(self):
+ return {
+ "limit_clause": self.select_statement._limit_clause,
+ "offset_clause": self.select_statement._offset_clause,
+ "distinct": self.distinct,
+ "distinct_on": self.distinct_on,
+ "prefixes": self.select_statement._prefixes,
+ "suffixes": self.select_statement._suffixes,
+ "group_by": self.group_by or None,
+ "fetch_clause": self.select_statement._fetch_clause,
+ "fetch_clause_options": (
+ self.select_statement._fetch_clause_options
+ ),
+ "independent_ctes": self.select_statement._independent_ctes,
+ "independent_ctes_opts": (
+ self.select_statement._independent_ctes_opts
+ ),
+ }
+
+ @property
+ def _should_nest_selectable(self):
+ kwargs = self._select_args
+ return (
+ kwargs.get("limit_clause") is not None
+ or kwargs.get("offset_clause") is not None
+ or kwargs.get("distinct", False)
+ or kwargs.get("distinct_on", ())
+ or kwargs.get("group_by", False)
+ )
+
+ def _get_extra_criteria(self, ext_info):
+ if (
+ "additional_entity_criteria",
+ ext_info.mapper,
+ ) in self.global_attributes:
+ return tuple(
+ ae._resolve_where_criteria(ext_info)
+ for ae in self.global_attributes[
+ ("additional_entity_criteria", ext_info.mapper)
+ ]
+ if (ae.include_aliases or ae.entity is ext_info)
+ and ae._should_include(self)
+ )
+ else:
+ return ()
+
+ def _adjust_for_extra_criteria(self):
+ """Apply extra criteria filtering.
+
+ For all distinct single-table-inheritance mappers represented in
+ the columns clause of this query, as well as the "select from entity",
+ add criterion to the WHERE
+ clause of the given QueryContext such that only the appropriate
+ subtypes are selected from the total results.
+
+ Additionally, add WHERE criteria originating from LoaderCriteriaOptions
+ associated with the global context.
+
+ """
+
+ for fromclause in self.from_clauses:
+ ext_info = fromclause._annotations.get("parententity", None)
+
+ if (
+ ext_info
+ and (
+ ext_info.mapper._single_table_criterion is not None
+ or ("additional_entity_criteria", ext_info.mapper)
+ in self.global_attributes
+ )
+ and ext_info not in self.extra_criteria_entities
+ ):
+ self.extra_criteria_entities[ext_info] = (
+ ext_info,
+ ext_info._adapter if ext_info.is_aliased_class else None,
+ )
+
+ search = set(self.extra_criteria_entities.values())
+
+ for ext_info, adapter in search:
+ if ext_info in self._join_entities:
+ continue
+
+ single_crit = ext_info.mapper._single_table_criterion
+
+ if self.compile_options._for_refresh_state:
+ additional_entity_criteria = []
+ else:
+ additional_entity_criteria = self._get_extra_criteria(ext_info)
+
+ if single_crit is not None:
+ additional_entity_criteria += (single_crit,)
+
+ current_adapter = self._get_current_adapter()
+ for crit in additional_entity_criteria:
+ if adapter:
+ crit = adapter.traverse(crit)
+
+ if current_adapter:
+ crit = sql_util._deep_annotate(crit, {"_orm_adapt": True})
+ crit = current_adapter(crit, False)
+ self._where_criteria += (crit,)
+
+
+def _column_descriptions(
+ query_or_select_stmt: Union[Query, Select, FromStatement],
+ compile_state: Optional[ORMSelectCompileState] = None,
+ legacy: bool = False,
+) -> List[ORMColumnDescription]:
+ if compile_state is None:
+ compile_state = ORMSelectCompileState._create_entities_collection(
+ query_or_select_stmt, legacy=legacy
+ )
+ ctx = compile_state
+ d = [
+ {
+ "name": ent._label_name,
+ "type": ent.type,
+ "aliased": getattr(insp_ent, "is_aliased_class", False),
+ "expr": ent.expr,
+ "entity": (
+ getattr(insp_ent, "entity", None)
+ if ent.entity_zero is not None
+ and not insp_ent.is_clause_element
+ else None
+ ),
+ }
+ for ent, insp_ent in [
+ (_ent, _ent.entity_zero) for _ent in ctx._entities
+ ]
+ ]
+ return d
+
+
+def _legacy_filter_by_entity_zero(
+ query_or_augmented_select: Union[Query[Any], Select[Any]]
+) -> Optional[_InternalEntityType[Any]]:
+ self = query_or_augmented_select
+ if self._setup_joins:
+ _last_joined_entity = self._last_joined_entity
+ if _last_joined_entity is not None:
+ return _last_joined_entity
+
+ if self._from_obj and "parententity" in self._from_obj[0]._annotations:
+ return self._from_obj[0]._annotations["parententity"]
+
+ return _entity_from_pre_ent_zero(self)
+
+
+def _entity_from_pre_ent_zero(
+ query_or_augmented_select: Union[Query[Any], Select[Any]]
+) -> Optional[_InternalEntityType[Any]]:
+ self = query_or_augmented_select
+ if not self._raw_columns:
+ return None
+
+ ent = self._raw_columns[0]
+
+ if "parententity" in ent._annotations:
+ return ent._annotations["parententity"]
+ elif isinstance(ent, ORMColumnsClauseRole):
+ return ent.entity
+ elif "bundle" in ent._annotations:
+ return ent._annotations["bundle"]
+ else:
+ return ent
+
+
+def _determine_last_joined_entity(
+ setup_joins: Tuple[_SetupJoinsElement, ...],
+ entity_zero: Optional[_InternalEntityType[Any]] = None,
+) -> Optional[Union[_InternalEntityType[Any], _JoinTargetElement]]:
+ if not setup_joins:
+ return None
+
+ (target, onclause, from_, flags) = setup_joins[-1]
+
+ if isinstance(
+ target,
+ attributes.QueryableAttribute,
+ ):
+ return target.entity
+ else:
+ return target
+
+
+class _QueryEntity:
+ """represent an entity column returned within a Query result."""
+
+ __slots__ = ()
+
+ supports_single_entity: bool
+
+ _non_hashable_value = False
+ _null_column_type = False
+ use_id_for_hash = False
+
+ _label_name: Optional[str]
+ type: Union[Type[Any], TypeEngine[Any]]
+ expr: Union[_InternalEntityType, ColumnElement[Any]]
+ entity_zero: Optional[_InternalEntityType]
+
+ def setup_compile_state(self, compile_state: ORMCompileState) -> None:
+ raise NotImplementedError()
+
+ def setup_dml_returning_compile_state(
+ self,
+ compile_state: ORMCompileState,
+ adapter: DMLReturningColFilter,
+ ) -> None:
+ raise NotImplementedError()
+
+ def row_processor(self, context, result):
+ raise NotImplementedError()
+
+ @classmethod
+ def to_compile_state(
+ cls, compile_state, entities, entities_collection, is_current_entities
+ ):
+ for idx, entity in enumerate(entities):
+ if entity._is_lambda_element:
+ if entity._is_sequence:
+ cls.to_compile_state(
+ compile_state,
+ entity._resolved,
+ entities_collection,
+ is_current_entities,
+ )
+ continue
+ else:
+ entity = entity._resolved
+
+ if entity.is_clause_element:
+ if entity.is_selectable:
+ if "parententity" in entity._annotations:
+ _MapperEntity(
+ compile_state,
+ entity,
+ entities_collection,
+ is_current_entities,
+ )
+ else:
+ _ColumnEntity._for_columns(
+ compile_state,
+ entity._select_iterable,
+ entities_collection,
+ idx,
+ is_current_entities,
+ )
+ else:
+ if entity._annotations.get("bundle", False):
+ _BundleEntity(
+ compile_state,
+ entity,
+ entities_collection,
+ is_current_entities,
+ )
+ elif entity._is_clause_list:
+ # this is legacy only - test_composites.py
+ # test_query_cols_legacy
+ _ColumnEntity._for_columns(
+ compile_state,
+ entity._select_iterable,
+ entities_collection,
+ idx,
+ is_current_entities,
+ )
+ else:
+ _ColumnEntity._for_columns(
+ compile_state,
+ [entity],
+ entities_collection,
+ idx,
+ is_current_entities,
+ )
+ elif entity.is_bundle:
+ _BundleEntity(compile_state, entity, entities_collection)
+
+ return entities_collection
+
+
+class _MapperEntity(_QueryEntity):
+ """mapper/class/AliasedClass entity"""
+
+ __slots__ = (
+ "expr",
+ "mapper",
+ "entity_zero",
+ "is_aliased_class",
+ "path",
+ "_extra_entities",
+ "_label_name",
+ "_with_polymorphic_mappers",
+ "selectable",
+ "_polymorphic_discriminator",
+ )
+
+ expr: _InternalEntityType
+ mapper: Mapper[Any]
+ entity_zero: _InternalEntityType
+ is_aliased_class: bool
+ path: PathRegistry
+ _label_name: str
+
+ def __init__(
+ self, compile_state, entity, entities_collection, is_current_entities
+ ):
+ entities_collection.append(self)
+ if is_current_entities:
+ if compile_state._primary_entity is None:
+ compile_state._primary_entity = self
+ compile_state._has_mapper_entities = True
+ compile_state._has_orm_entities = True
+
+ entity = entity._annotations["parententity"]
+ entity._post_inspect
+ ext_info = self.entity_zero = entity
+ entity = ext_info.entity
+
+ self.expr = entity
+ self.mapper = mapper = ext_info.mapper
+
+ self._extra_entities = (self.expr,)
+
+ if ext_info.is_aliased_class:
+ self._label_name = ext_info.name
+ else:
+ self._label_name = mapper.class_.__name__
+
+ self.is_aliased_class = ext_info.is_aliased_class
+ self.path = ext_info._path_registry
+
+ self.selectable = ext_info.selectable
+ self._with_polymorphic_mappers = ext_info.with_polymorphic_mappers
+ self._polymorphic_discriminator = ext_info.polymorphic_on
+
+ if mapper._should_select_with_poly_adapter:
+ compile_state._create_with_polymorphic_adapter(
+ ext_info, self.selectable
+ )
+
+ supports_single_entity = True
+
+ _non_hashable_value = True
+ use_id_for_hash = True
+
+ @property
+ def type(self):
+ return self.mapper.class_
+
+ @property
+ def entity_zero_or_selectable(self):
+ return self.entity_zero
+
+ def corresponds_to(self, entity):
+ return _entity_corresponds_to(self.entity_zero, entity)
+
+ def _get_entity_clauses(self, compile_state):
+ adapter = None
+
+ if not self.is_aliased_class:
+ if compile_state._polymorphic_adapters:
+ adapter = compile_state._polymorphic_adapters.get(
+ self.mapper, None
+ )
+ else:
+ adapter = self.entity_zero._adapter
+
+ if adapter:
+ if compile_state._from_obj_alias:
+ ret = adapter.wrap(compile_state._from_obj_alias)
+ else:
+ ret = adapter
+ else:
+ ret = compile_state._from_obj_alias
+
+ return ret
+
+ def row_processor(self, context, result):
+ compile_state = context.compile_state
+ adapter = self._get_entity_clauses(compile_state)
+
+ if compile_state.compound_eager_adapter and adapter:
+ adapter = adapter.wrap(compile_state.compound_eager_adapter)
+ elif not adapter:
+ adapter = compile_state.compound_eager_adapter
+
+ if compile_state._primary_entity is self:
+ only_load_props = compile_state.compile_options._only_load_props
+ refresh_state = context.refresh_state
+ else:
+ only_load_props = refresh_state = None
+
+ _instance = loading._instance_processor(
+ self,
+ self.mapper,
+ context,
+ result,
+ self.path,
+ adapter,
+ only_load_props=only_load_props,
+ refresh_state=refresh_state,
+ polymorphic_discriminator=self._polymorphic_discriminator,
+ )
+
+ return _instance, self._label_name, self._extra_entities
+
+ def setup_dml_returning_compile_state(
+ self,
+ compile_state: ORMCompileState,
+ adapter: DMLReturningColFilter,
+ ) -> None:
+ loading._setup_entity_query(
+ compile_state,
+ self.mapper,
+ self,
+ self.path,
+ adapter,
+ compile_state.primary_columns,
+ with_polymorphic=self._with_polymorphic_mappers,
+ only_load_props=compile_state.compile_options._only_load_props,
+ polymorphic_discriminator=self._polymorphic_discriminator,
+ )
+
+ def setup_compile_state(self, compile_state):
+ adapter = self._get_entity_clauses(compile_state)
+
+ single_table_crit = self.mapper._single_table_criterion
+ if (
+ single_table_crit is not None
+ or ("additional_entity_criteria", self.mapper)
+ in compile_state.global_attributes
+ ):
+ ext_info = self.entity_zero
+ compile_state.extra_criteria_entities[ext_info] = (
+ ext_info,
+ ext_info._adapter if ext_info.is_aliased_class else None,
+ )
+
+ loading._setup_entity_query(
+ compile_state,
+ self.mapper,
+ self,
+ self.path,
+ adapter,
+ compile_state.primary_columns,
+ with_polymorphic=self._with_polymorphic_mappers,
+ only_load_props=compile_state.compile_options._only_load_props,
+ polymorphic_discriminator=self._polymorphic_discriminator,
+ )
+ compile_state._fallback_from_clauses.append(self.selectable)
+
+
+class _BundleEntity(_QueryEntity):
+ _extra_entities = ()
+
+ __slots__ = (
+ "bundle",
+ "expr",
+ "type",
+ "_label_name",
+ "_entities",
+ "supports_single_entity",
+ )
+
+ _entities: List[_QueryEntity]
+ bundle: Bundle
+ type: Type[Any]
+ _label_name: str
+ supports_single_entity: bool
+ expr: Bundle
+
+ def __init__(
+ self,
+ compile_state,
+ expr,
+ entities_collection,
+ is_current_entities,
+ setup_entities=True,
+ parent_bundle=None,
+ ):
+ compile_state._has_orm_entities = True
+
+ expr = expr._annotations["bundle"]
+ if parent_bundle:
+ parent_bundle._entities.append(self)
+ else:
+ entities_collection.append(self)
+
+ if isinstance(
+ expr, (attributes.QueryableAttribute, interfaces.PropComparator)
+ ):
+ bundle = expr.__clause_element__()
+ else:
+ bundle = expr
+
+ self.bundle = self.expr = bundle
+ self.type = type(bundle)
+ self._label_name = bundle.name
+ self._entities = []
+
+ if setup_entities:
+ for expr in bundle.exprs:
+ if "bundle" in expr._annotations:
+ _BundleEntity(
+ compile_state,
+ expr,
+ entities_collection,
+ is_current_entities,
+ parent_bundle=self,
+ )
+ elif isinstance(expr, Bundle):
+ _BundleEntity(
+ compile_state,
+ expr,
+ entities_collection,
+ is_current_entities,
+ parent_bundle=self,
+ )
+ else:
+ _ORMColumnEntity._for_columns(
+ compile_state,
+ [expr],
+ entities_collection,
+ None,
+ is_current_entities,
+ parent_bundle=self,
+ )
+
+ self.supports_single_entity = self.bundle.single_entity
+
+ @property
+ def mapper(self):
+ ezero = self.entity_zero
+ if ezero is not None:
+ return ezero.mapper
+ else:
+ return None
+
+ @property
+ def entity_zero(self):
+ for ent in self._entities:
+ ezero = ent.entity_zero
+ if ezero is not None:
+ return ezero
+ else:
+ return None
+
+ def corresponds_to(self, entity):
+ # TODO: we might be able to implement this but for now
+ # we are working around it
+ return False
+
+ @property
+ def entity_zero_or_selectable(self):
+ for ent in self._entities:
+ ezero = ent.entity_zero_or_selectable
+ if ezero is not None:
+ return ezero
+ else:
+ return None
+
+ def setup_compile_state(self, compile_state):
+ for ent in self._entities:
+ ent.setup_compile_state(compile_state)
+
+ def setup_dml_returning_compile_state(
+ self,
+ compile_state: ORMCompileState,
+ adapter: DMLReturningColFilter,
+ ) -> None:
+ return self.setup_compile_state(compile_state)
+
+ def row_processor(self, context, result):
+ procs, labels, extra = zip(
+ *[ent.row_processor(context, result) for ent in self._entities]
+ )
+
+ proc = self.bundle.create_row_processor(context.query, procs, labels)
+
+ return proc, self._label_name, self._extra_entities
+
+
+class _ColumnEntity(_QueryEntity):
+ __slots__ = (
+ "_fetch_column",
+ "_row_processor",
+ "raw_column_index",
+ "translate_raw_column",
+ )
+
+ @classmethod
+ def _for_columns(
+ cls,
+ compile_state,
+ columns,
+ entities_collection,
+ raw_column_index,
+ is_current_entities,
+ parent_bundle=None,
+ ):
+ for column in columns:
+ annotations = column._annotations
+ if "parententity" in annotations:
+ _entity = annotations["parententity"]
+ else:
+ _entity = sql_util.extract_first_column_annotation(
+ column, "parententity"
+ )
+
+ if _entity:
+ if "identity_token" in column._annotations:
+ _IdentityTokenEntity(
+ compile_state,
+ column,
+ entities_collection,
+ _entity,
+ raw_column_index,
+ is_current_entities,
+ parent_bundle=parent_bundle,
+ )
+ else:
+ _ORMColumnEntity(
+ compile_state,
+ column,
+ entities_collection,
+ _entity,
+ raw_column_index,
+ is_current_entities,
+ parent_bundle=parent_bundle,
+ )
+ else:
+ _RawColumnEntity(
+ compile_state,
+ column,
+ entities_collection,
+ raw_column_index,
+ is_current_entities,
+ parent_bundle=parent_bundle,
+ )
+
+ @property
+ def type(self):
+ return self.column.type
+
+ @property
+ def _non_hashable_value(self):
+ return not self.column.type.hashable
+
+ @property
+ def _null_column_type(self):
+ return self.column.type._isnull
+
+ def row_processor(self, context, result):
+ compile_state = context.compile_state
+
+ # the resulting callable is entirely cacheable so just return
+ # it if we already made one
+ if self._row_processor is not None:
+ getter, label_name, extra_entities = self._row_processor
+ if self.translate_raw_column:
+ extra_entities += (
+ context.query._raw_columns[self.raw_column_index],
+ )
+
+ return getter, label_name, extra_entities
+
+ # retrieve the column that would have been set up in
+ # setup_compile_state, to avoid doing redundant work
+ if self._fetch_column is not None:
+ column = self._fetch_column
+ else:
+ # fetch_column will be None when we are doing a from_statement
+ # and setup_compile_state may not have been called.
+ column = self.column
+
+ # previously, the RawColumnEntity didn't look for from_obj_alias
+ # however I can't think of a case where we would be here and
+ # we'd want to ignore it if this is the from_statement use case.
+ # it's not really a use case to have raw columns + from_statement
+ if compile_state._from_obj_alias:
+ column = compile_state._from_obj_alias.columns[column]
+
+ if column._annotations:
+ # annotated columns perform more slowly in compiler and
+ # result due to the __eq__() method, so use deannotated
+ column = column._deannotate()
+
+ if compile_state.compound_eager_adapter:
+ column = compile_state.compound_eager_adapter.columns[column]
+
+ getter = result._getter(column)
+ ret = getter, self._label_name, self._extra_entities
+ self._row_processor = ret
+
+ if self.translate_raw_column:
+ extra_entities = self._extra_entities + (
+ context.query._raw_columns[self.raw_column_index],
+ )
+ return getter, self._label_name, extra_entities
+ else:
+ return ret
+
+
+class _RawColumnEntity(_ColumnEntity):
+ entity_zero = None
+ mapper = None
+ supports_single_entity = False
+
+ __slots__ = (
+ "expr",
+ "column",
+ "_label_name",
+ "entity_zero_or_selectable",
+ "_extra_entities",
+ )
+
+ def __init__(
+ self,
+ compile_state,
+ column,
+ entities_collection,
+ raw_column_index,
+ is_current_entities,
+ parent_bundle=None,
+ ):
+ self.expr = column
+ self.raw_column_index = raw_column_index
+ self.translate_raw_column = raw_column_index is not None
+
+ if column._is_star:
+ compile_state.compile_options += {"_is_star": True}
+
+ if not is_current_entities or column._is_text_clause:
+ self._label_name = None
+ else:
+ self._label_name = compile_state._label_convention(column)
+
+ if parent_bundle:
+ parent_bundle._entities.append(self)
+ else:
+ entities_collection.append(self)
+
+ self.column = column
+ self.entity_zero_or_selectable = (
+ self.column._from_objects[0] if self.column._from_objects else None
+ )
+ self._extra_entities = (self.expr, self.column)
+ self._fetch_column = self._row_processor = None
+
+ def corresponds_to(self, entity):
+ return False
+
+ def setup_dml_returning_compile_state(
+ self,
+ compile_state: ORMCompileState,
+ adapter: DMLReturningColFilter,
+ ) -> None:
+ return self.setup_compile_state(compile_state)
+
+ def setup_compile_state(self, compile_state):
+ current_adapter = compile_state._get_current_adapter()
+ if current_adapter:
+ column = current_adapter(self.column, False)
+ if column is None:
+ return
+ else:
+ column = self.column
+
+ if column._annotations:
+ # annotated columns perform more slowly in compiler and
+ # result due to the __eq__() method, so use deannotated
+ column = column._deannotate()
+
+ compile_state.dedupe_columns.add(column)
+ compile_state.primary_columns.append(column)
+ self._fetch_column = column
+
+
+class _ORMColumnEntity(_ColumnEntity):
+ """Column/expression based entity."""
+
+ supports_single_entity = False
+
+ __slots__ = (
+ "expr",
+ "mapper",
+ "column",
+ "_label_name",
+ "entity_zero_or_selectable",
+ "entity_zero",
+ "_extra_entities",
+ )
+
+ def __init__(
+ self,
+ compile_state,
+ column,
+ entities_collection,
+ parententity,
+ raw_column_index,
+ is_current_entities,
+ parent_bundle=None,
+ ):
+ annotations = column._annotations
+
+ _entity = parententity
+
+ # an AliasedClass won't have proxy_key in the annotations for
+ # a column if it was acquired using the class' adapter directly,
+ # such as using AliasedInsp._adapt_element(). this occurs
+ # within internal loaders.
+
+ orm_key = annotations.get("proxy_key", None)
+ proxy_owner = annotations.get("proxy_owner", _entity)
+ if orm_key:
+ self.expr = getattr(proxy_owner.entity, orm_key)
+ self.translate_raw_column = False
+ else:
+ # if orm_key is not present, that means this is an ad-hoc
+ # SQL ColumnElement, like a CASE() or other expression.
+ # include this column position from the invoked statement
+ # in the ORM-level ResultSetMetaData on each execute, so that
+ # it can be targeted by identity after caching
+ self.expr = column
+ self.translate_raw_column = raw_column_index is not None
+
+ self.raw_column_index = raw_column_index
+
+ if is_current_entities:
+ self._label_name = compile_state._label_convention(
+ column, col_name=orm_key
+ )
+ else:
+ self._label_name = None
+
+ _entity._post_inspect
+ self.entity_zero = self.entity_zero_or_selectable = ezero = _entity
+ self.mapper = mapper = _entity.mapper
+
+ if parent_bundle:
+ parent_bundle._entities.append(self)
+ else:
+ entities_collection.append(self)
+
+ compile_state._has_orm_entities = True
+
+ self.column = column
+
+ self._fetch_column = self._row_processor = None
+
+ self._extra_entities = (self.expr, self.column)
+
+ if mapper._should_select_with_poly_adapter:
+ compile_state._create_with_polymorphic_adapter(
+ ezero, ezero.selectable
+ )
+
+ def corresponds_to(self, entity):
+ if _is_aliased_class(entity):
+ # TODO: polymorphic subclasses ?
+ return entity is self.entity_zero
+ else:
+ return not _is_aliased_class(
+ self.entity_zero
+ ) and entity.common_parent(self.entity_zero)
+
+ def setup_dml_returning_compile_state(
+ self,
+ compile_state: ORMCompileState,
+ adapter: DMLReturningColFilter,
+ ) -> None:
+ self._fetch_column = self.column
+ column = adapter(self.column, False)
+ if column is not None:
+ compile_state.dedupe_columns.add(column)
+ compile_state.primary_columns.append(column)
+
+ def setup_compile_state(self, compile_state):
+ current_adapter = compile_state._get_current_adapter()
+ if current_adapter:
+ column = current_adapter(self.column, False)
+ if column is None:
+ assert compile_state.is_dml_returning
+ self._fetch_column = self.column
+ return
+ else:
+ column = self.column
+
+ ezero = self.entity_zero
+
+ single_table_crit = self.mapper._single_table_criterion
+ if (
+ single_table_crit is not None
+ or ("additional_entity_criteria", self.mapper)
+ in compile_state.global_attributes
+ ):
+ compile_state.extra_criteria_entities[ezero] = (
+ ezero,
+ ezero._adapter if ezero.is_aliased_class else None,
+ )
+
+ if column._annotations and not column._expression_label:
+ # annotated columns perform more slowly in compiler and
+ # result due to the __eq__() method, so use deannotated
+ column = column._deannotate()
+
+ # use entity_zero as the from if we have it. this is necessary
+ # for polymorphic scenarios where our FROM is based on ORM entity,
+ # not the FROM of the column. but also, don't use it if our column
+ # doesn't actually have any FROMs that line up, such as when its
+ # a scalar subquery.
+ if set(self.column._from_objects).intersection(
+ ezero.selectable._from_objects
+ ):
+ compile_state._fallback_from_clauses.append(ezero.selectable)
+
+ compile_state.dedupe_columns.add(column)
+ compile_state.primary_columns.append(column)
+ self._fetch_column = column
+
+
+class _IdentityTokenEntity(_ORMColumnEntity):
+ translate_raw_column = False
+
+ def setup_compile_state(self, compile_state):
+ pass
+
+ def row_processor(self, context, result):
+ def getter(row):
+ return context.load_options._identity_token
+
+ return getter, self._label_name, self._extra_entities
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_api.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_api.py
new file mode 100644
index 0000000..09128ea
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_api.py
@@ -0,0 +1,1875 @@
+# orm/decl_api.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""Public API functions and helpers for declarative."""
+
+from __future__ import annotations
+
+import itertools
+import re
+import typing
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import ClassVar
+from typing import Dict
+from typing import FrozenSet
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import Mapping
+from typing import Optional
+from typing import overload
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import attributes
+from . import clsregistry
+from . import instrumentation
+from . import interfaces
+from . import mapperlib
+from ._orm_constructors import composite
+from ._orm_constructors import deferred
+from ._orm_constructors import mapped_column
+from ._orm_constructors import relationship
+from ._orm_constructors import synonym
+from .attributes import InstrumentedAttribute
+from .base import _inspect_mapped_class
+from .base import _is_mapped_class
+from .base import Mapped
+from .base import ORMDescriptor
+from .decl_base import _add_attribute
+from .decl_base import _as_declarative
+from .decl_base import _ClassScanMapperConfig
+from .decl_base import _declarative_constructor
+from .decl_base import _DeferredMapperConfig
+from .decl_base import _del_attribute
+from .decl_base import _mapper
+from .descriptor_props import Composite
+from .descriptor_props import Synonym
+from .descriptor_props import Synonym as _orm_synonym
+from .mapper import Mapper
+from .properties import MappedColumn
+from .relationships import RelationshipProperty
+from .state import InstanceState
+from .. import exc
+from .. import inspection
+from .. import util
+from ..sql import sqltypes
+from ..sql.base import _NoArg
+from ..sql.elements import SQLCoreOperations
+from ..sql.schema import MetaData
+from ..sql.selectable import FromClause
+from ..util import hybridmethod
+from ..util import hybridproperty
+from ..util import typing as compat_typing
+from ..util.typing import CallableReference
+from ..util.typing import flatten_newtype
+from ..util.typing import is_generic
+from ..util.typing import is_literal
+from ..util.typing import is_newtype
+from ..util.typing import is_pep695
+from ..util.typing import Literal
+from ..util.typing import Self
+
+if TYPE_CHECKING:
+ from ._typing import _O
+ from ._typing import _RegistryType
+ from .decl_base import _DataclassArguments
+ from .instrumentation import ClassManager
+ from .interfaces import MapperProperty
+ from .state import InstanceState # noqa
+ from ..sql._typing import _TypeEngineArgument
+ from ..sql.type_api import _MatchedOnType
+
+_T = TypeVar("_T", bound=Any)
+
+_TT = TypeVar("_TT", bound=Any)
+
+# it's not clear how to have Annotated, Union objects etc. as keys here
+# from a typing perspective so just leave it open ended for now
+_TypeAnnotationMapType = Mapping[Any, "_TypeEngineArgument[Any]"]
+_MutableTypeAnnotationMapType = Dict[Any, "_TypeEngineArgument[Any]"]
+
+_DeclaredAttrDecorated = Callable[
+ ..., Union[Mapped[_T], ORMDescriptor[_T], SQLCoreOperations[_T]]
+]
+
+
+def has_inherited_table(cls: Type[_O]) -> bool:
+ """Given a class, return True if any of the classes it inherits from has a
+ mapped table, otherwise return False.
+
+ This is used in declarative mixins to build attributes that behave
+ differently for the base class vs. a subclass in an inheritance
+ hierarchy.
+
+ .. seealso::
+
+ :ref:`decl_mixin_inheritance`
+
+ """
+ for class_ in cls.__mro__[1:]:
+ if getattr(class_, "__table__", None) is not None:
+ return True
+ return False
+
+
+class _DynamicAttributesType(type):
+ def __setattr__(cls, key: str, value: Any) -> None:
+ if "__mapper__" in cls.__dict__:
+ _add_attribute(cls, key, value)
+ else:
+ type.__setattr__(cls, key, value)
+
+ def __delattr__(cls, key: str) -> None:
+ if "__mapper__" in cls.__dict__:
+ _del_attribute(cls, key)
+ else:
+ type.__delattr__(cls, key)
+
+
+class DeclarativeAttributeIntercept(
+ _DynamicAttributesType,
+ # Inspectable is used only by the mypy plugin
+ inspection.Inspectable[Mapper[Any]],
+):
+ """Metaclass that may be used in conjunction with the
+ :class:`_orm.DeclarativeBase` class to support addition of class
+ attributes dynamically.
+
+ """
+
+
+@compat_typing.dataclass_transform(
+ field_specifiers=(
+ MappedColumn,
+ RelationshipProperty,
+ Composite,
+ Synonym,
+ mapped_column,
+ relationship,
+ composite,
+ synonym,
+ deferred,
+ ),
+)
+class DCTransformDeclarative(DeclarativeAttributeIntercept):
+ """metaclass that includes @dataclass_transforms"""
+
+
+class DeclarativeMeta(DeclarativeAttributeIntercept):
+ metadata: MetaData
+ registry: RegistryType
+
+ def __init__(
+ cls, classname: Any, bases: Any, dict_: Any, **kw: Any
+ ) -> None:
+ # use cls.__dict__, which can be modified by an
+ # __init_subclass__() method (#7900)
+ dict_ = cls.__dict__
+
+ # early-consume registry from the initial declarative base,
+ # assign privately to not conflict with subclass attributes named
+ # "registry"
+ reg = getattr(cls, "_sa_registry", None)
+ if reg is None:
+ reg = dict_.get("registry", None)
+ if not isinstance(reg, registry):
+ raise exc.InvalidRequestError(
+ "Declarative base class has no 'registry' attribute, "
+ "or registry is not a sqlalchemy.orm.registry() object"
+ )
+ else:
+ cls._sa_registry = reg
+
+ if not cls.__dict__.get("__abstract__", False):
+ _as_declarative(reg, cls, dict_)
+ type.__init__(cls, classname, bases, dict_)
+
+
+def synonym_for(
+ name: str, map_column: bool = False
+) -> Callable[[Callable[..., Any]], Synonym[Any]]:
+ """Decorator that produces an :func:`_orm.synonym`
+ attribute in conjunction with a Python descriptor.
+
+ The function being decorated is passed to :func:`_orm.synonym` as the
+ :paramref:`.orm.synonym.descriptor` parameter::
+
+ class MyClass(Base):
+ __tablename__ = 'my_table'
+
+ id = Column(Integer, primary_key=True)
+ _job_status = Column("job_status", String(50))
+
+ @synonym_for("job_status")
+ @property
+ def job_status(self):
+ return "Status: %s" % self._job_status
+
+ The :ref:`hybrid properties <mapper_hybrids>` feature of SQLAlchemy
+ is typically preferred instead of synonyms, which is a more legacy
+ feature.
+
+ .. seealso::
+
+ :ref:`synonyms` - Overview of synonyms
+
+ :func:`_orm.synonym` - the mapper-level function
+
+ :ref:`mapper_hybrids` - The Hybrid Attribute extension provides an
+ updated approach to augmenting attribute behavior more flexibly than
+ can be achieved with synonyms.
+
+ """
+
+ def decorate(fn: Callable[..., Any]) -> Synonym[Any]:
+ return _orm_synonym(name, map_column=map_column, descriptor=fn)
+
+ return decorate
+
+
+class _declared_attr_common:
+ def __init__(
+ self,
+ fn: Callable[..., Any],
+ cascading: bool = False,
+ quiet: bool = False,
+ ):
+ # suppport
+ # @declared_attr
+ # @classmethod
+ # def foo(cls) -> Mapped[thing]:
+ # ...
+ # which seems to help typing tools interpret the fn as a classmethod
+ # for situations where needed
+ if isinstance(fn, classmethod):
+ fn = fn.__func__
+
+ self.fget = fn
+ self._cascading = cascading
+ self._quiet = quiet
+ self.__doc__ = fn.__doc__
+
+ def _collect_return_annotation(self) -> Optional[Type[Any]]:
+ return util.get_annotations(self.fget).get("return")
+
+ def __get__(self, instance: Optional[object], owner: Any) -> Any:
+ # the declared_attr needs to make use of a cache that exists
+ # for the span of the declarative scan_attributes() phase.
+ # to achieve this we look at the class manager that's configured.
+
+ # note this method should not be called outside of the declarative
+ # setup phase
+
+ cls = owner
+ manager = attributes.opt_manager_of_class(cls)
+ if manager is None:
+ if not re.match(r"^__.+__$", self.fget.__name__):
+ # if there is no manager at all, then this class hasn't been
+ # run through declarative or mapper() at all, emit a warning.
+ util.warn(
+ "Unmanaged access of declarative attribute %s from "
+ "non-mapped class %s" % (self.fget.__name__, cls.__name__)
+ )
+ return self.fget(cls)
+ elif manager.is_mapped:
+ # the class is mapped, which means we're outside of the declarative
+ # scan setup, just run the function.
+ return self.fget(cls)
+
+ # here, we are inside of the declarative scan. use the registry
+ # that is tracking the values of these attributes.
+ declarative_scan = manager.declarative_scan()
+
+ # assert that we are in fact in the declarative scan
+ assert declarative_scan is not None
+
+ reg = declarative_scan.declared_attr_reg
+
+ if self in reg:
+ return reg[self]
+ else:
+ reg[self] = obj = self.fget(cls)
+ return obj
+
+
+class _declared_directive(_declared_attr_common, Generic[_T]):
+ # see mapping_api.rst for docstring
+
+ if typing.TYPE_CHECKING:
+
+ def __init__(
+ self,
+ fn: Callable[..., _T],
+ cascading: bool = False,
+ ): ...
+
+ def __get__(self, instance: Optional[object], owner: Any) -> _T: ...
+
+ def __set__(self, instance: Any, value: Any) -> None: ...
+
+ def __delete__(self, instance: Any) -> None: ...
+
+ def __call__(self, fn: Callable[..., _TT]) -> _declared_directive[_TT]:
+ # extensive fooling of mypy underway...
+ ...
+
+
+class declared_attr(interfaces._MappedAttribute[_T], _declared_attr_common):
+ """Mark a class-level method as representing the definition of
+ a mapped property or Declarative directive.
+
+ :class:`_orm.declared_attr` is typically applied as a decorator to a class
+ level method, turning the attribute into a scalar-like property that can be
+ invoked from the uninstantiated class. The Declarative mapping process
+ looks for these :class:`_orm.declared_attr` callables as it scans classes,
+ and assumes any attribute marked with :class:`_orm.declared_attr` will be a
+ callable that will produce an object specific to the Declarative mapping or
+ table configuration.
+
+ :class:`_orm.declared_attr` is usually applicable to
+ :ref:`mixins <orm_mixins_toplevel>`, to define relationships that are to be
+ applied to different implementors of the class. It may also be used to
+ define dynamically generated column expressions and other Declarative
+ attributes.
+
+ Example::
+
+ class ProvidesUserMixin:
+ "A mixin that adds a 'user' relationship to classes."
+
+ user_id: Mapped[int] = mapped_column(ForeignKey("user_table.id"))
+
+ @declared_attr
+ def user(cls) -> Mapped["User"]:
+ return relationship("User")
+
+ When used with Declarative directives such as ``__tablename__``, the
+ :meth:`_orm.declared_attr.directive` modifier may be used which indicates
+ to :pep:`484` typing tools that the given method is not dealing with
+ :class:`_orm.Mapped` attributes::
+
+ class CreateTableName:
+ @declared_attr.directive
+ def __tablename__(cls) -> str:
+ return cls.__name__.lower()
+
+ :class:`_orm.declared_attr` can also be applied directly to mapped
+ classes, to allow for attributes that dynamically configure themselves
+ on subclasses when using mapped inheritance schemes. Below
+ illustrates :class:`_orm.declared_attr` to create a dynamic scheme
+ for generating the :paramref:`_orm.Mapper.polymorphic_identity` parameter
+ for subclasses::
+
+ class Employee(Base):
+ __tablename__ = 'employee'
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ type: Mapped[str] = mapped_column(String(50))
+
+ @declared_attr.directive
+ def __mapper_args__(cls) -> Dict[str, Any]:
+ if cls.__name__ == 'Employee':
+ return {
+ "polymorphic_on":cls.type,
+ "polymorphic_identity":"Employee"
+ }
+ else:
+ return {"polymorphic_identity":cls.__name__}
+
+ class Engineer(Employee):
+ pass
+
+ :class:`_orm.declared_attr` supports decorating functions that are
+ explicitly decorated with ``@classmethod``. This is never necessary from a
+ runtime perspective, however may be needed in order to support :pep:`484`
+ typing tools that don't otherwise recognize the decorated function as
+ having class-level behaviors for the ``cls`` parameter::
+
+ class SomethingMixin:
+ x: Mapped[int]
+ y: Mapped[int]
+
+ @declared_attr
+ @classmethod
+ def x_plus_y(cls) -> Mapped[int]:
+ return column_property(cls.x + cls.y)
+
+ .. versionadded:: 2.0 - :class:`_orm.declared_attr` can accommodate a
+ function decorated with ``@classmethod`` to help with :pep:`484`
+ integration where needed.
+
+
+ .. seealso::
+
+ :ref:`orm_mixins_toplevel` - Declarative Mixin documentation with
+ background on use patterns for :class:`_orm.declared_attr`.
+
+ """ # noqa: E501
+
+ if typing.TYPE_CHECKING:
+
+ def __init__(
+ self,
+ fn: _DeclaredAttrDecorated[_T],
+ cascading: bool = False,
+ ): ...
+
+ def __set__(self, instance: Any, value: Any) -> None: ...
+
+ def __delete__(self, instance: Any) -> None: ...
+
+ # this is the Mapped[] API where at class descriptor get time we want
+ # the type checker to see InstrumentedAttribute[_T]. However the
+ # callable function prior to mapping in fact calls the given
+ # declarative function that does not return InstrumentedAttribute
+ @overload
+ def __get__(
+ self, instance: None, owner: Any
+ ) -> InstrumentedAttribute[_T]: ...
+
+ @overload
+ def __get__(self, instance: object, owner: Any) -> _T: ...
+
+ def __get__(
+ self, instance: Optional[object], owner: Any
+ ) -> Union[InstrumentedAttribute[_T], _T]: ...
+
+ @hybridmethod
+ def _stateful(cls, **kw: Any) -> _stateful_declared_attr[_T]:
+ return _stateful_declared_attr(**kw)
+
+ @hybridproperty
+ def directive(cls) -> _declared_directive[Any]:
+ # see mapping_api.rst for docstring
+ return _declared_directive # type: ignore
+
+ @hybridproperty
+ def cascading(cls) -> _stateful_declared_attr[_T]:
+ # see mapping_api.rst for docstring
+ return cls._stateful(cascading=True)
+
+
+class _stateful_declared_attr(declared_attr[_T]):
+ kw: Dict[str, Any]
+
+ def __init__(self, **kw: Any):
+ self.kw = kw
+
+ @hybridmethod
+ def _stateful(self, **kw: Any) -> _stateful_declared_attr[_T]:
+ new_kw = self.kw.copy()
+ new_kw.update(kw)
+ return _stateful_declared_attr(**new_kw)
+
+ def __call__(self, fn: _DeclaredAttrDecorated[_T]) -> declared_attr[_T]:
+ return declared_attr(fn, **self.kw)
+
+
+def declarative_mixin(cls: Type[_T]) -> Type[_T]:
+ """Mark a class as providing the feature of "declarative mixin".
+
+ E.g.::
+
+ from sqlalchemy.orm import declared_attr
+ from sqlalchemy.orm import declarative_mixin
+
+ @declarative_mixin
+ class MyMixin:
+
+ @declared_attr
+ def __tablename__(cls):
+ return cls.__name__.lower()
+
+ __table_args__ = {'mysql_engine': 'InnoDB'}
+ __mapper_args__= {'always_refresh': True}
+
+ id = Column(Integer, primary_key=True)
+
+ class MyModel(MyMixin, Base):
+ name = Column(String(1000))
+
+ The :func:`_orm.declarative_mixin` decorator currently does not modify
+ the given class in any way; it's current purpose is strictly to assist
+ the :ref:`Mypy plugin <mypy_toplevel>` in being able to identify
+ SQLAlchemy declarative mixin classes when no other context is present.
+
+ .. versionadded:: 1.4.6
+
+ .. seealso::
+
+ :ref:`orm_mixins_toplevel`
+
+ :ref:`mypy_declarative_mixins` - in the
+ :ref:`Mypy plugin documentation <mypy_toplevel>`
+
+ """ # noqa: E501
+
+ return cls
+
+
+def _setup_declarative_base(cls: Type[Any]) -> None:
+ if "metadata" in cls.__dict__:
+ metadata = cls.__dict__["metadata"]
+ else:
+ metadata = None
+
+ if "type_annotation_map" in cls.__dict__:
+ type_annotation_map = cls.__dict__["type_annotation_map"]
+ else:
+ type_annotation_map = None
+
+ reg = cls.__dict__.get("registry", None)
+ if reg is not None:
+ if not isinstance(reg, registry):
+ raise exc.InvalidRequestError(
+ "Declarative base class has a 'registry' attribute that is "
+ "not an instance of sqlalchemy.orm.registry()"
+ )
+ elif type_annotation_map is not None:
+ raise exc.InvalidRequestError(
+ "Declarative base class has both a 'registry' attribute and a "
+ "type_annotation_map entry. Per-base type_annotation_maps "
+ "are not supported. Please apply the type_annotation_map "
+ "to this registry directly."
+ )
+
+ else:
+ reg = registry(
+ metadata=metadata, type_annotation_map=type_annotation_map
+ )
+ cls.registry = reg
+
+ cls._sa_registry = reg
+
+ if "metadata" not in cls.__dict__:
+ cls.metadata = cls.registry.metadata
+
+ if getattr(cls, "__init__", object.__init__) is object.__init__:
+ cls.__init__ = cls.registry.constructor
+
+
+class MappedAsDataclass(metaclass=DCTransformDeclarative):
+ """Mixin class to indicate when mapping this class, also convert it to be
+ a dataclass.
+
+ .. seealso::
+
+ :ref:`orm_declarative_native_dataclasses` - complete background
+ on SQLAlchemy native dataclass mapping
+
+ .. versionadded:: 2.0
+
+ """
+
+ def __init_subclass__(
+ cls,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ eq: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ order: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ unsafe_hash: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ match_args: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ dataclass_callable: Union[
+ _NoArg, Callable[..., Type[Any]]
+ ] = _NoArg.NO_ARG,
+ **kw: Any,
+ ) -> None:
+ apply_dc_transforms: _DataclassArguments = {
+ "init": init,
+ "repr": repr,
+ "eq": eq,
+ "order": order,
+ "unsafe_hash": unsafe_hash,
+ "match_args": match_args,
+ "kw_only": kw_only,
+ "dataclass_callable": dataclass_callable,
+ }
+
+ current_transforms: _DataclassArguments
+
+ if hasattr(cls, "_sa_apply_dc_transforms"):
+ current = cls._sa_apply_dc_transforms
+
+ _ClassScanMapperConfig._assert_dc_arguments(current)
+
+ cls._sa_apply_dc_transforms = current_transforms = { # type: ignore # noqa: E501
+ k: current.get(k, _NoArg.NO_ARG) if v is _NoArg.NO_ARG else v
+ for k, v in apply_dc_transforms.items()
+ }
+ else:
+ cls._sa_apply_dc_transforms = current_transforms = (
+ apply_dc_transforms
+ )
+
+ super().__init_subclass__(**kw)
+
+ if not _is_mapped_class(cls):
+ new_anno = (
+ _ClassScanMapperConfig._update_annotations_for_non_mapped_class
+ )(cls)
+ _ClassScanMapperConfig._apply_dataclasses_to_any_class(
+ current_transforms, cls, new_anno
+ )
+
+
+class DeclarativeBase(
+ # Inspectable is used only by the mypy plugin
+ inspection.Inspectable[InstanceState[Any]],
+ metaclass=DeclarativeAttributeIntercept,
+):
+ """Base class used for declarative class definitions.
+
+ The :class:`_orm.DeclarativeBase` allows for the creation of new
+ declarative bases in such a way that is compatible with type checkers::
+
+
+ from sqlalchemy.orm import DeclarativeBase
+
+ class Base(DeclarativeBase):
+ pass
+
+
+ The above ``Base`` class is now usable as the base for new declarative
+ mappings. The superclass makes use of the ``__init_subclass__()``
+ method to set up new classes and metaclasses aren't used.
+
+ When first used, the :class:`_orm.DeclarativeBase` class instantiates a new
+ :class:`_orm.registry` to be used with the base, assuming one was not
+ provided explicitly. The :class:`_orm.DeclarativeBase` class supports
+ class-level attributes which act as parameters for the construction of this
+ registry; such as to indicate a specific :class:`_schema.MetaData`
+ collection as well as a specific value for
+ :paramref:`_orm.registry.type_annotation_map`::
+
+ from typing_extensions import Annotated
+
+ from sqlalchemy import BigInteger
+ from sqlalchemy import MetaData
+ from sqlalchemy import String
+ from sqlalchemy.orm import DeclarativeBase
+
+ bigint = Annotated[int, "bigint"]
+ my_metadata = MetaData()
+
+ class Base(DeclarativeBase):
+ metadata = my_metadata
+ type_annotation_map = {
+ str: String().with_variant(String(255), "mysql", "mariadb"),
+ bigint: BigInteger()
+ }
+
+ Class-level attributes which may be specified include:
+
+ :param metadata: optional :class:`_schema.MetaData` collection.
+ If a :class:`_orm.registry` is constructed automatically, this
+ :class:`_schema.MetaData` collection will be used to construct it.
+ Otherwise, the local :class:`_schema.MetaData` collection will supercede
+ that used by an existing :class:`_orm.registry` passed using the
+ :paramref:`_orm.DeclarativeBase.registry` parameter.
+ :param type_annotation_map: optional type annotation map that will be
+ passed to the :class:`_orm.registry` as
+ :paramref:`_orm.registry.type_annotation_map`.
+ :param registry: supply a pre-existing :class:`_orm.registry` directly.
+
+ .. versionadded:: 2.0 Added :class:`.DeclarativeBase`, so that declarative
+ base classes may be constructed in such a way that is also recognized
+ by :pep:`484` type checkers. As a result, :class:`.DeclarativeBase`
+ and other subclassing-oriented APIs should be seen as
+ superseding previous "class returned by a function" APIs, namely
+ :func:`_orm.declarative_base` and :meth:`_orm.registry.generate_base`,
+ where the base class returned cannot be recognized by type checkers
+ without using plugins.
+
+ **__init__ behavior**
+
+ In a plain Python class, the base-most ``__init__()`` method in the class
+ hierarchy is ``object.__init__()``, which accepts no arguments. However,
+ when the :class:`_orm.DeclarativeBase` subclass is first declared, the
+ class is given an ``__init__()`` method that links to the
+ :paramref:`_orm.registry.constructor` constructor function, if no
+ ``__init__()`` method is already present; this is the usual declarative
+ constructor that will assign keyword arguments as attributes on the
+ instance, assuming those attributes are established at the class level
+ (i.e. are mapped, or are linked to a descriptor). This constructor is
+ **never accessed by a mapped class without being called explicitly via
+ super()**, as mapped classes are themselves given an ``__init__()`` method
+ directly which calls :paramref:`_orm.registry.constructor`, so in the
+ default case works independently of what the base-most ``__init__()``
+ method does.
+
+ .. versionchanged:: 2.0.1 :class:`_orm.DeclarativeBase` has a default
+ constructor that links to :paramref:`_orm.registry.constructor` by
+ default, so that calls to ``super().__init__()`` can access this
+ constructor. Previously, due to an implementation mistake, this default
+ constructor was missing, and calling ``super().__init__()`` would invoke
+ ``object.__init__()``.
+
+ The :class:`_orm.DeclarativeBase` subclass may also declare an explicit
+ ``__init__()`` method which will replace the use of the
+ :paramref:`_orm.registry.constructor` function at this level::
+
+ class Base(DeclarativeBase):
+ def __init__(self, id=None):
+ self.id = id
+
+ Mapped classes still will not invoke this constructor implicitly; it
+ remains only accessible by calling ``super().__init__()``::
+
+ class MyClass(Base):
+ def __init__(self, id=None, name=None):
+ self.name = name
+ super().__init__(id=id)
+
+ Note that this is a different behavior from what functions like the legacy
+ :func:`_orm.declarative_base` would do; the base created by those functions
+ would always install :paramref:`_orm.registry.constructor` for
+ ``__init__()``.
+
+
+ """
+
+ if typing.TYPE_CHECKING:
+
+ def _sa_inspect_type(self) -> Mapper[Self]: ...
+
+ def _sa_inspect_instance(self) -> InstanceState[Self]: ...
+
+ _sa_registry: ClassVar[_RegistryType]
+
+ registry: ClassVar[_RegistryType]
+ """Refers to the :class:`_orm.registry` in use where new
+ :class:`_orm.Mapper` objects will be associated."""
+
+ metadata: ClassVar[MetaData]
+ """Refers to the :class:`_schema.MetaData` collection that will be used
+ for new :class:`_schema.Table` objects.
+
+ .. seealso::
+
+ :ref:`orm_declarative_metadata`
+
+ """
+
+ __name__: ClassVar[str]
+
+ # this ideally should be Mapper[Self], but mypy as of 1.4.1 does not
+ # like it, and breaks the declared_attr_one test. Pyright/pylance is
+ # ok with it.
+ __mapper__: ClassVar[Mapper[Any]]
+ """The :class:`_orm.Mapper` object to which a particular class is
+ mapped.
+
+ May also be acquired using :func:`_sa.inspect`, e.g.
+ ``inspect(klass)``.
+
+ """
+
+ __table__: ClassVar[FromClause]
+ """The :class:`_sql.FromClause` to which a particular subclass is
+ mapped.
+
+ This is usually an instance of :class:`_schema.Table` but may also
+ refer to other kinds of :class:`_sql.FromClause` such as
+ :class:`_sql.Subquery`, depending on how the class is mapped.
+
+ .. seealso::
+
+ :ref:`orm_declarative_metadata`
+
+ """
+
+ # pyright/pylance do not consider a classmethod a ClassVar so use Any
+ # https://github.com/microsoft/pylance-release/issues/3484
+ __tablename__: Any
+ """String name to assign to the generated
+ :class:`_schema.Table` object, if not specified directly via
+ :attr:`_orm.DeclarativeBase.__table__`.
+
+ .. seealso::
+
+ :ref:`orm_declarative_table`
+
+ """
+
+ __mapper_args__: Any
+ """Dictionary of arguments which will be passed to the
+ :class:`_orm.Mapper` constructor.
+
+ .. seealso::
+
+ :ref:`orm_declarative_mapper_options`
+
+ """
+
+ __table_args__: Any
+ """A dictionary or tuple of arguments that will be passed to the
+ :class:`_schema.Table` constructor. See
+ :ref:`orm_declarative_table_configuration`
+ for background on the specific structure of this collection.
+
+ .. seealso::
+
+ :ref:`orm_declarative_table_configuration`
+
+ """
+
+ def __init__(self, **kw: Any): ...
+
+ def __init_subclass__(cls, **kw: Any) -> None:
+ if DeclarativeBase in cls.__bases__:
+ _check_not_declarative(cls, DeclarativeBase)
+ _setup_declarative_base(cls)
+ else:
+ _as_declarative(cls._sa_registry, cls, cls.__dict__)
+ super().__init_subclass__(**kw)
+
+
+def _check_not_declarative(cls: Type[Any], base: Type[Any]) -> None:
+ cls_dict = cls.__dict__
+ if (
+ "__table__" in cls_dict
+ and not (
+ callable(cls_dict["__table__"])
+ or hasattr(cls_dict["__table__"], "__get__")
+ )
+ ) or isinstance(cls_dict.get("__tablename__", None), str):
+ raise exc.InvalidRequestError(
+ f"Cannot use {base.__name__!r} directly as a declarative base "
+ "class. Create a Base by creating a subclass of it."
+ )
+
+
+class DeclarativeBaseNoMeta(
+ # Inspectable is used only by the mypy plugin
+ inspection.Inspectable[InstanceState[Any]]
+):
+ """Same as :class:`_orm.DeclarativeBase`, but does not use a metaclass
+ to intercept new attributes.
+
+ The :class:`_orm.DeclarativeBaseNoMeta` base may be used when use of
+ custom metaclasses is desirable.
+
+ .. versionadded:: 2.0
+
+
+ """
+
+ _sa_registry: ClassVar[_RegistryType]
+
+ registry: ClassVar[_RegistryType]
+ """Refers to the :class:`_orm.registry` in use where new
+ :class:`_orm.Mapper` objects will be associated."""
+
+ metadata: ClassVar[MetaData]
+ """Refers to the :class:`_schema.MetaData` collection that will be used
+ for new :class:`_schema.Table` objects.
+
+ .. seealso::
+
+ :ref:`orm_declarative_metadata`
+
+ """
+
+ # this ideally should be Mapper[Self], but mypy as of 1.4.1 does not
+ # like it, and breaks the declared_attr_one test. Pyright/pylance is
+ # ok with it.
+ __mapper__: ClassVar[Mapper[Any]]
+ """The :class:`_orm.Mapper` object to which a particular class is
+ mapped.
+
+ May also be acquired using :func:`_sa.inspect`, e.g.
+ ``inspect(klass)``.
+
+ """
+
+ __table__: Optional[FromClause]
+ """The :class:`_sql.FromClause` to which a particular subclass is
+ mapped.
+
+ This is usually an instance of :class:`_schema.Table` but may also
+ refer to other kinds of :class:`_sql.FromClause` such as
+ :class:`_sql.Subquery`, depending on how the class is mapped.
+
+ .. seealso::
+
+ :ref:`orm_declarative_metadata`
+
+ """
+
+ if typing.TYPE_CHECKING:
+
+ def _sa_inspect_type(self) -> Mapper[Self]: ...
+
+ def _sa_inspect_instance(self) -> InstanceState[Self]: ...
+
+ __tablename__: Any
+ """String name to assign to the generated
+ :class:`_schema.Table` object, if not specified directly via
+ :attr:`_orm.DeclarativeBase.__table__`.
+
+ .. seealso::
+
+ :ref:`orm_declarative_table`
+
+ """
+
+ __mapper_args__: Any
+ """Dictionary of arguments which will be passed to the
+ :class:`_orm.Mapper` constructor.
+
+ .. seealso::
+
+ :ref:`orm_declarative_mapper_options`
+
+ """
+
+ __table_args__: Any
+ """A dictionary or tuple of arguments that will be passed to the
+ :class:`_schema.Table` constructor. See
+ :ref:`orm_declarative_table_configuration`
+ for background on the specific structure of this collection.
+
+ .. seealso::
+
+ :ref:`orm_declarative_table_configuration`
+
+ """
+
+ def __init__(self, **kw: Any): ...
+
+ def __init_subclass__(cls, **kw: Any) -> None:
+ if DeclarativeBaseNoMeta in cls.__bases__:
+ _check_not_declarative(cls, DeclarativeBaseNoMeta)
+ _setup_declarative_base(cls)
+ else:
+ _as_declarative(cls._sa_registry, cls, cls.__dict__)
+ super().__init_subclass__(**kw)
+
+
+def add_mapped_attribute(
+ target: Type[_O], key: str, attr: MapperProperty[Any]
+) -> None:
+ """Add a new mapped attribute to an ORM mapped class.
+
+ E.g.::
+
+ add_mapped_attribute(User, "addresses", relationship(Address))
+
+ This may be used for ORM mappings that aren't using a declarative
+ metaclass that intercepts attribute set operations.
+
+ .. versionadded:: 2.0
+
+
+ """
+ _add_attribute(target, key, attr)
+
+
+def declarative_base(
+ *,
+ metadata: Optional[MetaData] = None,
+ mapper: Optional[Callable[..., Mapper[Any]]] = None,
+ cls: Type[Any] = object,
+ name: str = "Base",
+ class_registry: Optional[clsregistry._ClsRegistryType] = None,
+ type_annotation_map: Optional[_TypeAnnotationMapType] = None,
+ constructor: Callable[..., None] = _declarative_constructor,
+ metaclass: Type[Any] = DeclarativeMeta,
+) -> Any:
+ r"""Construct a base class for declarative class definitions.
+
+ The new base class will be given a metaclass that produces
+ appropriate :class:`~sqlalchemy.schema.Table` objects and makes
+ the appropriate :class:`_orm.Mapper` calls based on the
+ information provided declaratively in the class and any subclasses
+ of the class.
+
+ .. versionchanged:: 2.0 Note that the :func:`_orm.declarative_base`
+ function is superseded by the new :class:`_orm.DeclarativeBase` class,
+ which generates a new "base" class using subclassing, rather than
+ return value of a function. This allows an approach that is compatible
+ with :pep:`484` typing tools.
+
+ The :func:`_orm.declarative_base` function is a shorthand version
+ of using the :meth:`_orm.registry.generate_base`
+ method. That is, the following::
+
+ from sqlalchemy.orm import declarative_base
+
+ Base = declarative_base()
+
+ Is equivalent to::
+
+ from sqlalchemy.orm import registry
+
+ mapper_registry = registry()
+ Base = mapper_registry.generate_base()
+
+ See the docstring for :class:`_orm.registry`
+ and :meth:`_orm.registry.generate_base`
+ for more details.
+
+ .. versionchanged:: 1.4 The :func:`_orm.declarative_base`
+ function is now a specialization of the more generic
+ :class:`_orm.registry` class. The function also moves to the
+ ``sqlalchemy.orm`` package from the ``declarative.ext`` package.
+
+
+ :param metadata:
+ An optional :class:`~sqlalchemy.schema.MetaData` instance. All
+ :class:`~sqlalchemy.schema.Table` objects implicitly declared by
+ subclasses of the base will share this MetaData. A MetaData instance
+ will be created if none is provided. The
+ :class:`~sqlalchemy.schema.MetaData` instance will be available via the
+ ``metadata`` attribute of the generated declarative base class.
+
+ :param mapper:
+ An optional callable, defaults to :class:`_orm.Mapper`. Will
+ be used to map subclasses to their Tables.
+
+ :param cls:
+ Defaults to :class:`object`. A type to use as the base for the generated
+ declarative base class. May be a class or tuple of classes.
+
+ :param name:
+ Defaults to ``Base``. The display name for the generated
+ class. Customizing this is not required, but can improve clarity in
+ tracebacks and debugging.
+
+ :param constructor:
+ Specify the implementation for the ``__init__`` function on a mapped
+ class that has no ``__init__`` of its own. Defaults to an
+ implementation that assigns \**kwargs for declared
+ fields and relationships to an instance. If ``None`` is supplied,
+ no __init__ will be provided and construction will fall back to
+ cls.__init__ by way of the normal Python semantics.
+
+ :param class_registry: optional dictionary that will serve as the
+ registry of class names-> mapped classes when string names
+ are used to identify classes inside of :func:`_orm.relationship`
+ and others. Allows two or more declarative base classes
+ to share the same registry of class names for simplified
+ inter-base relationships.
+
+ :param type_annotation_map: optional dictionary of Python types to
+ SQLAlchemy :class:`_types.TypeEngine` classes or instances. This
+ is used exclusively by the :class:`_orm.MappedColumn` construct
+ to produce column types based on annotations within the
+ :class:`_orm.Mapped` type.
+
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`orm_declarative_mapped_column_type_map`
+
+ :param metaclass:
+ Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__
+ compatible callable to use as the meta type of the generated
+ declarative base class.
+
+ .. seealso::
+
+ :class:`_orm.registry`
+
+ """
+
+ return registry(
+ metadata=metadata,
+ class_registry=class_registry,
+ constructor=constructor,
+ type_annotation_map=type_annotation_map,
+ ).generate_base(
+ mapper=mapper,
+ cls=cls,
+ name=name,
+ metaclass=metaclass,
+ )
+
+
+class registry:
+ """Generalized registry for mapping classes.
+
+ The :class:`_orm.registry` serves as the basis for maintaining a collection
+ of mappings, and provides configurational hooks used to map classes.
+
+ The three general kinds of mappings supported are Declarative Base,
+ Declarative Decorator, and Imperative Mapping. All of these mapping
+ styles may be used interchangeably:
+
+ * :meth:`_orm.registry.generate_base` returns a new declarative base
+ class, and is the underlying implementation of the
+ :func:`_orm.declarative_base` function.
+
+ * :meth:`_orm.registry.mapped` provides a class decorator that will
+ apply declarative mapping to a class without the use of a declarative
+ base class.
+
+ * :meth:`_orm.registry.map_imperatively` will produce a
+ :class:`_orm.Mapper` for a class without scanning the class for
+ declarative class attributes. This method suits the use case historically
+ provided by the ``sqlalchemy.orm.mapper()`` classical mapping function,
+ which is removed as of SQLAlchemy 2.0.
+
+ .. versionadded:: 1.4
+
+ .. seealso::
+
+ :ref:`orm_mapping_classes_toplevel` - overview of class mapping
+ styles.
+
+ """
+
+ _class_registry: clsregistry._ClsRegistryType
+ _managers: weakref.WeakKeyDictionary[ClassManager[Any], Literal[True]]
+ _non_primary_mappers: weakref.WeakKeyDictionary[Mapper[Any], Literal[True]]
+ metadata: MetaData
+ constructor: CallableReference[Callable[..., None]]
+ type_annotation_map: _MutableTypeAnnotationMapType
+ _dependents: Set[_RegistryType]
+ _dependencies: Set[_RegistryType]
+ _new_mappers: bool
+
+ def __init__(
+ self,
+ *,
+ metadata: Optional[MetaData] = None,
+ class_registry: Optional[clsregistry._ClsRegistryType] = None,
+ type_annotation_map: Optional[_TypeAnnotationMapType] = None,
+ constructor: Callable[..., None] = _declarative_constructor,
+ ):
+ r"""Construct a new :class:`_orm.registry`
+
+ :param metadata:
+ An optional :class:`_schema.MetaData` instance. All
+ :class:`_schema.Table` objects generated using declarative
+ table mapping will make use of this :class:`_schema.MetaData`
+ collection. If this argument is left at its default of ``None``,
+ a blank :class:`_schema.MetaData` collection is created.
+
+ :param constructor:
+ Specify the implementation for the ``__init__`` function on a mapped
+ class that has no ``__init__`` of its own. Defaults to an
+ implementation that assigns \**kwargs for declared
+ fields and relationships to an instance. If ``None`` is supplied,
+ no __init__ will be provided and construction will fall back to
+ cls.__init__ by way of the normal Python semantics.
+
+ :param class_registry: optional dictionary that will serve as the
+ registry of class names-> mapped classes when string names
+ are used to identify classes inside of :func:`_orm.relationship`
+ and others. Allows two or more declarative base classes
+ to share the same registry of class names for simplified
+ inter-base relationships.
+
+ :param type_annotation_map: optional dictionary of Python types to
+ SQLAlchemy :class:`_types.TypeEngine` classes or instances.
+ The provided dict will update the default type mapping. This
+ is used exclusively by the :class:`_orm.MappedColumn` construct
+ to produce column types based on annotations within the
+ :class:`_orm.Mapped` type.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`orm_declarative_mapped_column_type_map`
+
+
+ """
+ lcl_metadata = metadata or MetaData()
+
+ if class_registry is None:
+ class_registry = weakref.WeakValueDictionary()
+
+ self._class_registry = class_registry
+ self._managers = weakref.WeakKeyDictionary()
+ self._non_primary_mappers = weakref.WeakKeyDictionary()
+ self.metadata = lcl_metadata
+ self.constructor = constructor
+ self.type_annotation_map = {}
+ if type_annotation_map is not None:
+ self.update_type_annotation_map(type_annotation_map)
+ self._dependents = set()
+ self._dependencies = set()
+
+ self._new_mappers = False
+
+ with mapperlib._CONFIGURE_MUTEX:
+ mapperlib._mapper_registries[self] = True
+
+ def update_type_annotation_map(
+ self,
+ type_annotation_map: _TypeAnnotationMapType,
+ ) -> None:
+ """update the :paramref:`_orm.registry.type_annotation_map` with new
+ values."""
+
+ self.type_annotation_map.update(
+ {
+ sub_type: sqltype
+ for typ, sqltype in type_annotation_map.items()
+ for sub_type in compat_typing.expand_unions(
+ typ, include_union=True, discard_none=True
+ )
+ }
+ )
+
+ def _resolve_type(
+ self, python_type: _MatchedOnType
+ ) -> Optional[sqltypes.TypeEngine[Any]]:
+ search: Iterable[Tuple[_MatchedOnType, Type[Any]]]
+ python_type_type: Type[Any]
+
+ if is_generic(python_type):
+ if is_literal(python_type):
+ python_type_type = cast("Type[Any]", python_type)
+
+ search = ( # type: ignore[assignment]
+ (python_type, python_type_type),
+ (Literal, python_type_type),
+ )
+ else:
+ python_type_type = python_type.__origin__
+ search = ((python_type, python_type_type),)
+ elif is_newtype(python_type):
+ python_type_type = flatten_newtype(python_type)
+ search = ((python_type, python_type_type),)
+ elif is_pep695(python_type):
+ python_type_type = python_type.__value__
+ flattened = None
+ search = ((python_type, python_type_type),)
+ else:
+ python_type_type = cast("Type[Any]", python_type)
+ flattened = None
+ search = ((pt, pt) for pt in python_type_type.__mro__)
+
+ for pt, flattened in search:
+ # we search through full __mro__ for types. however...
+ sql_type = self.type_annotation_map.get(pt)
+ if sql_type is None:
+ sql_type = sqltypes._type_map_get(pt) # type: ignore # noqa: E501
+
+ if sql_type is not None:
+ sql_type_inst = sqltypes.to_instance(sql_type)
+
+ # ... this additional step will reject most
+ # type -> supertype matches, such as if we had
+ # a MyInt(int) subclass. note also we pass NewType()
+ # here directly; these always have to be in the
+ # type_annotation_map to be useful
+ resolved_sql_type = sql_type_inst._resolve_for_python_type(
+ python_type_type,
+ pt,
+ flattened,
+ )
+ if resolved_sql_type is not None:
+ return resolved_sql_type
+
+ return None
+
+ @property
+ def mappers(self) -> FrozenSet[Mapper[Any]]:
+ """read only collection of all :class:`_orm.Mapper` objects."""
+
+ return frozenset(manager.mapper for manager in self._managers).union(
+ self._non_primary_mappers
+ )
+
+ def _set_depends_on(self, registry: RegistryType) -> None:
+ if registry is self:
+ return
+ registry._dependents.add(self)
+ self._dependencies.add(registry)
+
+ def _flag_new_mapper(self, mapper: Mapper[Any]) -> None:
+ mapper._ready_for_configure = True
+ if self._new_mappers:
+ return
+
+ for reg in self._recurse_with_dependents({self}):
+ reg._new_mappers = True
+
+ @classmethod
+ def _recurse_with_dependents(
+ cls, registries: Set[RegistryType]
+ ) -> Iterator[RegistryType]:
+ todo = registries
+ done = set()
+ while todo:
+ reg = todo.pop()
+ done.add(reg)
+
+ # if yielding would remove dependents, make sure we have
+ # them before
+ todo.update(reg._dependents.difference(done))
+ yield reg
+
+ # if yielding would add dependents, make sure we have them
+ # after
+ todo.update(reg._dependents.difference(done))
+
+ @classmethod
+ def _recurse_with_dependencies(
+ cls, registries: Set[RegistryType]
+ ) -> Iterator[RegistryType]:
+ todo = registries
+ done = set()
+ while todo:
+ reg = todo.pop()
+ done.add(reg)
+
+ # if yielding would remove dependencies, make sure we have
+ # them before
+ todo.update(reg._dependencies.difference(done))
+
+ yield reg
+
+ # if yielding would remove dependencies, make sure we have
+ # them before
+ todo.update(reg._dependencies.difference(done))
+
+ def _mappers_to_configure(self) -> Iterator[Mapper[Any]]:
+ return itertools.chain(
+ (
+ manager.mapper
+ for manager in list(self._managers)
+ if manager.is_mapped
+ and not manager.mapper.configured
+ and manager.mapper._ready_for_configure
+ ),
+ (
+ npm
+ for npm in list(self._non_primary_mappers)
+ if not npm.configured and npm._ready_for_configure
+ ),
+ )
+
+ def _add_non_primary_mapper(self, np_mapper: Mapper[Any]) -> None:
+ self._non_primary_mappers[np_mapper] = True
+
+ def _dispose_cls(self, cls: Type[_O]) -> None:
+ clsregistry.remove_class(cls.__name__, cls, self._class_registry)
+
+ def _add_manager(self, manager: ClassManager[Any]) -> None:
+ self._managers[manager] = True
+ if manager.is_mapped:
+ raise exc.ArgumentError(
+ "Class '%s' already has a primary mapper defined. "
+ % manager.class_
+ )
+ assert manager.registry is None
+ manager.registry = self
+
+ def configure(self, cascade: bool = False) -> None:
+ """Configure all as-yet unconfigured mappers in this
+ :class:`_orm.registry`.
+
+ The configure step is used to reconcile and initialize the
+ :func:`_orm.relationship` linkages between mapped classes, as well as
+ to invoke configuration events such as the
+ :meth:`_orm.MapperEvents.before_configured` and
+ :meth:`_orm.MapperEvents.after_configured`, which may be used by ORM
+ extensions or user-defined extension hooks.
+
+ If one or more mappers in this registry contain
+ :func:`_orm.relationship` constructs that refer to mapped classes in
+ other registries, this registry is said to be *dependent* on those
+ registries. In order to configure those dependent registries
+ automatically, the :paramref:`_orm.registry.configure.cascade` flag
+ should be set to ``True``. Otherwise, if they are not configured, an
+ exception will be raised. The rationale behind this behavior is to
+ allow an application to programmatically invoke configuration of
+ registries while controlling whether or not the process implicitly
+ reaches other registries.
+
+ As an alternative to invoking :meth:`_orm.registry.configure`, the ORM
+ function :func:`_orm.configure_mappers` function may be used to ensure
+ configuration is complete for all :class:`_orm.registry` objects in
+ memory. This is generally simpler to use and also predates the usage of
+ :class:`_orm.registry` objects overall. However, this function will
+ impact all mappings throughout the running Python process and may be
+ more memory/time consuming for an application that has many registries
+ in use for different purposes that may not be needed immediately.
+
+ .. seealso::
+
+ :func:`_orm.configure_mappers`
+
+
+ .. versionadded:: 1.4.0b2
+
+ """
+ mapperlib._configure_registries({self}, cascade=cascade)
+
+ def dispose(self, cascade: bool = False) -> None:
+ """Dispose of all mappers in this :class:`_orm.registry`.
+
+ After invocation, all the classes that were mapped within this registry
+ will no longer have class instrumentation associated with them. This
+ method is the per-:class:`_orm.registry` analogue to the
+ application-wide :func:`_orm.clear_mappers` function.
+
+ If this registry contains mappers that are dependencies of other
+ registries, typically via :func:`_orm.relationship` links, then those
+ registries must be disposed as well. When such registries exist in
+ relation to this one, their :meth:`_orm.registry.dispose` method will
+ also be called, if the :paramref:`_orm.registry.dispose.cascade` flag
+ is set to ``True``; otherwise, an error is raised if those registries
+ were not already disposed.
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :func:`_orm.clear_mappers`
+
+ """
+
+ mapperlib._dispose_registries({self}, cascade=cascade)
+
+ def _dispose_manager_and_mapper(self, manager: ClassManager[Any]) -> None:
+ if "mapper" in manager.__dict__:
+ mapper = manager.mapper
+
+ mapper._set_dispose_flags()
+
+ class_ = manager.class_
+ self._dispose_cls(class_)
+ instrumentation._instrumentation_factory.unregister(class_)
+
+ def generate_base(
+ self,
+ mapper: Optional[Callable[..., Mapper[Any]]] = None,
+ cls: Type[Any] = object,
+ name: str = "Base",
+ metaclass: Type[Any] = DeclarativeMeta,
+ ) -> Any:
+ """Generate a declarative base class.
+
+ Classes that inherit from the returned class object will be
+ automatically mapped using declarative mapping.
+
+ E.g.::
+
+ from sqlalchemy.orm import registry
+
+ mapper_registry = registry()
+
+ Base = mapper_registry.generate_base()
+
+ class MyClass(Base):
+ __tablename__ = "my_table"
+ id = Column(Integer, primary_key=True)
+
+ The above dynamically generated class is equivalent to the
+ non-dynamic example below::
+
+ from sqlalchemy.orm import registry
+ from sqlalchemy.orm.decl_api import DeclarativeMeta
+
+ mapper_registry = registry()
+
+ class Base(metaclass=DeclarativeMeta):
+ __abstract__ = True
+ registry = mapper_registry
+ metadata = mapper_registry.metadata
+
+ __init__ = mapper_registry.constructor
+
+ .. versionchanged:: 2.0 Note that the
+ :meth:`_orm.registry.generate_base` method is superseded by the new
+ :class:`_orm.DeclarativeBase` class, which generates a new "base"
+ class using subclassing, rather than return value of a function.
+ This allows an approach that is compatible with :pep:`484` typing
+ tools.
+
+ The :meth:`_orm.registry.generate_base` method provides the
+ implementation for the :func:`_orm.declarative_base` function, which
+ creates the :class:`_orm.registry` and base class all at once.
+
+ See the section :ref:`orm_declarative_mapping` for background and
+ examples.
+
+ :param mapper:
+ An optional callable, defaults to :class:`_orm.Mapper`.
+ This function is used to generate new :class:`_orm.Mapper` objects.
+
+ :param cls:
+ Defaults to :class:`object`. A type to use as the base for the
+ generated declarative base class. May be a class or tuple of classes.
+
+ :param name:
+ Defaults to ``Base``. The display name for the generated
+ class. Customizing this is not required, but can improve clarity in
+ tracebacks and debugging.
+
+ :param metaclass:
+ Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__
+ compatible callable to use as the meta type of the generated
+ declarative base class.
+
+ .. seealso::
+
+ :ref:`orm_declarative_mapping`
+
+ :func:`_orm.declarative_base`
+
+ """
+ metadata = self.metadata
+
+ bases = not isinstance(cls, tuple) and (cls,) or cls
+
+ class_dict: Dict[str, Any] = dict(registry=self, metadata=metadata)
+ if isinstance(cls, type):
+ class_dict["__doc__"] = cls.__doc__
+
+ if self.constructor is not None:
+ class_dict["__init__"] = self.constructor
+
+ class_dict["__abstract__"] = True
+ if mapper:
+ class_dict["__mapper_cls__"] = mapper
+
+ if hasattr(cls, "__class_getitem__"):
+
+ def __class_getitem__(cls: Type[_T], key: Any) -> Type[_T]:
+ # allow generic classes in py3.9+
+ return cls
+
+ class_dict["__class_getitem__"] = __class_getitem__
+
+ return metaclass(name, bases, class_dict)
+
+ @compat_typing.dataclass_transform(
+ field_specifiers=(
+ MappedColumn,
+ RelationshipProperty,
+ Composite,
+ Synonym,
+ mapped_column,
+ relationship,
+ composite,
+ synonym,
+ deferred,
+ ),
+ )
+ @overload
+ def mapped_as_dataclass(self, __cls: Type[_O]) -> Type[_O]: ...
+
+ @overload
+ def mapped_as_dataclass(
+ self,
+ __cls: Literal[None] = ...,
+ *,
+ init: Union[_NoArg, bool] = ...,
+ repr: Union[_NoArg, bool] = ..., # noqa: A002
+ eq: Union[_NoArg, bool] = ...,
+ order: Union[_NoArg, bool] = ...,
+ unsafe_hash: Union[_NoArg, bool] = ...,
+ match_args: Union[_NoArg, bool] = ...,
+ kw_only: Union[_NoArg, bool] = ...,
+ dataclass_callable: Union[_NoArg, Callable[..., Type[Any]]] = ...,
+ ) -> Callable[[Type[_O]], Type[_O]]: ...
+
+ def mapped_as_dataclass(
+ self,
+ __cls: Optional[Type[_O]] = None,
+ *,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ eq: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ order: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ unsafe_hash: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ match_args: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ dataclass_callable: Union[
+ _NoArg, Callable[..., Type[Any]]
+ ] = _NoArg.NO_ARG,
+ ) -> Union[Type[_O], Callable[[Type[_O]], Type[_O]]]:
+ """Class decorator that will apply the Declarative mapping process
+ to a given class, and additionally convert the class to be a
+ Python dataclass.
+
+ .. seealso::
+
+ :ref:`orm_declarative_native_dataclasses` - complete background
+ on SQLAlchemy native dataclass mapping
+
+
+ .. versionadded:: 2.0
+
+
+ """
+
+ def decorate(cls: Type[_O]) -> Type[_O]:
+ setattr(
+ cls,
+ "_sa_apply_dc_transforms",
+ {
+ "init": init,
+ "repr": repr,
+ "eq": eq,
+ "order": order,
+ "unsafe_hash": unsafe_hash,
+ "match_args": match_args,
+ "kw_only": kw_only,
+ "dataclass_callable": dataclass_callable,
+ },
+ )
+ _as_declarative(self, cls, cls.__dict__)
+ return cls
+
+ if __cls:
+ return decorate(__cls)
+ else:
+ return decorate
+
+ def mapped(self, cls: Type[_O]) -> Type[_O]:
+ """Class decorator that will apply the Declarative mapping process
+ to a given class.
+
+ E.g.::
+
+ from sqlalchemy.orm import registry
+
+ mapper_registry = registry()
+
+ @mapper_registry.mapped
+ class Foo:
+ __tablename__ = 'some_table'
+
+ id = Column(Integer, primary_key=True)
+ name = Column(String)
+
+ See the section :ref:`orm_declarative_mapping` for complete
+ details and examples.
+
+ :param cls: class to be mapped.
+
+ :return: the class that was passed.
+
+ .. seealso::
+
+ :ref:`orm_declarative_mapping`
+
+ :meth:`_orm.registry.generate_base` - generates a base class
+ that will apply Declarative mapping to subclasses automatically
+ using a Python metaclass.
+
+ .. seealso::
+
+ :meth:`_orm.registry.mapped_as_dataclass`
+
+ """
+ _as_declarative(self, cls, cls.__dict__)
+ return cls
+
+ def as_declarative_base(self, **kw: Any) -> Callable[[Type[_T]], Type[_T]]:
+ """
+ Class decorator which will invoke
+ :meth:`_orm.registry.generate_base`
+ for a given base class.
+
+ E.g.::
+
+ from sqlalchemy.orm import registry
+
+ mapper_registry = registry()
+
+ @mapper_registry.as_declarative_base()
+ class Base:
+ @declared_attr
+ def __tablename__(cls):
+ return cls.__name__.lower()
+ id = Column(Integer, primary_key=True)
+
+ class MyMappedClass(Base):
+ # ...
+
+ All keyword arguments passed to
+ :meth:`_orm.registry.as_declarative_base` are passed
+ along to :meth:`_orm.registry.generate_base`.
+
+ """
+
+ def decorate(cls: Type[_T]) -> Type[_T]:
+ kw["cls"] = cls
+ kw["name"] = cls.__name__
+ return self.generate_base(**kw) # type: ignore
+
+ return decorate
+
+ def map_declaratively(self, cls: Type[_O]) -> Mapper[_O]:
+ """Map a class declaratively.
+
+ In this form of mapping, the class is scanned for mapping information,
+ including for columns to be associated with a table, and/or an
+ actual table object.
+
+ Returns the :class:`_orm.Mapper` object.
+
+ E.g.::
+
+ from sqlalchemy.orm import registry
+
+ mapper_registry = registry()
+
+ class Foo:
+ __tablename__ = 'some_table'
+
+ id = Column(Integer, primary_key=True)
+ name = Column(String)
+
+ mapper = mapper_registry.map_declaratively(Foo)
+
+ This function is more conveniently invoked indirectly via either the
+ :meth:`_orm.registry.mapped` class decorator or by subclassing a
+ declarative metaclass generated from
+ :meth:`_orm.registry.generate_base`.
+
+ See the section :ref:`orm_declarative_mapping` for complete
+ details and examples.
+
+ :param cls: class to be mapped.
+
+ :return: a :class:`_orm.Mapper` object.
+
+ .. seealso::
+
+ :ref:`orm_declarative_mapping`
+
+ :meth:`_orm.registry.mapped` - more common decorator interface
+ to this function.
+
+ :meth:`_orm.registry.map_imperatively`
+
+ """
+ _as_declarative(self, cls, cls.__dict__)
+ return cls.__mapper__ # type: ignore
+
+ def map_imperatively(
+ self,
+ class_: Type[_O],
+ local_table: Optional[FromClause] = None,
+ **kw: Any,
+ ) -> Mapper[_O]:
+ r"""Map a class imperatively.
+
+ In this form of mapping, the class is not scanned for any mapping
+ information. Instead, all mapping constructs are passed as
+ arguments.
+
+ This method is intended to be fully equivalent to the now-removed
+ SQLAlchemy ``mapper()`` function, except that it's in terms of
+ a particular registry.
+
+ E.g.::
+
+ from sqlalchemy.orm import registry
+
+ mapper_registry = registry()
+
+ my_table = Table(
+ "my_table",
+ mapper_registry.metadata,
+ Column('id', Integer, primary_key=True)
+ )
+
+ class MyClass:
+ pass
+
+ mapper_registry.map_imperatively(MyClass, my_table)
+
+ See the section :ref:`orm_imperative_mapping` for complete background
+ and usage examples.
+
+ :param class\_: The class to be mapped. Corresponds to the
+ :paramref:`_orm.Mapper.class_` parameter.
+
+ :param local_table: the :class:`_schema.Table` or other
+ :class:`_sql.FromClause` object that is the subject of the mapping.
+ Corresponds to the
+ :paramref:`_orm.Mapper.local_table` parameter.
+
+ :param \**kw: all other keyword arguments are passed to the
+ :class:`_orm.Mapper` constructor directly.
+
+ .. seealso::
+
+ :ref:`orm_imperative_mapping`
+
+ :ref:`orm_declarative_mapping`
+
+ """
+ return _mapper(self, class_, local_table, kw)
+
+
+RegistryType = registry
+
+if not TYPE_CHECKING:
+ # allow for runtime type resolution of ``ClassVar[_RegistryType]``
+ _RegistryType = registry # noqa
+
+
+def as_declarative(**kw: Any) -> Callable[[Type[_T]], Type[_T]]:
+ """
+ Class decorator which will adapt a given class into a
+ :func:`_orm.declarative_base`.
+
+ This function makes use of the :meth:`_orm.registry.as_declarative_base`
+ method, by first creating a :class:`_orm.registry` automatically
+ and then invoking the decorator.
+
+ E.g.::
+
+ from sqlalchemy.orm import as_declarative
+
+ @as_declarative()
+ class Base:
+ @declared_attr
+ def __tablename__(cls):
+ return cls.__name__.lower()
+ id = Column(Integer, primary_key=True)
+
+ class MyMappedClass(Base):
+ # ...
+
+ .. seealso::
+
+ :meth:`_orm.registry.as_declarative_base`
+
+ """
+ metadata, class_registry = (
+ kw.pop("metadata", None),
+ kw.pop("class_registry", None),
+ )
+
+ return registry(
+ metadata=metadata, class_registry=class_registry
+ ).as_declarative_base(**kw)
+
+
+@inspection._inspects(
+ DeclarativeMeta, DeclarativeBase, DeclarativeAttributeIntercept
+)
+def _inspect_decl_meta(cls: Type[Any]) -> Optional[Mapper[Any]]:
+ mp: Optional[Mapper[Any]] = _inspect_mapped_class(cls)
+ if mp is None:
+ if _DeferredMapperConfig.has_cls(cls):
+ _DeferredMapperConfig.raise_unmapped_for_cls(cls)
+ return mp
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py
new file mode 100644
index 0000000..96530c3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py
@@ -0,0 +1,2152 @@
+# orm/decl_base.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""Internal implementation for declarative."""
+
+from __future__ import annotations
+
+import collections
+import dataclasses
+import re
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Mapping
+from typing import NamedTuple
+from typing import NoReturn
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import attributes
+from . import clsregistry
+from . import exc as orm_exc
+from . import instrumentation
+from . import mapperlib
+from ._typing import _O
+from ._typing import attr_is_internal_proxy
+from .attributes import InstrumentedAttribute
+from .attributes import QueryableAttribute
+from .base import _is_mapped_class
+from .base import InspectionAttr
+from .descriptor_props import CompositeProperty
+from .descriptor_props import SynonymProperty
+from .interfaces import _AttributeOptions
+from .interfaces import _DCAttributeOptions
+from .interfaces import _IntrospectsAnnotations
+from .interfaces import _MappedAttribute
+from .interfaces import _MapsColumns
+from .interfaces import MapperProperty
+from .mapper import Mapper
+from .properties import ColumnProperty
+from .properties import MappedColumn
+from .util import _extract_mapped_subtype
+from .util import _is_mapped_annotation
+from .util import class_mapper
+from .util import de_stringify_annotation
+from .. import event
+from .. import exc
+from .. import util
+from ..sql import expression
+from ..sql.base import _NoArg
+from ..sql.schema import Column
+from ..sql.schema import Table
+from ..util import topological
+from ..util.typing import _AnnotationScanType
+from ..util.typing import is_fwd_ref
+from ..util.typing import is_literal
+from ..util.typing import Protocol
+from ..util.typing import TypedDict
+from ..util.typing import typing_get_args
+
+if TYPE_CHECKING:
+ from ._typing import _ClassDict
+ from ._typing import _RegistryType
+ from .base import Mapped
+ from .decl_api import declared_attr
+ from .instrumentation import ClassManager
+ from ..sql.elements import NamedColumn
+ from ..sql.schema import MetaData
+ from ..sql.selectable import FromClause
+
+_T = TypeVar("_T", bound=Any)
+
+_MapperKwArgs = Mapping[str, Any]
+_TableArgsType = Union[Tuple[Any, ...], Dict[str, Any]]
+
+
+class MappedClassProtocol(Protocol[_O]):
+ """A protocol representing a SQLAlchemy mapped class.
+
+ The protocol is generic on the type of class, use
+ ``MappedClassProtocol[Any]`` to allow any mapped class.
+ """
+
+ __name__: str
+ __mapper__: Mapper[_O]
+ __table__: FromClause
+
+ def __call__(self, **kw: Any) -> _O: ...
+
+
+class _DeclMappedClassProtocol(MappedClassProtocol[_O], Protocol):
+ "Internal more detailed version of ``MappedClassProtocol``."
+ metadata: MetaData
+ __tablename__: str
+ __mapper_args__: _MapperKwArgs
+ __table_args__: Optional[_TableArgsType]
+
+ _sa_apply_dc_transforms: Optional[_DataclassArguments]
+
+ def __declare_first__(self) -> None: ...
+
+ def __declare_last__(self) -> None: ...
+
+
+class _DataclassArguments(TypedDict):
+ init: Union[_NoArg, bool]
+ repr: Union[_NoArg, bool]
+ eq: Union[_NoArg, bool]
+ order: Union[_NoArg, bool]
+ unsafe_hash: Union[_NoArg, bool]
+ match_args: Union[_NoArg, bool]
+ kw_only: Union[_NoArg, bool]
+ dataclass_callable: Union[_NoArg, Callable[..., Type[Any]]]
+
+
+def _declared_mapping_info(
+ cls: Type[Any],
+) -> Optional[Union[_DeferredMapperConfig, Mapper[Any]]]:
+ # deferred mapping
+ if _DeferredMapperConfig.has_cls(cls):
+ return _DeferredMapperConfig.config_for_cls(cls)
+ # regular mapping
+ elif _is_mapped_class(cls):
+ return class_mapper(cls, configure=False)
+ else:
+ return None
+
+
+def _is_supercls_for_inherits(cls: Type[Any]) -> bool:
+ """return True if this class will be used as a superclass to set in
+ 'inherits'.
+
+ This includes deferred mapper configs that aren't mapped yet, however does
+ not include classes with _sa_decl_prepare_nocascade (e.g.
+ ``AbstractConcreteBase``); these concrete-only classes are not set up as
+ "inherits" until after mappers are configured using
+ mapper._set_concrete_base()
+
+ """
+ if _DeferredMapperConfig.has_cls(cls):
+ return not _get_immediate_cls_attr(
+ cls, "_sa_decl_prepare_nocascade", strict=True
+ )
+ # regular mapping
+ elif _is_mapped_class(cls):
+ return True
+ else:
+ return False
+
+
+def _resolve_for_abstract_or_classical(cls: Type[Any]) -> Optional[Type[Any]]:
+ if cls is object:
+ return None
+
+ sup: Optional[Type[Any]]
+
+ if cls.__dict__.get("__abstract__", False):
+ for base_ in cls.__bases__:
+ sup = _resolve_for_abstract_or_classical(base_)
+ if sup is not None:
+ return sup
+ else:
+ return None
+ else:
+ clsmanager = _dive_for_cls_manager(cls)
+
+ if clsmanager:
+ return clsmanager.class_
+ else:
+ return cls
+
+
+def _get_immediate_cls_attr(
+ cls: Type[Any], attrname: str, strict: bool = False
+) -> Optional[Any]:
+ """return an attribute of the class that is either present directly
+ on the class, e.g. not on a superclass, or is from a superclass but
+ this superclass is a non-mapped mixin, that is, not a descendant of
+ the declarative base and is also not classically mapped.
+
+ This is used to detect attributes that indicate something about
+ a mapped class independently from any mapped classes that it may
+ inherit from.
+
+ """
+
+ # the rules are different for this name than others,
+ # make sure we've moved it out. transitional
+ assert attrname != "__abstract__"
+
+ if not issubclass(cls, object):
+ return None
+
+ if attrname in cls.__dict__:
+ return getattr(cls, attrname)
+
+ for base in cls.__mro__[1:]:
+ _is_classical_inherits = _dive_for_cls_manager(base) is not None
+
+ if attrname in base.__dict__ and (
+ base is cls
+ or (
+ (base in cls.__bases__ if strict else True)
+ and not _is_classical_inherits
+ )
+ ):
+ return getattr(base, attrname)
+ else:
+ return None
+
+
+def _dive_for_cls_manager(cls: Type[_O]) -> Optional[ClassManager[_O]]:
+ # because the class manager registration is pluggable,
+ # we need to do the search for every class in the hierarchy,
+ # rather than just a simple "cls._sa_class_manager"
+
+ for base in cls.__mro__:
+ manager: Optional[ClassManager[_O]] = attributes.opt_manager_of_class(
+ base
+ )
+ if manager:
+ return manager
+ return None
+
+
+def _as_declarative(
+ registry: _RegistryType, cls: Type[Any], dict_: _ClassDict
+) -> Optional[_MapperConfig]:
+ # declarative scans the class for attributes. no table or mapper
+ # args passed separately.
+ return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
+
+
+def _mapper(
+ registry: _RegistryType,
+ cls: Type[_O],
+ table: Optional[FromClause],
+ mapper_kw: _MapperKwArgs,
+) -> Mapper[_O]:
+ _ImperativeMapperConfig(registry, cls, table, mapper_kw)
+ return cast("MappedClassProtocol[_O]", cls).__mapper__
+
+
+@util.preload_module("sqlalchemy.orm.decl_api")
+def _is_declarative_props(obj: Any) -> bool:
+ _declared_attr_common = util.preloaded.orm_decl_api._declared_attr_common
+
+ return isinstance(obj, (_declared_attr_common, util.classproperty))
+
+
+def _check_declared_props_nocascade(
+ obj: Any, name: str, cls: Type[_O]
+) -> bool:
+ if _is_declarative_props(obj):
+ if getattr(obj, "_cascading", False):
+ util.warn(
+ "@declared_attr.cascading is not supported on the %s "
+ "attribute on class %s. This attribute invokes for "
+ "subclasses in any case." % (name, cls)
+ )
+ return True
+ else:
+ return False
+
+
+class _MapperConfig:
+ __slots__ = (
+ "cls",
+ "classname",
+ "properties",
+ "declared_attr_reg",
+ "__weakref__",
+ )
+
+ cls: Type[Any]
+ classname: str
+ properties: util.OrderedDict[
+ str,
+ Union[
+ Sequence[NamedColumn[Any]], NamedColumn[Any], MapperProperty[Any]
+ ],
+ ]
+ declared_attr_reg: Dict[declared_attr[Any], Any]
+
+ @classmethod
+ def setup_mapping(
+ cls,
+ registry: _RegistryType,
+ cls_: Type[_O],
+ dict_: _ClassDict,
+ table: Optional[FromClause],
+ mapper_kw: _MapperKwArgs,
+ ) -> Optional[_MapperConfig]:
+ manager = attributes.opt_manager_of_class(cls)
+ if manager and manager.class_ is cls_:
+ raise exc.InvalidRequestError(
+ f"Class {cls!r} already has been instrumented declaratively"
+ )
+
+ if cls_.__dict__.get("__abstract__", False):
+ return None
+
+ defer_map = _get_immediate_cls_attr(
+ cls_, "_sa_decl_prepare_nocascade", strict=True
+ ) or hasattr(cls_, "_sa_decl_prepare")
+
+ if defer_map:
+ return _DeferredMapperConfig(
+ registry, cls_, dict_, table, mapper_kw
+ )
+ else:
+ return _ClassScanMapperConfig(
+ registry, cls_, dict_, table, mapper_kw
+ )
+
+ def __init__(
+ self,
+ registry: _RegistryType,
+ cls_: Type[Any],
+ mapper_kw: _MapperKwArgs,
+ ):
+ self.cls = util.assert_arg_type(cls_, type, "cls_")
+ self.classname = cls_.__name__
+ self.properties = util.OrderedDict()
+ self.declared_attr_reg = {}
+
+ if not mapper_kw.get("non_primary", False):
+ instrumentation.register_class(
+ self.cls,
+ finalize=False,
+ registry=registry,
+ declarative_scan=self,
+ init_method=registry.constructor,
+ )
+ else:
+ manager = attributes.opt_manager_of_class(self.cls)
+ if not manager or not manager.is_mapped:
+ raise exc.InvalidRequestError(
+ "Class %s has no primary mapper configured. Configure "
+ "a primary mapper first before setting up a non primary "
+ "Mapper." % self.cls
+ )
+
+ def set_cls_attribute(self, attrname: str, value: _T) -> _T:
+ manager = instrumentation.manager_of_class(self.cls)
+ manager.install_member(attrname, value)
+ return value
+
+ def map(self, mapper_kw: _MapperKwArgs = ...) -> Mapper[Any]:
+ raise NotImplementedError()
+
+ def _early_mapping(self, mapper_kw: _MapperKwArgs) -> None:
+ self.map(mapper_kw)
+
+
+class _ImperativeMapperConfig(_MapperConfig):
+ __slots__ = ("local_table", "inherits")
+
+ def __init__(
+ self,
+ registry: _RegistryType,
+ cls_: Type[_O],
+ table: Optional[FromClause],
+ mapper_kw: _MapperKwArgs,
+ ):
+ super().__init__(registry, cls_, mapper_kw)
+
+ self.local_table = self.set_cls_attribute("__table__", table)
+
+ with mapperlib._CONFIGURE_MUTEX:
+ if not mapper_kw.get("non_primary", False):
+ clsregistry.add_class(
+ self.classname, self.cls, registry._class_registry
+ )
+
+ self._setup_inheritance(mapper_kw)
+
+ self._early_mapping(mapper_kw)
+
+ def map(self, mapper_kw: _MapperKwArgs = util.EMPTY_DICT) -> Mapper[Any]:
+ mapper_cls = Mapper
+
+ return self.set_cls_attribute(
+ "__mapper__",
+ mapper_cls(self.cls, self.local_table, **mapper_kw),
+ )
+
+ def _setup_inheritance(self, mapper_kw: _MapperKwArgs) -> None:
+ cls = self.cls
+
+ inherits = mapper_kw.get("inherits", None)
+
+ if inherits is None:
+ # since we search for classical mappings now, search for
+ # multiple mapped bases as well and raise an error.
+ inherits_search = []
+ for base_ in cls.__bases__:
+ c = _resolve_for_abstract_or_classical(base_)
+ if c is None:
+ continue
+
+ if _is_supercls_for_inherits(c) and c not in inherits_search:
+ inherits_search.append(c)
+
+ if inherits_search:
+ if len(inherits_search) > 1:
+ raise exc.InvalidRequestError(
+ "Class %s has multiple mapped bases: %r"
+ % (cls, inherits_search)
+ )
+ inherits = inherits_search[0]
+ elif isinstance(inherits, Mapper):
+ inherits = inherits.class_
+
+ self.inherits = inherits
+
+
+class _CollectedAnnotation(NamedTuple):
+ raw_annotation: _AnnotationScanType
+ mapped_container: Optional[Type[Mapped[Any]]]
+ extracted_mapped_annotation: Union[Type[Any], str]
+ is_dataclass: bool
+ attr_value: Any
+ originating_module: str
+ originating_class: Type[Any]
+
+
+class _ClassScanMapperConfig(_MapperConfig):
+ __slots__ = (
+ "registry",
+ "clsdict_view",
+ "collected_attributes",
+ "collected_annotations",
+ "local_table",
+ "persist_selectable",
+ "declared_columns",
+ "column_ordering",
+ "column_copies",
+ "table_args",
+ "tablename",
+ "mapper_args",
+ "mapper_args_fn",
+ "inherits",
+ "single",
+ "allow_dataclass_fields",
+ "dataclass_setup_arguments",
+ "is_dataclass_prior_to_mapping",
+ "allow_unmapped_annotations",
+ )
+
+ is_deferred = False
+ registry: _RegistryType
+ clsdict_view: _ClassDict
+ collected_annotations: Dict[str, _CollectedAnnotation]
+ collected_attributes: Dict[str, Any]
+ local_table: Optional[FromClause]
+ persist_selectable: Optional[FromClause]
+ declared_columns: util.OrderedSet[Column[Any]]
+ column_ordering: Dict[Column[Any], int]
+ column_copies: Dict[
+ Union[MappedColumn[Any], Column[Any]],
+ Union[MappedColumn[Any], Column[Any]],
+ ]
+ tablename: Optional[str]
+ mapper_args: Mapping[str, Any]
+ table_args: Optional[_TableArgsType]
+ mapper_args_fn: Optional[Callable[[], Dict[str, Any]]]
+ inherits: Optional[Type[Any]]
+ single: bool
+
+ is_dataclass_prior_to_mapping: bool
+ allow_unmapped_annotations: bool
+
+ dataclass_setup_arguments: Optional[_DataclassArguments]
+ """if the class has SQLAlchemy native dataclass parameters, where
+ we will turn the class into a dataclass within the declarative mapping
+ process.
+
+ """
+
+ allow_dataclass_fields: bool
+ """if true, look for dataclass-processed Field objects on the target
+ class as well as superclasses and extract ORM mapping directives from
+ the "metadata" attribute of each Field.
+
+ if False, dataclass fields can still be used, however they won't be
+ mapped.
+
+ """
+
+ def __init__(
+ self,
+ registry: _RegistryType,
+ cls_: Type[_O],
+ dict_: _ClassDict,
+ table: Optional[FromClause],
+ mapper_kw: _MapperKwArgs,
+ ):
+ # grab class dict before the instrumentation manager has been added.
+ # reduces cycles
+ self.clsdict_view = (
+ util.immutabledict(dict_) if dict_ else util.EMPTY_DICT
+ )
+ super().__init__(registry, cls_, mapper_kw)
+ self.registry = registry
+ self.persist_selectable = None
+
+ self.collected_attributes = {}
+ self.collected_annotations = {}
+ self.declared_columns = util.OrderedSet()
+ self.column_ordering = {}
+ self.column_copies = {}
+ self.single = False
+ self.dataclass_setup_arguments = dca = getattr(
+ self.cls, "_sa_apply_dc_transforms", None
+ )
+
+ self.allow_unmapped_annotations = getattr(
+ self.cls, "__allow_unmapped__", False
+ ) or bool(self.dataclass_setup_arguments)
+
+ self.is_dataclass_prior_to_mapping = cld = dataclasses.is_dataclass(
+ cls_
+ )
+
+ sdk = _get_immediate_cls_attr(cls_, "__sa_dataclass_metadata_key__")
+
+ # we don't want to consume Field objects from a not-already-dataclass.
+ # the Field objects won't have their "name" or "type" populated,
+ # and while it seems like we could just set these on Field as we
+ # read them, Field is documented as "user read only" and we need to
+ # stay far away from any off-label use of dataclasses APIs.
+ if (not cld or dca) and sdk:
+ raise exc.InvalidRequestError(
+ "SQLAlchemy mapped dataclasses can't consume mapping "
+ "information from dataclass.Field() objects if the immediate "
+ "class is not already a dataclass."
+ )
+
+ # if already a dataclass, and __sa_dataclass_metadata_key__ present,
+ # then also look inside of dataclass.Field() objects yielded by
+ # dataclasses.get_fields(cls) when scanning for attributes
+ self.allow_dataclass_fields = bool(sdk and cld)
+
+ self._setup_declared_events()
+
+ self._scan_attributes()
+
+ self._setup_dataclasses_transforms()
+
+ with mapperlib._CONFIGURE_MUTEX:
+ clsregistry.add_class(
+ self.classname, self.cls, registry._class_registry
+ )
+
+ self._setup_inheriting_mapper(mapper_kw)
+
+ self._extract_mappable_attributes()
+
+ self._extract_declared_columns()
+
+ self._setup_table(table)
+
+ self._setup_inheriting_columns(mapper_kw)
+
+ self._early_mapping(mapper_kw)
+
+ def _setup_declared_events(self) -> None:
+ if _get_immediate_cls_attr(self.cls, "__declare_last__"):
+
+ @event.listens_for(Mapper, "after_configured")
+ def after_configured() -> None:
+ cast(
+ "_DeclMappedClassProtocol[Any]", self.cls
+ ).__declare_last__()
+
+ if _get_immediate_cls_attr(self.cls, "__declare_first__"):
+
+ @event.listens_for(Mapper, "before_configured")
+ def before_configured() -> None:
+ cast(
+ "_DeclMappedClassProtocol[Any]", self.cls
+ ).__declare_first__()
+
+ def _cls_attr_override_checker(
+ self, cls: Type[_O]
+ ) -> Callable[[str, Any], bool]:
+ """Produce a function that checks if a class has overridden an
+ attribute, taking SQLAlchemy-enabled dataclass fields into account.
+
+ """
+
+ if self.allow_dataclass_fields:
+ sa_dataclass_metadata_key = _get_immediate_cls_attr(
+ cls, "__sa_dataclass_metadata_key__"
+ )
+ else:
+ sa_dataclass_metadata_key = None
+
+ if not sa_dataclass_metadata_key:
+
+ def attribute_is_overridden(key: str, obj: Any) -> bool:
+ return getattr(cls, key, obj) is not obj
+
+ else:
+ all_datacls_fields = {
+ f.name: f.metadata[sa_dataclass_metadata_key]
+ for f in util.dataclass_fields(cls)
+ if sa_dataclass_metadata_key in f.metadata
+ }
+ local_datacls_fields = {
+ f.name: f.metadata[sa_dataclass_metadata_key]
+ for f in util.local_dataclass_fields(cls)
+ if sa_dataclass_metadata_key in f.metadata
+ }
+
+ absent = object()
+
+ def attribute_is_overridden(key: str, obj: Any) -> bool:
+ if _is_declarative_props(obj):
+ obj = obj.fget
+
+ # this function likely has some failure modes still if
+ # someone is doing a deep mixing of the same attribute
+ # name as plain Python attribute vs. dataclass field.
+
+ ret = local_datacls_fields.get(key, absent)
+ if _is_declarative_props(ret):
+ ret = ret.fget
+
+ if ret is obj:
+ return False
+ elif ret is not absent:
+ return True
+
+ all_field = all_datacls_fields.get(key, absent)
+
+ ret = getattr(cls, key, obj)
+
+ if ret is obj:
+ return False
+
+ # for dataclasses, this could be the
+ # 'default' of the field. so filter more specifically
+ # for an already-mapped InstrumentedAttribute
+ if ret is not absent and isinstance(
+ ret, InstrumentedAttribute
+ ):
+ return True
+
+ if all_field is obj:
+ return False
+ elif all_field is not absent:
+ return True
+
+ # can't find another attribute
+ return False
+
+ return attribute_is_overridden
+
+ _include_dunders = {
+ "__table__",
+ "__mapper_args__",
+ "__tablename__",
+ "__table_args__",
+ }
+
+ _match_exclude_dunders = re.compile(r"^(?:_sa_|__)")
+
+ def _cls_attr_resolver(
+ self, cls: Type[Any]
+ ) -> Callable[[], Iterable[Tuple[str, Any, Any, bool]]]:
+ """produce a function to iterate the "attributes" of a class
+ which we want to consider for mapping, adjusting for SQLAlchemy fields
+ embedded in dataclass fields.
+
+ """
+ cls_annotations = util.get_annotations(cls)
+
+ cls_vars = vars(cls)
+
+ _include_dunders = self._include_dunders
+ _match_exclude_dunders = self._match_exclude_dunders
+
+ names = [
+ n
+ for n in util.merge_lists_w_ordering(
+ list(cls_vars), list(cls_annotations)
+ )
+ if not _match_exclude_dunders.match(n) or n in _include_dunders
+ ]
+
+ if self.allow_dataclass_fields:
+ sa_dataclass_metadata_key: Optional[str] = _get_immediate_cls_attr(
+ cls, "__sa_dataclass_metadata_key__"
+ )
+ else:
+ sa_dataclass_metadata_key = None
+
+ if not sa_dataclass_metadata_key:
+
+ def local_attributes_for_class() -> (
+ Iterable[Tuple[str, Any, Any, bool]]
+ ):
+ return (
+ (
+ name,
+ cls_vars.get(name),
+ cls_annotations.get(name),
+ False,
+ )
+ for name in names
+ )
+
+ else:
+ dataclass_fields = {
+ field.name: field for field in util.local_dataclass_fields(cls)
+ }
+
+ fixed_sa_dataclass_metadata_key = sa_dataclass_metadata_key
+
+ def local_attributes_for_class() -> (
+ Iterable[Tuple[str, Any, Any, bool]]
+ ):
+ for name in names:
+ field = dataclass_fields.get(name, None)
+ if field and sa_dataclass_metadata_key in field.metadata:
+ yield field.name, _as_dc_declaredattr(
+ field.metadata, fixed_sa_dataclass_metadata_key
+ ), cls_annotations.get(field.name), True
+ else:
+ yield name, cls_vars.get(name), cls_annotations.get(
+ name
+ ), False
+
+ return local_attributes_for_class
+
+ def _scan_attributes(self) -> None:
+ cls = self.cls
+
+ cls_as_Decl = cast("_DeclMappedClassProtocol[Any]", cls)
+
+ clsdict_view = self.clsdict_view
+ collected_attributes = self.collected_attributes
+ column_copies = self.column_copies
+ _include_dunders = self._include_dunders
+ mapper_args_fn = None
+ table_args = inherited_table_args = None
+
+ tablename = None
+ fixed_table = "__table__" in clsdict_view
+
+ attribute_is_overridden = self._cls_attr_override_checker(self.cls)
+
+ bases = []
+
+ for base in cls.__mro__:
+ # collect bases and make sure standalone columns are copied
+ # to be the column they will ultimately be on the class,
+ # so that declared_attr functions use the right columns.
+ # need to do this all the way up the hierarchy first
+ # (see #8190)
+
+ class_mapped = base is not cls and _is_supercls_for_inherits(base)
+
+ local_attributes_for_class = self._cls_attr_resolver(base)
+
+ if not class_mapped and base is not cls:
+ locally_collected_columns = self._produce_column_copies(
+ local_attributes_for_class,
+ attribute_is_overridden,
+ fixed_table,
+ base,
+ )
+ else:
+ locally_collected_columns = {}
+
+ bases.append(
+ (
+ base,
+ class_mapped,
+ local_attributes_for_class,
+ locally_collected_columns,
+ )
+ )
+
+ for (
+ base,
+ class_mapped,
+ local_attributes_for_class,
+ locally_collected_columns,
+ ) in bases:
+ # this transfer can also take place as we scan each name
+ # for finer-grained control of how collected_attributes is
+ # populated, as this is what impacts column ordering.
+ # however it's simpler to get it out of the way here.
+ collected_attributes.update(locally_collected_columns)
+
+ for (
+ name,
+ obj,
+ annotation,
+ is_dataclass_field,
+ ) in local_attributes_for_class():
+ if name in _include_dunders:
+ if name == "__mapper_args__":
+ check_decl = _check_declared_props_nocascade(
+ obj, name, cls
+ )
+ if not mapper_args_fn and (
+ not class_mapped or check_decl
+ ):
+ # don't even invoke __mapper_args__ until
+ # after we've determined everything about the
+ # mapped table.
+ # make a copy of it so a class-level dictionary
+ # is not overwritten when we update column-based
+ # arguments.
+ def _mapper_args_fn() -> Dict[str, Any]:
+ return dict(cls_as_Decl.__mapper_args__)
+
+ mapper_args_fn = _mapper_args_fn
+
+ elif name == "__tablename__":
+ check_decl = _check_declared_props_nocascade(
+ obj, name, cls
+ )
+ if not tablename and (not class_mapped or check_decl):
+ tablename = cls_as_Decl.__tablename__
+ elif name == "__table_args__":
+ check_decl = _check_declared_props_nocascade(
+ obj, name, cls
+ )
+ if not table_args and (not class_mapped or check_decl):
+ table_args = cls_as_Decl.__table_args__
+ if not isinstance(
+ table_args, (tuple, dict, type(None))
+ ):
+ raise exc.ArgumentError(
+ "__table_args__ value must be a tuple, "
+ "dict, or None"
+ )
+ if base is not cls:
+ inherited_table_args = True
+ else:
+ # skip all other dunder names, which at the moment
+ # should only be __table__
+ continue
+ elif class_mapped:
+ if _is_declarative_props(obj) and not obj._quiet:
+ util.warn(
+ "Regular (i.e. not __special__) "
+ "attribute '%s.%s' uses @declared_attr, "
+ "but owning class %s is mapped - "
+ "not applying to subclass %s."
+ % (base.__name__, name, base, cls)
+ )
+
+ continue
+ elif base is not cls:
+ # we're a mixin, abstract base, or something that is
+ # acting like that for now.
+
+ if isinstance(obj, (Column, MappedColumn)):
+ # already copied columns to the mapped class.
+ continue
+ elif isinstance(obj, MapperProperty):
+ raise exc.InvalidRequestError(
+ "Mapper properties (i.e. deferred,"
+ "column_property(), relationship(), etc.) must "
+ "be declared as @declared_attr callables "
+ "on declarative mixin classes. For dataclass "
+ "field() objects, use a lambda:"
+ )
+ elif _is_declarative_props(obj):
+ # tried to get overloads to tell this to
+ # pylance, no luck
+ assert obj is not None
+
+ if obj._cascading:
+ if name in clsdict_view:
+ # unfortunately, while we can use the user-
+ # defined attribute here to allow a clean
+ # override, if there's another
+ # subclass below then it still tries to use
+ # this. not sure if there is enough
+ # information here to add this as a feature
+ # later on.
+ util.warn(
+ "Attribute '%s' on class %s cannot be "
+ "processed due to "
+ "@declared_attr.cascading; "
+ "skipping" % (name, cls)
+ )
+ collected_attributes[name] = column_copies[obj] = (
+ ret
+ ) = obj.__get__(obj, cls)
+ setattr(cls, name, ret)
+ else:
+ if is_dataclass_field:
+ # access attribute using normal class access
+ # first, to see if it's been mapped on a
+ # superclass. note if the dataclasses.field()
+ # has "default", this value can be anything.
+ ret = getattr(cls, name, None)
+
+ # so, if it's anything that's not ORM
+ # mapped, assume we should invoke the
+ # declared_attr
+ if not isinstance(ret, InspectionAttr):
+ ret = obj.fget()
+ else:
+ # access attribute using normal class access.
+ # if the declared attr already took place
+ # on a superclass that is mapped, then
+ # this is no longer a declared_attr, it will
+ # be the InstrumentedAttribute
+ ret = getattr(cls, name)
+
+ # correct for proxies created from hybrid_property
+ # or similar. note there is no known case that
+ # produces nested proxies, so we are only
+ # looking one level deep right now.
+
+ if (
+ isinstance(ret, InspectionAttr)
+ and attr_is_internal_proxy(ret)
+ and not isinstance(
+ ret.original_property, MapperProperty
+ )
+ ):
+ ret = ret.descriptor
+
+ collected_attributes[name] = column_copies[obj] = (
+ ret
+ )
+
+ if (
+ isinstance(ret, (Column, MapperProperty))
+ and ret.doc is None
+ ):
+ ret.doc = obj.__doc__
+
+ self._collect_annotation(
+ name,
+ obj._collect_return_annotation(),
+ base,
+ True,
+ obj,
+ )
+ elif _is_mapped_annotation(annotation, cls, base):
+ # Mapped annotation without any object.
+ # product_column_copies should have handled this.
+ # if future support for other MapperProperty,
+ # then test if this name is already handled and
+ # otherwise proceed to generate.
+ if not fixed_table:
+ assert (
+ name in collected_attributes
+ or attribute_is_overridden(name, None)
+ )
+ continue
+ else:
+ # here, the attribute is some other kind of
+ # property that we assume is not part of the
+ # declarative mapping. however, check for some
+ # more common mistakes
+ self._warn_for_decl_attributes(base, name, obj)
+ elif is_dataclass_field and (
+ name not in clsdict_view or clsdict_view[name] is not obj
+ ):
+ # here, we are definitely looking at the target class
+ # and not a superclass. this is currently a
+ # dataclass-only path. if the name is only
+ # a dataclass field and isn't in local cls.__dict__,
+ # put the object there.
+ # assert that the dataclass-enabled resolver agrees
+ # with what we are seeing
+
+ assert not attribute_is_overridden(name, obj)
+
+ if _is_declarative_props(obj):
+ obj = obj.fget()
+
+ collected_attributes[name] = obj
+ self._collect_annotation(
+ name, annotation, base, False, obj
+ )
+ else:
+ collected_annotation = self._collect_annotation(
+ name, annotation, base, None, obj
+ )
+ is_mapped = (
+ collected_annotation is not None
+ and collected_annotation.mapped_container is not None
+ )
+ generated_obj = (
+ collected_annotation.attr_value
+ if collected_annotation is not None
+ else obj
+ )
+ if obj is None and not fixed_table and is_mapped:
+ collected_attributes[name] = (
+ generated_obj
+ if generated_obj is not None
+ else MappedColumn()
+ )
+ elif name in clsdict_view:
+ collected_attributes[name] = obj
+ # else if the name is not in the cls.__dict__,
+ # don't collect it as an attribute.
+ # we will see the annotation only, which is meaningful
+ # both for mapping and dataclasses setup
+
+ if inherited_table_args and not tablename:
+ table_args = None
+
+ self.table_args = table_args
+ self.tablename = tablename
+ self.mapper_args_fn = mapper_args_fn
+
+ def _setup_dataclasses_transforms(self) -> None:
+ dataclass_setup_arguments = self.dataclass_setup_arguments
+ if not dataclass_setup_arguments:
+ return
+
+ # can't use is_dataclass since it uses hasattr
+ if "__dataclass_fields__" in self.cls.__dict__:
+ raise exc.InvalidRequestError(
+ f"Class {self.cls} is already a dataclass; ensure that "
+ "base classes / decorator styles of establishing dataclasses "
+ "are not being mixed. "
+ "This can happen if a class that inherits from "
+ "'MappedAsDataclass', even indirectly, is been mapped with "
+ "'@registry.mapped_as_dataclass'"
+ )
+
+ warn_for_non_dc_attrs = collections.defaultdict(list)
+
+ def _allow_dataclass_field(
+ key: str, originating_class: Type[Any]
+ ) -> bool:
+ if (
+ originating_class is not self.cls
+ and "__dataclass_fields__" not in originating_class.__dict__
+ ):
+ warn_for_non_dc_attrs[originating_class].append(key)
+
+ return True
+
+ manager = instrumentation.manager_of_class(self.cls)
+ assert manager is not None
+
+ field_list = [
+ _AttributeOptions._get_arguments_for_make_dataclass(
+ key,
+ anno,
+ mapped_container,
+ self.collected_attributes.get(key, _NoArg.NO_ARG),
+ )
+ for key, anno, mapped_container in (
+ (
+ key,
+ mapped_anno if mapped_anno else raw_anno,
+ mapped_container,
+ )
+ for key, (
+ raw_anno,
+ mapped_container,
+ mapped_anno,
+ is_dc,
+ attr_value,
+ originating_module,
+ originating_class,
+ ) in self.collected_annotations.items()
+ if _allow_dataclass_field(key, originating_class)
+ and (
+ key not in self.collected_attributes
+ # issue #9226; check for attributes that we've collected
+ # which are already instrumented, which we would assume
+ # mean we are in an ORM inheritance mapping and this
+ # attribute is already mapped on the superclass. Under
+ # no circumstance should any QueryableAttribute be sent to
+ # the dataclass() function; anything that's mapped should
+ # be Field and that's it
+ or not isinstance(
+ self.collected_attributes[key], QueryableAttribute
+ )
+ )
+ )
+ ]
+
+ if warn_for_non_dc_attrs:
+ for (
+ originating_class,
+ non_dc_attrs,
+ ) in warn_for_non_dc_attrs.items():
+ util.warn_deprecated(
+ f"When transforming {self.cls} to a dataclass, "
+ f"attribute(s) "
+ f"{', '.join(repr(key) for key in non_dc_attrs)} "
+ f"originates from superclass "
+ f"{originating_class}, which is not a dataclass. This "
+ f"usage is deprecated and will raise an error in "
+ f"SQLAlchemy 2.1. When declaring SQLAlchemy Declarative "
+ f"Dataclasses, ensure that all mixin classes and other "
+ f"superclasses which include attributes are also a "
+ f"subclass of MappedAsDataclass.",
+ "2.0",
+ code="dcmx",
+ )
+
+ annotations = {}
+ defaults = {}
+ for item in field_list:
+ if len(item) == 2:
+ name, tp = item
+ elif len(item) == 3:
+ name, tp, spec = item
+ defaults[name] = spec
+ else:
+ assert False
+ annotations[name] = tp
+
+ for k, v in defaults.items():
+ setattr(self.cls, k, v)
+
+ self._apply_dataclasses_to_any_class(
+ dataclass_setup_arguments, self.cls, annotations
+ )
+
+ @classmethod
+ def _update_annotations_for_non_mapped_class(
+ cls, klass: Type[_O]
+ ) -> Mapping[str, _AnnotationScanType]:
+ cls_annotations = util.get_annotations(klass)
+
+ new_anno = {}
+ for name, annotation in cls_annotations.items():
+ if _is_mapped_annotation(annotation, klass, klass):
+ extracted = _extract_mapped_subtype(
+ annotation,
+ klass,
+ klass.__module__,
+ name,
+ type(None),
+ required=False,
+ is_dataclass_field=False,
+ expect_mapped=False,
+ )
+ if extracted:
+ inner, _ = extracted
+ new_anno[name] = inner
+ else:
+ new_anno[name] = annotation
+ return new_anno
+
+ @classmethod
+ def _apply_dataclasses_to_any_class(
+ cls,
+ dataclass_setup_arguments: _DataclassArguments,
+ klass: Type[_O],
+ use_annotations: Mapping[str, _AnnotationScanType],
+ ) -> None:
+ cls._assert_dc_arguments(dataclass_setup_arguments)
+
+ dataclass_callable = dataclass_setup_arguments["dataclass_callable"]
+ if dataclass_callable is _NoArg.NO_ARG:
+ dataclass_callable = dataclasses.dataclass
+
+ restored: Optional[Any]
+
+ if use_annotations:
+ # apply constructed annotations that should look "normal" to a
+ # dataclasses callable, based on the fields present. This
+ # means remove the Mapped[] container and ensure all Field
+ # entries have an annotation
+ restored = getattr(klass, "__annotations__", None)
+ klass.__annotations__ = cast("Dict[str, Any]", use_annotations)
+ else:
+ restored = None
+
+ try:
+ dataclass_callable(
+ klass,
+ **{
+ k: v
+ for k, v in dataclass_setup_arguments.items()
+ if v is not _NoArg.NO_ARG and k != "dataclass_callable"
+ },
+ )
+ except (TypeError, ValueError) as ex:
+ raise exc.InvalidRequestError(
+ f"Python dataclasses error encountered when creating "
+ f"dataclass for {klass.__name__!r}: "
+ f"{ex!r}. Please refer to Python dataclasses "
+ "documentation for additional information.",
+ code="dcte",
+ ) from ex
+ finally:
+ # restore original annotations outside of the dataclasses
+ # process; for mixins and __abstract__ superclasses, SQLAlchemy
+ # Declarative will need to see the Mapped[] container inside the
+ # annotations in order to map subclasses
+ if use_annotations:
+ if restored is None:
+ del klass.__annotations__
+ else:
+ klass.__annotations__ = restored
+
+ @classmethod
+ def _assert_dc_arguments(cls, arguments: _DataclassArguments) -> None:
+ allowed = {
+ "init",
+ "repr",
+ "order",
+ "eq",
+ "unsafe_hash",
+ "kw_only",
+ "match_args",
+ "dataclass_callable",
+ }
+ disallowed_args = set(arguments).difference(allowed)
+ if disallowed_args:
+ msg = ", ".join(f"{arg!r}" for arg in sorted(disallowed_args))
+ raise exc.ArgumentError(
+ f"Dataclass argument(s) {msg} are not accepted"
+ )
+
+ def _collect_annotation(
+ self,
+ name: str,
+ raw_annotation: _AnnotationScanType,
+ originating_class: Type[Any],
+ expect_mapped: Optional[bool],
+ attr_value: Any,
+ ) -> Optional[_CollectedAnnotation]:
+ if name in self.collected_annotations:
+ return self.collected_annotations[name]
+
+ if raw_annotation is None:
+ return None
+
+ is_dataclass = self.is_dataclass_prior_to_mapping
+ allow_unmapped = self.allow_unmapped_annotations
+
+ if expect_mapped is None:
+ is_dataclass_field = isinstance(attr_value, dataclasses.Field)
+ expect_mapped = (
+ not is_dataclass_field
+ and not allow_unmapped
+ and (
+ attr_value is None
+ or isinstance(attr_value, _MappedAttribute)
+ )
+ )
+ else:
+ is_dataclass_field = False
+
+ is_dataclass_field = False
+ extracted = _extract_mapped_subtype(
+ raw_annotation,
+ self.cls,
+ originating_class.__module__,
+ name,
+ type(attr_value),
+ required=False,
+ is_dataclass_field=is_dataclass_field,
+ expect_mapped=expect_mapped
+ and not is_dataclass, # self.allow_dataclass_fields,
+ )
+
+ if extracted is None:
+ # ClassVar can come out here
+ return None
+
+ extracted_mapped_annotation, mapped_container = extracted
+
+ if attr_value is None and not is_literal(extracted_mapped_annotation):
+ for elem in typing_get_args(extracted_mapped_annotation):
+ if isinstance(elem, str) or is_fwd_ref(
+ elem, check_generic=True
+ ):
+ elem = de_stringify_annotation(
+ self.cls,
+ elem,
+ originating_class.__module__,
+ include_generic=True,
+ )
+ # look in Annotated[...] for an ORM construct,
+ # such as Annotated[int, mapped_column(primary_key=True)]
+ if isinstance(elem, _IntrospectsAnnotations):
+ attr_value = elem.found_in_pep593_annotated()
+
+ self.collected_annotations[name] = ca = _CollectedAnnotation(
+ raw_annotation,
+ mapped_container,
+ extracted_mapped_annotation,
+ is_dataclass,
+ attr_value,
+ originating_class.__module__,
+ originating_class,
+ )
+ return ca
+
+ def _warn_for_decl_attributes(
+ self, cls: Type[Any], key: str, c: Any
+ ) -> None:
+ if isinstance(c, expression.ColumnElement):
+ util.warn(
+ f"Attribute '{key}' on class {cls} appears to "
+ "be a non-schema SQLAlchemy expression "
+ "object; this won't be part of the declarative mapping. "
+ "To map arbitrary expressions, use ``column_property()`` "
+ "or a similar function such as ``deferred()``, "
+ "``query_expression()`` etc. "
+ )
+
+ def _produce_column_copies(
+ self,
+ attributes_for_class: Callable[
+ [], Iterable[Tuple[str, Any, Any, bool]]
+ ],
+ attribute_is_overridden: Callable[[str, Any], bool],
+ fixed_table: bool,
+ originating_class: Type[Any],
+ ) -> Dict[str, Union[Column[Any], MappedColumn[Any]]]:
+ cls = self.cls
+ dict_ = self.clsdict_view
+ locally_collected_attributes = {}
+ column_copies = self.column_copies
+ # copy mixin columns to the mapped class
+
+ for name, obj, annotation, is_dataclass in attributes_for_class():
+ if (
+ not fixed_table
+ and obj is None
+ and _is_mapped_annotation(annotation, cls, originating_class)
+ ):
+ # obj is None means this is the annotation only path
+
+ if attribute_is_overridden(name, obj):
+ # perform same "overridden" check as we do for
+ # Column/MappedColumn, this is how a mixin col is not
+ # applied to an inherited subclass that does not have
+ # the mixin. the anno-only path added here for
+ # #9564
+ continue
+
+ collected_annotation = self._collect_annotation(
+ name, annotation, originating_class, True, obj
+ )
+ obj = (
+ collected_annotation.attr_value
+ if collected_annotation is not None
+ else obj
+ )
+ if obj is None:
+ obj = MappedColumn()
+
+ locally_collected_attributes[name] = obj
+ setattr(cls, name, obj)
+
+ elif isinstance(obj, (Column, MappedColumn)):
+ if attribute_is_overridden(name, obj):
+ # if column has been overridden
+ # (like by the InstrumentedAttribute of the
+ # superclass), skip. don't collect the annotation
+ # either (issue #8718)
+ continue
+
+ collected_annotation = self._collect_annotation(
+ name, annotation, originating_class, True, obj
+ )
+ obj = (
+ collected_annotation.attr_value
+ if collected_annotation is not None
+ else obj
+ )
+
+ if name not in dict_ and not (
+ "__table__" in dict_
+ and (getattr(obj, "name", None) or name)
+ in dict_["__table__"].c
+ ):
+ if obj.foreign_keys:
+ for fk in obj.foreign_keys:
+ if (
+ fk._table_column is not None
+ and fk._table_column.table is None
+ ):
+ raise exc.InvalidRequestError(
+ "Columns with foreign keys to "
+ "non-table-bound "
+ "columns must be declared as "
+ "@declared_attr callables "
+ "on declarative mixin classes. "
+ "For dataclass "
+ "field() objects, use a lambda:."
+ )
+
+ column_copies[obj] = copy_ = obj._copy()
+
+ locally_collected_attributes[name] = copy_
+ setattr(cls, name, copy_)
+
+ return locally_collected_attributes
+
+ def _extract_mappable_attributes(self) -> None:
+ cls = self.cls
+ collected_attributes = self.collected_attributes
+
+ our_stuff = self.properties
+
+ _include_dunders = self._include_dunders
+
+ late_mapped = _get_immediate_cls_attr(
+ cls, "_sa_decl_prepare_nocascade", strict=True
+ )
+
+ allow_unmapped_annotations = self.allow_unmapped_annotations
+ expect_annotations_wo_mapped = (
+ allow_unmapped_annotations or self.is_dataclass_prior_to_mapping
+ )
+
+ look_for_dataclass_things = bool(self.dataclass_setup_arguments)
+
+ for k in list(collected_attributes):
+ if k in _include_dunders:
+ continue
+
+ value = collected_attributes[k]
+
+ if _is_declarative_props(value):
+ # @declared_attr in collected_attributes only occurs here for a
+ # @declared_attr that's directly on the mapped class;
+ # for a mixin, these have already been evaluated
+ if value._cascading:
+ util.warn(
+ "Use of @declared_attr.cascading only applies to "
+ "Declarative 'mixin' and 'abstract' classes. "
+ "Currently, this flag is ignored on mapped class "
+ "%s" % self.cls
+ )
+
+ value = getattr(cls, k)
+
+ elif (
+ isinstance(value, QueryableAttribute)
+ and value.class_ is not cls
+ and value.key != k
+ ):
+ # detect a QueryableAttribute that's already mapped being
+ # assigned elsewhere in userland, turn into a synonym()
+ value = SynonymProperty(value.key)
+ setattr(cls, k, value)
+
+ if (
+ isinstance(value, tuple)
+ and len(value) == 1
+ and isinstance(value[0], (Column, _MappedAttribute))
+ ):
+ util.warn(
+ "Ignoring declarative-like tuple value of attribute "
+ "'%s': possibly a copy-and-paste error with a comma "
+ "accidentally placed at the end of the line?" % k
+ )
+ continue
+ elif look_for_dataclass_things and isinstance(
+ value, dataclasses.Field
+ ):
+ # we collected a dataclass Field; dataclasses would have
+ # set up the correct state on the class
+ continue
+ elif not isinstance(value, (Column, _DCAttributeOptions)):
+ # using @declared_attr for some object that
+ # isn't Column/MapperProperty/_DCAttributeOptions; remove
+ # from the clsdict_view
+ # and place the evaluated value onto the class.
+ collected_attributes.pop(k)
+ self._warn_for_decl_attributes(cls, k, value)
+ if not late_mapped:
+ setattr(cls, k, value)
+ continue
+ # we expect to see the name 'metadata' in some valid cases;
+ # however at this point we see it's assigned to something trying
+ # to be mapped, so raise for that.
+ # TODO: should "registry" here be also? might be too late
+ # to change that now (2.0 betas)
+ elif k in ("metadata",):
+ raise exc.InvalidRequestError(
+ f"Attribute name '{k}' is reserved when using the "
+ "Declarative API."
+ )
+ elif isinstance(value, Column):
+ _undefer_column_name(
+ k, self.column_copies.get(value, value) # type: ignore
+ )
+ else:
+ if isinstance(value, _IntrospectsAnnotations):
+ (
+ annotation,
+ mapped_container,
+ extracted_mapped_annotation,
+ is_dataclass,
+ attr_value,
+ originating_module,
+ originating_class,
+ ) = self.collected_annotations.get(
+ k, (None, None, None, False, None, None, None)
+ )
+
+ # issue #8692 - don't do any annotation interpretation if
+ # an annotation were present and a container such as
+ # Mapped[] etc. were not used. If annotation is None,
+ # do declarative_scan so that the property can raise
+ # for required
+ if (
+ mapped_container is not None
+ or annotation is None
+ # issue #10516: need to do declarative_scan even with
+ # a non-Mapped annotation if we are doing
+ # __allow_unmapped__, for things like col.name
+ # assignment
+ or allow_unmapped_annotations
+ ):
+ try:
+ value.declarative_scan(
+ self,
+ self.registry,
+ cls,
+ originating_module,
+ k,
+ mapped_container,
+ annotation,
+ extracted_mapped_annotation,
+ is_dataclass,
+ )
+ except NameError as ne:
+ raise exc.ArgumentError(
+ f"Could not resolve all types within mapped "
+ f'annotation: "{annotation}". Ensure all '
+ f"types are written correctly and are "
+ f"imported within the module in use."
+ ) from ne
+ else:
+ # assert that we were expecting annotations
+ # without Mapped[] were going to be passed.
+ # otherwise an error should have been raised
+ # by util._extract_mapped_subtype before we got here.
+ assert expect_annotations_wo_mapped
+
+ if isinstance(value, _DCAttributeOptions):
+ if (
+ value._has_dataclass_arguments
+ and not look_for_dataclass_things
+ ):
+ if isinstance(value, MapperProperty):
+ argnames = [
+ "init",
+ "default_factory",
+ "repr",
+ "default",
+ ]
+ else:
+ argnames = ["init", "default_factory", "repr"]
+
+ args = {
+ a
+ for a in argnames
+ if getattr(
+ value._attribute_options, f"dataclasses_{a}"
+ )
+ is not _NoArg.NO_ARG
+ }
+
+ raise exc.ArgumentError(
+ f"Attribute '{k}' on class {cls} includes "
+ f"dataclasses argument(s): "
+ f"{', '.join(sorted(repr(a) for a in args))} but "
+ f"class does not specify "
+ "SQLAlchemy native dataclass configuration."
+ )
+
+ if not isinstance(value, (MapperProperty, _MapsColumns)):
+ # filter for _DCAttributeOptions objects that aren't
+ # MapperProperty / mapped_column(). Currently this
+ # includes AssociationProxy. pop it from the things
+ # we're going to map and set it up as a descriptor
+ # on the class.
+ collected_attributes.pop(k)
+
+ # Assoc Prox (or other descriptor object that may
+ # use _DCAttributeOptions) is usually here, except if
+ # 1. we're a
+ # dataclass, dataclasses would have removed the
+ # attr here or 2. assoc proxy is coming from a
+ # superclass, we want it to be direct here so it
+ # tracks state or 3. assoc prox comes from
+ # declared_attr, uncommon case
+ setattr(cls, k, value)
+ continue
+
+ our_stuff[k] = value
+
+ def _extract_declared_columns(self) -> None:
+ our_stuff = self.properties
+
+ # extract columns from the class dict
+ declared_columns = self.declared_columns
+ column_ordering = self.column_ordering
+ name_to_prop_key = collections.defaultdict(set)
+
+ for key, c in list(our_stuff.items()):
+ if isinstance(c, _MapsColumns):
+ mp_to_assign = c.mapper_property_to_assign
+ if mp_to_assign:
+ our_stuff[key] = mp_to_assign
+ else:
+ # if no mapper property to assign, this currently means
+ # this is a MappedColumn that will produce a Column for us
+ del our_stuff[key]
+
+ for col, sort_order in c.columns_to_assign:
+ if not isinstance(c, CompositeProperty):
+ name_to_prop_key[col.name].add(key)
+ declared_columns.add(col)
+
+ # we would assert this, however we want the below
+ # warning to take effect instead. See #9630
+ # assert col not in column_ordering
+
+ column_ordering[col] = sort_order
+
+ # if this is a MappedColumn and the attribute key we
+ # have is not what the column has for its key, map the
+ # Column explicitly under the attribute key name.
+ # otherwise, Mapper will map it under the column key.
+ if mp_to_assign is None and key != col.key:
+ our_stuff[key] = col
+ elif isinstance(c, Column):
+ # undefer previously occurred here, and now occurs earlier.
+ # ensure every column we get here has been named
+ assert c.name is not None
+ name_to_prop_key[c.name].add(key)
+ declared_columns.add(c)
+ # if the column is the same name as the key,
+ # remove it from the explicit properties dict.
+ # the normal rules for assigning column-based properties
+ # will take over, including precedence of columns
+ # in multi-column ColumnProperties.
+ if key == c.key:
+ del our_stuff[key]
+
+ for name, keys in name_to_prop_key.items():
+ if len(keys) > 1:
+ util.warn(
+ "On class %r, Column object %r named "
+ "directly multiple times, "
+ "only one will be used: %s. "
+ "Consider using orm.synonym instead"
+ % (self.classname, name, (", ".join(sorted(keys))))
+ )
+
+ def _setup_table(self, table: Optional[FromClause] = None) -> None:
+ cls = self.cls
+ cls_as_Decl = cast("MappedClassProtocol[Any]", cls)
+
+ tablename = self.tablename
+ table_args = self.table_args
+ clsdict_view = self.clsdict_view
+ declared_columns = self.declared_columns
+ column_ordering = self.column_ordering
+
+ manager = attributes.manager_of_class(cls)
+
+ if "__table__" not in clsdict_view and table is None:
+ if hasattr(cls, "__table_cls__"):
+ table_cls = cast(
+ Type[Table],
+ util.unbound_method_to_callable(cls.__table_cls__), # type: ignore # noqa: E501
+ )
+ else:
+ table_cls = Table
+
+ if tablename is not None:
+ args: Tuple[Any, ...] = ()
+ table_kw: Dict[str, Any] = {}
+
+ if table_args:
+ if isinstance(table_args, dict):
+ table_kw = table_args
+ elif isinstance(table_args, tuple):
+ if isinstance(table_args[-1], dict):
+ args, table_kw = table_args[0:-1], table_args[-1]
+ else:
+ args = table_args
+
+ autoload_with = clsdict_view.get("__autoload_with__")
+ if autoload_with:
+ table_kw["autoload_with"] = autoload_with
+
+ autoload = clsdict_view.get("__autoload__")
+ if autoload:
+ table_kw["autoload"] = True
+
+ sorted_columns = sorted(
+ declared_columns,
+ key=lambda c: column_ordering.get(c, 0),
+ )
+ table = self.set_cls_attribute(
+ "__table__",
+ table_cls(
+ tablename,
+ self._metadata_for_cls(manager),
+ *sorted_columns,
+ *args,
+ **table_kw,
+ ),
+ )
+ else:
+ if table is None:
+ table = cls_as_Decl.__table__
+ if declared_columns:
+ for c in declared_columns:
+ if not table.c.contains_column(c):
+ raise exc.ArgumentError(
+ "Can't add additional column %r when "
+ "specifying __table__" % c.key
+ )
+
+ self.local_table = table
+
+ def _metadata_for_cls(self, manager: ClassManager[Any]) -> MetaData:
+ meta: Optional[MetaData] = getattr(self.cls, "metadata", None)
+ if meta is not None:
+ return meta
+ else:
+ return manager.registry.metadata
+
+ def _setup_inheriting_mapper(self, mapper_kw: _MapperKwArgs) -> None:
+ cls = self.cls
+
+ inherits = mapper_kw.get("inherits", None)
+
+ if inherits is None:
+ # since we search for classical mappings now, search for
+ # multiple mapped bases as well and raise an error.
+ inherits_search = []
+ for base_ in cls.__bases__:
+ c = _resolve_for_abstract_or_classical(base_)
+ if c is None:
+ continue
+
+ if _is_supercls_for_inherits(c) and c not in inherits_search:
+ inherits_search.append(c)
+
+ if inherits_search:
+ if len(inherits_search) > 1:
+ raise exc.InvalidRequestError(
+ "Class %s has multiple mapped bases: %r"
+ % (cls, inherits_search)
+ )
+ inherits = inherits_search[0]
+ elif isinstance(inherits, Mapper):
+ inherits = inherits.class_
+
+ self.inherits = inherits
+
+ clsdict_view = self.clsdict_view
+ if "__table__" not in clsdict_view and self.tablename is None:
+ self.single = True
+
+ def _setup_inheriting_columns(self, mapper_kw: _MapperKwArgs) -> None:
+ table = self.local_table
+ cls = self.cls
+ table_args = self.table_args
+ declared_columns = self.declared_columns
+
+ if (
+ table is None
+ and self.inherits is None
+ and not _get_immediate_cls_attr(cls, "__no_table__")
+ ):
+ raise exc.InvalidRequestError(
+ "Class %r does not have a __table__ or __tablename__ "
+ "specified and does not inherit from an existing "
+ "table-mapped class." % cls
+ )
+ elif self.inherits:
+ inherited_mapper_or_config = _declared_mapping_info(self.inherits)
+ assert inherited_mapper_or_config is not None
+ inherited_table = inherited_mapper_or_config.local_table
+ inherited_persist_selectable = (
+ inherited_mapper_or_config.persist_selectable
+ )
+
+ if table is None:
+ # single table inheritance.
+ # ensure no table args
+ if table_args:
+ raise exc.ArgumentError(
+ "Can't place __table_args__ on an inherited class "
+ "with no table."
+ )
+
+ # add any columns declared here to the inherited table.
+ if declared_columns and not isinstance(inherited_table, Table):
+ raise exc.ArgumentError(
+ f"Can't declare columns on single-table-inherited "
+ f"subclass {self.cls}; superclass {self.inherits} "
+ "is not mapped to a Table"
+ )
+
+ for col in declared_columns:
+ assert inherited_table is not None
+ if col.name in inherited_table.c:
+ if inherited_table.c[col.name] is col:
+ continue
+ raise exc.ArgumentError(
+ f"Column '{col}' on class {cls.__name__} "
+ f"conflicts with existing column "
+ f"'{inherited_table.c[col.name]}'. If using "
+ f"Declarative, consider using the "
+ "use_existing_column parameter of mapped_column() "
+ "to resolve conflicts."
+ )
+ if col.primary_key:
+ raise exc.ArgumentError(
+ "Can't place primary key columns on an inherited "
+ "class with no table."
+ )
+
+ if TYPE_CHECKING:
+ assert isinstance(inherited_table, Table)
+
+ inherited_table.append_column(col)
+ if (
+ inherited_persist_selectable is not None
+ and inherited_persist_selectable is not inherited_table
+ ):
+ inherited_persist_selectable._refresh_for_new_column(
+ col
+ )
+
+ def _prepare_mapper_arguments(self, mapper_kw: _MapperKwArgs) -> None:
+ properties = self.properties
+
+ if self.mapper_args_fn:
+ mapper_args = self.mapper_args_fn()
+ else:
+ mapper_args = {}
+
+ if mapper_kw:
+ mapper_args.update(mapper_kw)
+
+ if "properties" in mapper_args:
+ properties = dict(properties)
+ properties.update(mapper_args["properties"])
+
+ # make sure that column copies are used rather
+ # than the original columns from any mixins
+ for k in ("version_id_col", "polymorphic_on"):
+ if k in mapper_args:
+ v = mapper_args[k]
+ mapper_args[k] = self.column_copies.get(v, v)
+
+ if "primary_key" in mapper_args:
+ mapper_args["primary_key"] = [
+ self.column_copies.get(v, v)
+ for v in util.to_list(mapper_args["primary_key"])
+ ]
+
+ if "inherits" in mapper_args:
+ inherits_arg = mapper_args["inherits"]
+ if isinstance(inherits_arg, Mapper):
+ inherits_arg = inherits_arg.class_
+
+ if inherits_arg is not self.inherits:
+ raise exc.InvalidRequestError(
+ "mapper inherits argument given for non-inheriting "
+ "class %s" % (mapper_args["inherits"])
+ )
+
+ if self.inherits:
+ mapper_args["inherits"] = self.inherits
+
+ if self.inherits and not mapper_args.get("concrete", False):
+ # note the superclass is expected to have a Mapper assigned and
+ # not be a deferred config, as this is called within map()
+ inherited_mapper = class_mapper(self.inherits, False)
+ inherited_table = inherited_mapper.local_table
+
+ # single or joined inheritance
+ # exclude any cols on the inherited table which are
+ # not mapped on the parent class, to avoid
+ # mapping columns specific to sibling/nephew classes
+ if "exclude_properties" not in mapper_args:
+ mapper_args["exclude_properties"] = exclude_properties = {
+ c.key
+ for c in inherited_table.c
+ if c not in inherited_mapper._columntoproperty
+ }.union(inherited_mapper.exclude_properties or ())
+ exclude_properties.difference_update(
+ [c.key for c in self.declared_columns]
+ )
+
+ # look through columns in the current mapper that
+ # are keyed to a propname different than the colname
+ # (if names were the same, we'd have popped it out above,
+ # in which case the mapper makes this combination).
+ # See if the superclass has a similar column property.
+ # If so, join them together.
+ for k, col in list(properties.items()):
+ if not isinstance(col, expression.ColumnElement):
+ continue
+ if k in inherited_mapper._props:
+ p = inherited_mapper._props[k]
+ if isinstance(p, ColumnProperty):
+ # note here we place the subclass column
+ # first. See [ticket:1892] for background.
+ properties[k] = [col] + p.columns
+ result_mapper_args = mapper_args.copy()
+ result_mapper_args["properties"] = properties
+ self.mapper_args = result_mapper_args
+
+ def map(self, mapper_kw: _MapperKwArgs = util.EMPTY_DICT) -> Mapper[Any]:
+ self._prepare_mapper_arguments(mapper_kw)
+ if hasattr(self.cls, "__mapper_cls__"):
+ mapper_cls = cast(
+ "Type[Mapper[Any]]",
+ util.unbound_method_to_callable(
+ self.cls.__mapper_cls__ # type: ignore
+ ),
+ )
+ else:
+ mapper_cls = Mapper
+
+ return self.set_cls_attribute(
+ "__mapper__",
+ mapper_cls(self.cls, self.local_table, **self.mapper_args),
+ )
+
+
+@util.preload_module("sqlalchemy.orm.decl_api")
+def _as_dc_declaredattr(
+ field_metadata: Mapping[str, Any], sa_dataclass_metadata_key: str
+) -> Any:
+ # wrap lambdas inside dataclass fields inside an ad-hoc declared_attr.
+ # we can't write it because field.metadata is immutable :( so we have
+ # to go through extra trouble to compare these
+ decl_api = util.preloaded.orm_decl_api
+ obj = field_metadata[sa_dataclass_metadata_key]
+ if callable(obj) and not isinstance(obj, decl_api.declared_attr):
+ return decl_api.declared_attr(obj)
+ else:
+ return obj
+
+
+class _DeferredMapperConfig(_ClassScanMapperConfig):
+ _cls: weakref.ref[Type[Any]]
+
+ is_deferred = True
+
+ _configs: util.OrderedDict[
+ weakref.ref[Type[Any]], _DeferredMapperConfig
+ ] = util.OrderedDict()
+
+ def _early_mapping(self, mapper_kw: _MapperKwArgs) -> None:
+ pass
+
+ # mypy disallows plain property override of variable
+ @property # type: ignore
+ def cls(self) -> Type[Any]:
+ return self._cls() # type: ignore
+
+ @cls.setter
+ def cls(self, class_: Type[Any]) -> None:
+ self._cls = weakref.ref(class_, self._remove_config_cls)
+ self._configs[self._cls] = self
+
+ @classmethod
+ def _remove_config_cls(cls, ref: weakref.ref[Type[Any]]) -> None:
+ cls._configs.pop(ref, None)
+
+ @classmethod
+ def has_cls(cls, class_: Type[Any]) -> bool:
+ # 2.6 fails on weakref if class_ is an old style class
+ return isinstance(class_, type) and weakref.ref(class_) in cls._configs
+
+ @classmethod
+ def raise_unmapped_for_cls(cls, class_: Type[Any]) -> NoReturn:
+ if hasattr(class_, "_sa_raise_deferred_config"):
+ class_._sa_raise_deferred_config()
+
+ raise orm_exc.UnmappedClassError(
+ class_,
+ msg=(
+ f"Class {orm_exc._safe_cls_name(class_)} has a deferred "
+ "mapping on it. It is not yet usable as a mapped class."
+ ),
+ )
+
+ @classmethod
+ def config_for_cls(cls, class_: Type[Any]) -> _DeferredMapperConfig:
+ return cls._configs[weakref.ref(class_)]
+
+ @classmethod
+ def classes_for_base(
+ cls, base_cls: Type[Any], sort: bool = True
+ ) -> List[_DeferredMapperConfig]:
+ classes_for_base = [
+ m
+ for m, cls_ in [(m, m.cls) for m in cls._configs.values()]
+ if cls_ is not None and issubclass(cls_, base_cls)
+ ]
+
+ if not sort:
+ return classes_for_base
+
+ all_m_by_cls = {m.cls: m for m in classes_for_base}
+
+ tuples: List[Tuple[_DeferredMapperConfig, _DeferredMapperConfig]] = []
+ for m_cls in all_m_by_cls:
+ tuples.extend(
+ (all_m_by_cls[base_cls], all_m_by_cls[m_cls])
+ for base_cls in m_cls.__bases__
+ if base_cls in all_m_by_cls
+ )
+ return list(topological.sort(tuples, classes_for_base))
+
+ def map(self, mapper_kw: _MapperKwArgs = util.EMPTY_DICT) -> Mapper[Any]:
+ self._configs.pop(self._cls, None)
+ return super().map(mapper_kw)
+
+
+def _add_attribute(
+ cls: Type[Any], key: str, value: MapperProperty[Any]
+) -> None:
+ """add an attribute to an existing declarative class.
+
+ This runs through the logic to determine MapperProperty,
+ adds it to the Mapper, adds a column to the mapped Table, etc.
+
+ """
+
+ if "__mapper__" in cls.__dict__:
+ mapped_cls = cast("MappedClassProtocol[Any]", cls)
+
+ def _table_or_raise(mc: MappedClassProtocol[Any]) -> Table:
+ if isinstance(mc.__table__, Table):
+ return mc.__table__
+ raise exc.InvalidRequestError(
+ f"Cannot add a new attribute to mapped class {mc.__name__!r} "
+ "because it's not mapped against a table."
+ )
+
+ if isinstance(value, Column):
+ _undefer_column_name(key, value)
+ _table_or_raise(mapped_cls).append_column(
+ value, replace_existing=True
+ )
+ mapped_cls.__mapper__.add_property(key, value)
+ elif isinstance(value, _MapsColumns):
+ mp = value.mapper_property_to_assign
+ for col, _ in value.columns_to_assign:
+ _undefer_column_name(key, col)
+ _table_or_raise(mapped_cls).append_column(
+ col, replace_existing=True
+ )
+ if not mp:
+ mapped_cls.__mapper__.add_property(key, col)
+ if mp:
+ mapped_cls.__mapper__.add_property(key, mp)
+ elif isinstance(value, MapperProperty):
+ mapped_cls.__mapper__.add_property(key, value)
+ elif isinstance(value, QueryableAttribute) and value.key != key:
+ # detect a QueryableAttribute that's already mapped being
+ # assigned elsewhere in userland, turn into a synonym()
+ value = SynonymProperty(value.key)
+ mapped_cls.__mapper__.add_property(key, value)
+ else:
+ type.__setattr__(cls, key, value)
+ mapped_cls.__mapper__._expire_memoizations()
+ else:
+ type.__setattr__(cls, key, value)
+
+
+def _del_attribute(cls: Type[Any], key: str) -> None:
+ if (
+ "__mapper__" in cls.__dict__
+ and key in cls.__dict__
+ and not cast(
+ "MappedClassProtocol[Any]", cls
+ ).__mapper__._dispose_called
+ ):
+ value = cls.__dict__[key]
+ if isinstance(
+ value, (Column, _MapsColumns, MapperProperty, QueryableAttribute)
+ ):
+ raise NotImplementedError(
+ "Can't un-map individual mapped attributes on a mapped class."
+ )
+ else:
+ type.__delattr__(cls, key)
+ cast(
+ "MappedClassProtocol[Any]", cls
+ ).__mapper__._expire_memoizations()
+ else:
+ type.__delattr__(cls, key)
+
+
+def _declarative_constructor(self: Any, **kwargs: Any) -> None:
+ """A simple constructor that allows initialization from kwargs.
+
+ Sets attributes on the constructed instance using the names and
+ values in ``kwargs``.
+
+ Only keys that are present as
+ attributes of the instance's class are allowed. These could be,
+ for example, any mapped columns or relationships.
+ """
+ cls_ = type(self)
+ for k in kwargs:
+ if not hasattr(cls_, k):
+ raise TypeError(
+ "%r is an invalid keyword argument for %s" % (k, cls_.__name__)
+ )
+ setattr(self, k, kwargs[k])
+
+
+_declarative_constructor.__name__ = "__init__"
+
+
+def _undefer_column_name(key: str, column: Column[Any]) -> None:
+ if column.key is None:
+ column.key = key
+ if column.name is None:
+ column.name = key
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py
new file mode 100644
index 0000000..71c06fb
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py
@@ -0,0 +1,1304 @@
+# orm/dependency.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: ignore-errors
+
+
+"""Relationship dependencies.
+
+"""
+
+from __future__ import annotations
+
+from . import attributes
+from . import exc
+from . import sync
+from . import unitofwork
+from . import util as mapperutil
+from .interfaces import MANYTOMANY
+from .interfaces import MANYTOONE
+from .interfaces import ONETOMANY
+from .. import exc as sa_exc
+from .. import sql
+from .. import util
+
+
+class DependencyProcessor:
+ def __init__(self, prop):
+ self.prop = prop
+ self.cascade = prop.cascade
+ self.mapper = prop.mapper
+ self.parent = prop.parent
+ self.secondary = prop.secondary
+ self.direction = prop.direction
+ self.post_update = prop.post_update
+ self.passive_deletes = prop.passive_deletes
+ self.passive_updates = prop.passive_updates
+ self.enable_typechecks = prop.enable_typechecks
+ if self.passive_deletes:
+ self._passive_delete_flag = attributes.PASSIVE_NO_INITIALIZE
+ else:
+ self._passive_delete_flag = attributes.PASSIVE_OFF
+ if self.passive_updates:
+ self._passive_update_flag = attributes.PASSIVE_NO_INITIALIZE
+ else:
+ self._passive_update_flag = attributes.PASSIVE_OFF
+
+ self.sort_key = "%s_%s" % (self.parent._sort_key, prop.key)
+ self.key = prop.key
+ if not self.prop.synchronize_pairs:
+ raise sa_exc.ArgumentError(
+ "Can't build a DependencyProcessor for relationship %s. "
+ "No target attributes to populate between parent and "
+ "child are present" % self.prop
+ )
+
+ @classmethod
+ def from_relationship(cls, prop):
+ return _direction_to_processor[prop.direction](prop)
+
+ def hasparent(self, state):
+ """return True if the given object instance has a parent,
+ according to the ``InstrumentedAttribute`` handled by this
+ ``DependencyProcessor``.
+
+ """
+ return self.parent.class_manager.get_impl(self.key).hasparent(state)
+
+ def per_property_preprocessors(self, uow):
+ """establish actions and dependencies related to a flush.
+
+ These actions will operate on all relevant states in
+ the aggregate.
+
+ """
+ uow.register_preprocessor(self, True)
+
+ def per_property_flush_actions(self, uow):
+ after_save = unitofwork.ProcessAll(uow, self, False, True)
+ before_delete = unitofwork.ProcessAll(uow, self, True, True)
+
+ parent_saves = unitofwork.SaveUpdateAll(
+ uow, self.parent.primary_base_mapper
+ )
+ child_saves = unitofwork.SaveUpdateAll(
+ uow, self.mapper.primary_base_mapper
+ )
+
+ parent_deletes = unitofwork.DeleteAll(
+ uow, self.parent.primary_base_mapper
+ )
+ child_deletes = unitofwork.DeleteAll(
+ uow, self.mapper.primary_base_mapper
+ )
+
+ self.per_property_dependencies(
+ uow,
+ parent_saves,
+ child_saves,
+ parent_deletes,
+ child_deletes,
+ after_save,
+ before_delete,
+ )
+
+ def per_state_flush_actions(self, uow, states, isdelete):
+ """establish actions and dependencies related to a flush.
+
+ These actions will operate on all relevant states
+ individually. This occurs only if there are cycles
+ in the 'aggregated' version of events.
+
+ """
+
+ child_base_mapper = self.mapper.primary_base_mapper
+ child_saves = unitofwork.SaveUpdateAll(uow, child_base_mapper)
+ child_deletes = unitofwork.DeleteAll(uow, child_base_mapper)
+
+ # locate and disable the aggregate processors
+ # for this dependency
+
+ if isdelete:
+ before_delete = unitofwork.ProcessAll(uow, self, True, True)
+ before_delete.disabled = True
+ else:
+ after_save = unitofwork.ProcessAll(uow, self, False, True)
+ after_save.disabled = True
+
+ # check if the "child" side is part of the cycle
+
+ if child_saves not in uow.cycles:
+ # based on the current dependencies we use, the saves/
+ # deletes should always be in the 'cycles' collection
+ # together. if this changes, we will have to break up
+ # this method a bit more.
+ assert child_deletes not in uow.cycles
+
+ # child side is not part of the cycle, so we will link per-state
+ # actions to the aggregate "saves", "deletes" actions
+ child_actions = [(child_saves, False), (child_deletes, True)]
+ child_in_cycles = False
+ else:
+ child_in_cycles = True
+
+ # check if the "parent" side is part of the cycle
+ if not isdelete:
+ parent_saves = unitofwork.SaveUpdateAll(
+ uow, self.parent.base_mapper
+ )
+ parent_deletes = before_delete = None
+ if parent_saves in uow.cycles:
+ parent_in_cycles = True
+ else:
+ parent_deletes = unitofwork.DeleteAll(uow, self.parent.base_mapper)
+ parent_saves = after_save = None
+ if parent_deletes in uow.cycles:
+ parent_in_cycles = True
+
+ # now create actions /dependencies for each state.
+
+ for state in states:
+ # detect if there's anything changed or loaded
+ # by a preprocessor on this state/attribute. In the
+ # case of deletes we may try to load missing items here as well.
+ sum_ = state.manager[self.key].impl.get_all_pending(
+ state,
+ state.dict,
+ (
+ self._passive_delete_flag
+ if isdelete
+ else attributes.PASSIVE_NO_INITIALIZE
+ ),
+ )
+
+ if not sum_:
+ continue
+
+ if isdelete:
+ before_delete = unitofwork.ProcessState(uow, self, True, state)
+ if parent_in_cycles:
+ parent_deletes = unitofwork.DeleteState(uow, state)
+ else:
+ after_save = unitofwork.ProcessState(uow, self, False, state)
+ if parent_in_cycles:
+ parent_saves = unitofwork.SaveUpdateState(uow, state)
+
+ if child_in_cycles:
+ child_actions = []
+ for child_state, child in sum_:
+ if child_state not in uow.states:
+ child_action = (None, None)
+ else:
+ (deleted, listonly) = uow.states[child_state]
+ if deleted:
+ child_action = (
+ unitofwork.DeleteState(uow, child_state),
+ True,
+ )
+ else:
+ child_action = (
+ unitofwork.SaveUpdateState(uow, child_state),
+ False,
+ )
+ child_actions.append(child_action)
+
+ # establish dependencies between our possibly per-state
+ # parent action and our possibly per-state child action.
+ for child_action, childisdelete in child_actions:
+ self.per_state_dependencies(
+ uow,
+ parent_saves,
+ parent_deletes,
+ child_action,
+ after_save,
+ before_delete,
+ isdelete,
+ childisdelete,
+ )
+
+ def presort_deletes(self, uowcommit, states):
+ return False
+
+ def presort_saves(self, uowcommit, states):
+ return False
+
+ def process_deletes(self, uowcommit, states):
+ pass
+
+ def process_saves(self, uowcommit, states):
+ pass
+
+ def prop_has_changes(self, uowcommit, states, isdelete):
+ if not isdelete or self.passive_deletes:
+ passive = (
+ attributes.PASSIVE_NO_INITIALIZE
+ | attributes.INCLUDE_PENDING_MUTATIONS
+ )
+ elif self.direction is MANYTOONE:
+ # here, we were hoping to optimize having to fetch many-to-one
+ # for history and ignore it, if there's no further cascades
+ # to take place. however there are too many less common conditions
+ # that still take place and tests in test_relationships /
+ # test_cascade etc. will still fail.
+ passive = attributes.PASSIVE_NO_FETCH_RELATED
+ else:
+ passive = (
+ attributes.PASSIVE_OFF | attributes.INCLUDE_PENDING_MUTATIONS
+ )
+
+ for s in states:
+ # TODO: add a high speed method
+ # to InstanceState which returns: attribute
+ # has a non-None value, or had one
+ history = uowcommit.get_attribute_history(s, self.key, passive)
+ if history and not history.empty():
+ return True
+ else:
+ return (
+ states
+ and not self.prop._is_self_referential
+ and self.mapper in uowcommit.mappers
+ )
+
+ def _verify_canload(self, state):
+ if self.prop.uselist and state is None:
+ raise exc.FlushError(
+ "Can't flush None value found in "
+ "collection %s" % (self.prop,)
+ )
+ elif state is not None and not self.mapper._canload(
+ state, allow_subtypes=not self.enable_typechecks
+ ):
+ if self.mapper._canload(state, allow_subtypes=True):
+ raise exc.FlushError(
+ "Attempting to flush an item of type "
+ "%(x)s as a member of collection "
+ '"%(y)s". Expected an object of type '
+ "%(z)s or a polymorphic subclass of "
+ "this type. If %(x)s is a subclass of "
+ '%(z)s, configure mapper "%(zm)s" to '
+ "load this subtype polymorphically, or "
+ "set enable_typechecks=False to allow "
+ "any subtype to be accepted for flush. "
+ % {
+ "x": state.class_,
+ "y": self.prop,
+ "z": self.mapper.class_,
+ "zm": self.mapper,
+ }
+ )
+ else:
+ raise exc.FlushError(
+ "Attempting to flush an item of type "
+ "%(x)s as a member of collection "
+ '"%(y)s". Expected an object of type '
+ "%(z)s or a polymorphic subclass of "
+ "this type."
+ % {
+ "x": state.class_,
+ "y": self.prop,
+ "z": self.mapper.class_,
+ }
+ )
+
+ def _synchronize(self, state, child, associationrow, clearkeys, uowcommit):
+ raise NotImplementedError()
+
+ def _get_reversed_processed_set(self, uow):
+ if not self.prop._reverse_property:
+ return None
+
+ process_key = tuple(
+ sorted([self.key] + [p.key for p in self.prop._reverse_property])
+ )
+ return uow.memo(("reverse_key", process_key), set)
+
+ def _post_update(self, state, uowcommit, related, is_m2o_delete=False):
+ for x in related:
+ if not is_m2o_delete or x is not None:
+ uowcommit.register_post_update(
+ state, [r for l, r in self.prop.synchronize_pairs]
+ )
+ break
+
+ def _pks_changed(self, uowcommit, state):
+ raise NotImplementedError()
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self.prop)
+
+
+class OneToManyDP(DependencyProcessor):
+ def per_property_dependencies(
+ self,
+ uow,
+ parent_saves,
+ child_saves,
+ parent_deletes,
+ child_deletes,
+ after_save,
+ before_delete,
+ ):
+ if self.post_update:
+ child_post_updates = unitofwork.PostUpdateAll(
+ uow, self.mapper.primary_base_mapper, False
+ )
+ child_pre_updates = unitofwork.PostUpdateAll(
+ uow, self.mapper.primary_base_mapper, True
+ )
+
+ uow.dependencies.update(
+ [
+ (child_saves, after_save),
+ (parent_saves, after_save),
+ (after_save, child_post_updates),
+ (before_delete, child_pre_updates),
+ (child_pre_updates, parent_deletes),
+ (child_pre_updates, child_deletes),
+ ]
+ )
+ else:
+ uow.dependencies.update(
+ [
+ (parent_saves, after_save),
+ (after_save, child_saves),
+ (after_save, child_deletes),
+ (child_saves, parent_deletes),
+ (child_deletes, parent_deletes),
+ (before_delete, child_saves),
+ (before_delete, child_deletes),
+ ]
+ )
+
+ def per_state_dependencies(
+ self,
+ uow,
+ save_parent,
+ delete_parent,
+ child_action,
+ after_save,
+ before_delete,
+ isdelete,
+ childisdelete,
+ ):
+ if self.post_update:
+ child_post_updates = unitofwork.PostUpdateAll(
+ uow, self.mapper.primary_base_mapper, False
+ )
+ child_pre_updates = unitofwork.PostUpdateAll(
+ uow, self.mapper.primary_base_mapper, True
+ )
+
+ # TODO: this whole block is not covered
+ # by any tests
+ if not isdelete:
+ if childisdelete:
+ uow.dependencies.update(
+ [
+ (child_action, after_save),
+ (after_save, child_post_updates),
+ ]
+ )
+ else:
+ uow.dependencies.update(
+ [
+ (save_parent, after_save),
+ (child_action, after_save),
+ (after_save, child_post_updates),
+ ]
+ )
+ else:
+ if childisdelete:
+ uow.dependencies.update(
+ [
+ (before_delete, child_pre_updates),
+ (child_pre_updates, delete_parent),
+ ]
+ )
+ else:
+ uow.dependencies.update(
+ [
+ (before_delete, child_pre_updates),
+ (child_pre_updates, delete_parent),
+ ]
+ )
+ elif not isdelete:
+ uow.dependencies.update(
+ [
+ (save_parent, after_save),
+ (after_save, child_action),
+ (save_parent, child_action),
+ ]
+ )
+ else:
+ uow.dependencies.update(
+ [(before_delete, child_action), (child_action, delete_parent)]
+ )
+
+ def presort_deletes(self, uowcommit, states):
+ # head object is being deleted, and we manage its list of
+ # child objects the child objects have to have their
+ # foreign key to the parent set to NULL
+ should_null_fks = (
+ not self.cascade.delete and not self.passive_deletes == "all"
+ )
+
+ for state in states:
+ history = uowcommit.get_attribute_history(
+ state, self.key, self._passive_delete_flag
+ )
+ if history:
+ for child in history.deleted:
+ if child is not None and self.hasparent(child) is False:
+ if self.cascade.delete_orphan:
+ uowcommit.register_object(child, isdelete=True)
+ else:
+ uowcommit.register_object(child)
+
+ if should_null_fks:
+ for child in history.unchanged:
+ if child is not None:
+ uowcommit.register_object(
+ child, operation="delete", prop=self.prop
+ )
+
+ def presort_saves(self, uowcommit, states):
+ children_added = uowcommit.memo(("children_added", self), set)
+
+ should_null_fks = (
+ not self.cascade.delete_orphan
+ and not self.passive_deletes == "all"
+ )
+
+ for state in states:
+ pks_changed = self._pks_changed(uowcommit, state)
+
+ if not pks_changed or self.passive_updates:
+ passive = (
+ attributes.PASSIVE_NO_INITIALIZE
+ | attributes.INCLUDE_PENDING_MUTATIONS
+ )
+ else:
+ passive = (
+ attributes.PASSIVE_OFF
+ | attributes.INCLUDE_PENDING_MUTATIONS
+ )
+
+ history = uowcommit.get_attribute_history(state, self.key, passive)
+ if history:
+ for child in history.added:
+ if child is not None:
+ uowcommit.register_object(
+ child,
+ cancel_delete=True,
+ operation="add",
+ prop=self.prop,
+ )
+
+ children_added.update(history.added)
+
+ for child in history.deleted:
+ if not self.cascade.delete_orphan:
+ if should_null_fks:
+ uowcommit.register_object(
+ child,
+ isdelete=False,
+ operation="delete",
+ prop=self.prop,
+ )
+ elif self.hasparent(child) is False:
+ uowcommit.register_object(
+ child,
+ isdelete=True,
+ operation="delete",
+ prop=self.prop,
+ )
+ for c, m, st_, dct_ in self.mapper.cascade_iterator(
+ "delete", child
+ ):
+ uowcommit.register_object(st_, isdelete=True)
+
+ if pks_changed:
+ if history:
+ for child in history.unchanged:
+ if child is not None:
+ uowcommit.register_object(
+ child,
+ False,
+ self.passive_updates,
+ operation="pk change",
+ prop=self.prop,
+ )
+
+ def process_deletes(self, uowcommit, states):
+ # head object is being deleted, and we manage its list of
+ # child objects the child objects have to have their foreign
+ # key to the parent set to NULL this phase can be called
+ # safely for any cascade but is unnecessary if delete cascade
+ # is on.
+
+ if self.post_update or not self.passive_deletes == "all":
+ children_added = uowcommit.memo(("children_added", self), set)
+
+ for state in states:
+ history = uowcommit.get_attribute_history(
+ state, self.key, self._passive_delete_flag
+ )
+ if history:
+ for child in history.deleted:
+ if (
+ child is not None
+ and self.hasparent(child) is False
+ ):
+ self._synchronize(
+ state, child, None, True, uowcommit, False
+ )
+ if self.post_update and child:
+ self._post_update(child, uowcommit, [state])
+
+ if self.post_update or not self.cascade.delete:
+ for child in set(history.unchanged).difference(
+ children_added
+ ):
+ if child is not None:
+ self._synchronize(
+ state, child, None, True, uowcommit, False
+ )
+ if self.post_update and child:
+ self._post_update(
+ child, uowcommit, [state]
+ )
+
+ # technically, we can even remove each child from the
+ # collection here too. but this would be a somewhat
+ # inconsistent behavior since it wouldn't happen
+ # if the old parent wasn't deleted but child was moved.
+
+ def process_saves(self, uowcommit, states):
+ should_null_fks = (
+ not self.cascade.delete_orphan
+ and not self.passive_deletes == "all"
+ )
+
+ for state in states:
+ history = uowcommit.get_attribute_history(
+ state, self.key, attributes.PASSIVE_NO_INITIALIZE
+ )
+ if history:
+ for child in history.added:
+ self._synchronize(
+ state, child, None, False, uowcommit, False
+ )
+ if child is not None and self.post_update:
+ self._post_update(child, uowcommit, [state])
+
+ for child in history.deleted:
+ if (
+ should_null_fks
+ and not self.cascade.delete_orphan
+ and not self.hasparent(child)
+ ):
+ self._synchronize(
+ state, child, None, True, uowcommit, False
+ )
+
+ if self._pks_changed(uowcommit, state):
+ for child in history.unchanged:
+ self._synchronize(
+ state, child, None, False, uowcommit, True
+ )
+
+ def _synchronize(
+ self, state, child, associationrow, clearkeys, uowcommit, pks_changed
+ ):
+ source = state
+ dest = child
+ self._verify_canload(child)
+ if dest is None or (
+ not self.post_update and uowcommit.is_deleted(dest)
+ ):
+ return
+ if clearkeys:
+ sync.clear(dest, self.mapper, self.prop.synchronize_pairs)
+ else:
+ sync.populate(
+ source,
+ self.parent,
+ dest,
+ self.mapper,
+ self.prop.synchronize_pairs,
+ uowcommit,
+ self.passive_updates and pks_changed,
+ )
+
+ def _pks_changed(self, uowcommit, state):
+ return sync.source_modified(
+ uowcommit, state, self.parent, self.prop.synchronize_pairs
+ )
+
+
+class ManyToOneDP(DependencyProcessor):
+ def __init__(self, prop):
+ DependencyProcessor.__init__(self, prop)
+ for mapper in self.mapper.self_and_descendants:
+ mapper._dependency_processors.append(DetectKeySwitch(prop))
+
+ def per_property_dependencies(
+ self,
+ uow,
+ parent_saves,
+ child_saves,
+ parent_deletes,
+ child_deletes,
+ after_save,
+ before_delete,
+ ):
+ if self.post_update:
+ parent_post_updates = unitofwork.PostUpdateAll(
+ uow, self.parent.primary_base_mapper, False
+ )
+ parent_pre_updates = unitofwork.PostUpdateAll(
+ uow, self.parent.primary_base_mapper, True
+ )
+
+ uow.dependencies.update(
+ [
+ (child_saves, after_save),
+ (parent_saves, after_save),
+ (after_save, parent_post_updates),
+ (after_save, parent_pre_updates),
+ (before_delete, parent_pre_updates),
+ (parent_pre_updates, child_deletes),
+ (parent_pre_updates, parent_deletes),
+ ]
+ )
+ else:
+ uow.dependencies.update(
+ [
+ (child_saves, after_save),
+ (after_save, parent_saves),
+ (parent_saves, child_deletes),
+ (parent_deletes, child_deletes),
+ ]
+ )
+
+ def per_state_dependencies(
+ self,
+ uow,
+ save_parent,
+ delete_parent,
+ child_action,
+ after_save,
+ before_delete,
+ isdelete,
+ childisdelete,
+ ):
+ if self.post_update:
+ if not isdelete:
+ parent_post_updates = unitofwork.PostUpdateAll(
+ uow, self.parent.primary_base_mapper, False
+ )
+ if childisdelete:
+ uow.dependencies.update(
+ [
+ (after_save, parent_post_updates),
+ (parent_post_updates, child_action),
+ ]
+ )
+ else:
+ uow.dependencies.update(
+ [
+ (save_parent, after_save),
+ (child_action, after_save),
+ (after_save, parent_post_updates),
+ ]
+ )
+ else:
+ parent_pre_updates = unitofwork.PostUpdateAll(
+ uow, self.parent.primary_base_mapper, True
+ )
+
+ uow.dependencies.update(
+ [
+ (before_delete, parent_pre_updates),
+ (parent_pre_updates, delete_parent),
+ (parent_pre_updates, child_action),
+ ]
+ )
+
+ elif not isdelete:
+ if not childisdelete:
+ uow.dependencies.update(
+ [(child_action, after_save), (after_save, save_parent)]
+ )
+ else:
+ uow.dependencies.update([(after_save, save_parent)])
+
+ else:
+ if childisdelete:
+ uow.dependencies.update([(delete_parent, child_action)])
+
+ def presort_deletes(self, uowcommit, states):
+ if self.cascade.delete or self.cascade.delete_orphan:
+ for state in states:
+ history = uowcommit.get_attribute_history(
+ state, self.key, self._passive_delete_flag
+ )
+ if history:
+ if self.cascade.delete_orphan:
+ todelete = history.sum()
+ else:
+ todelete = history.non_deleted()
+ for child in todelete:
+ if child is None:
+ continue
+ uowcommit.register_object(
+ child,
+ isdelete=True,
+ operation="delete",
+ prop=self.prop,
+ )
+ t = self.mapper.cascade_iterator("delete", child)
+ for c, m, st_, dct_ in t:
+ uowcommit.register_object(st_, isdelete=True)
+
+ def presort_saves(self, uowcommit, states):
+ for state in states:
+ uowcommit.register_object(state, operation="add", prop=self.prop)
+ if self.cascade.delete_orphan:
+ history = uowcommit.get_attribute_history(
+ state, self.key, self._passive_delete_flag
+ )
+ if history:
+ for child in history.deleted:
+ if self.hasparent(child) is False:
+ uowcommit.register_object(
+ child,
+ isdelete=True,
+ operation="delete",
+ prop=self.prop,
+ )
+
+ t = self.mapper.cascade_iterator("delete", child)
+ for c, m, st_, dct_ in t:
+ uowcommit.register_object(st_, isdelete=True)
+
+ def process_deletes(self, uowcommit, states):
+ if (
+ self.post_update
+ and not self.cascade.delete_orphan
+ and not self.passive_deletes == "all"
+ ):
+ # post_update means we have to update our
+ # row to not reference the child object
+ # before we can DELETE the row
+ for state in states:
+ self._synchronize(state, None, None, True, uowcommit)
+ if state and self.post_update:
+ history = uowcommit.get_attribute_history(
+ state, self.key, self._passive_delete_flag
+ )
+ if history:
+ self._post_update(
+ state, uowcommit, history.sum(), is_m2o_delete=True
+ )
+
+ def process_saves(self, uowcommit, states):
+ for state in states:
+ history = uowcommit.get_attribute_history(
+ state, self.key, attributes.PASSIVE_NO_INITIALIZE
+ )
+ if history:
+ if history.added:
+ for child in history.added:
+ self._synchronize(
+ state, child, None, False, uowcommit, "add"
+ )
+ elif history.deleted:
+ self._synchronize(
+ state, None, None, True, uowcommit, "delete"
+ )
+ if self.post_update:
+ self._post_update(state, uowcommit, history.sum())
+
+ def _synchronize(
+ self,
+ state,
+ child,
+ associationrow,
+ clearkeys,
+ uowcommit,
+ operation=None,
+ ):
+ if state is None or (
+ not self.post_update and uowcommit.is_deleted(state)
+ ):
+ return
+
+ if (
+ operation is not None
+ and child is not None
+ and not uowcommit.session._contains_state(child)
+ ):
+ util.warn(
+ "Object of type %s not in session, %s "
+ "operation along '%s' won't proceed"
+ % (mapperutil.state_class_str(child), operation, self.prop)
+ )
+ return
+
+ if clearkeys or child is None:
+ sync.clear(state, self.parent, self.prop.synchronize_pairs)
+ else:
+ self._verify_canload(child)
+ sync.populate(
+ child,
+ self.mapper,
+ state,
+ self.parent,
+ self.prop.synchronize_pairs,
+ uowcommit,
+ False,
+ )
+
+
+class DetectKeySwitch(DependencyProcessor):
+ """For many-to-one relationships with no one-to-many backref,
+ searches for parents through the unit of work when a primary
+ key has changed and updates them.
+
+ Theoretically, this approach could be expanded to support transparent
+ deletion of objects referenced via many-to-one as well, although
+ the current attribute system doesn't do enough bookkeeping for this
+ to be efficient.
+
+ """
+
+ def per_property_preprocessors(self, uow):
+ if self.prop._reverse_property:
+ if self.passive_updates:
+ return
+ else:
+ if False in (
+ prop.passive_updates
+ for prop in self.prop._reverse_property
+ ):
+ return
+
+ uow.register_preprocessor(self, False)
+
+ def per_property_flush_actions(self, uow):
+ parent_saves = unitofwork.SaveUpdateAll(uow, self.parent.base_mapper)
+ after_save = unitofwork.ProcessAll(uow, self, False, False)
+ uow.dependencies.update([(parent_saves, after_save)])
+
+ def per_state_flush_actions(self, uow, states, isdelete):
+ pass
+
+ def presort_deletes(self, uowcommit, states):
+ pass
+
+ def presort_saves(self, uow, states):
+ if not self.passive_updates:
+ # for non-passive updates, register in the preprocess stage
+ # so that mapper save_obj() gets a hold of changes
+ self._process_key_switches(states, uow)
+
+ def prop_has_changes(self, uow, states, isdelete):
+ if not isdelete and self.passive_updates:
+ d = self._key_switchers(uow, states)
+ return bool(d)
+
+ return False
+
+ def process_deletes(self, uowcommit, states):
+ assert False
+
+ def process_saves(self, uowcommit, states):
+ # for passive updates, register objects in the process stage
+ # so that we avoid ManyToOneDP's registering the object without
+ # the listonly flag in its own preprocess stage (results in UPDATE)
+ # statements being emitted
+ assert self.passive_updates
+ self._process_key_switches(states, uowcommit)
+
+ def _key_switchers(self, uow, states):
+ switched, notswitched = uow.memo(
+ ("pk_switchers", self), lambda: (set(), set())
+ )
+
+ allstates = switched.union(notswitched)
+ for s in states:
+ if s not in allstates:
+ if self._pks_changed(uow, s):
+ switched.add(s)
+ else:
+ notswitched.add(s)
+ return switched
+
+ def _process_key_switches(self, deplist, uowcommit):
+ switchers = self._key_switchers(uowcommit, deplist)
+ if switchers:
+ # if primary key values have actually changed somewhere, perform
+ # a linear search through the UOW in search of a parent.
+ for state in uowcommit.session.identity_map.all_states():
+ if not issubclass(state.class_, self.parent.class_):
+ continue
+ dict_ = state.dict
+ related = state.get_impl(self.key).get(
+ state, dict_, passive=self._passive_update_flag
+ )
+ if (
+ related is not attributes.PASSIVE_NO_RESULT
+ and related is not None
+ ):
+ if self.prop.uselist:
+ if not related:
+ continue
+ related_obj = related[0]
+ else:
+ related_obj = related
+ related_state = attributes.instance_state(related_obj)
+ if related_state in switchers:
+ uowcommit.register_object(
+ state, False, self.passive_updates
+ )
+ sync.populate(
+ related_state,
+ self.mapper,
+ state,
+ self.parent,
+ self.prop.synchronize_pairs,
+ uowcommit,
+ self.passive_updates,
+ )
+
+ def _pks_changed(self, uowcommit, state):
+ return bool(state.key) and sync.source_modified(
+ uowcommit, state, self.mapper, self.prop.synchronize_pairs
+ )
+
+
+class ManyToManyDP(DependencyProcessor):
+ def per_property_dependencies(
+ self,
+ uow,
+ parent_saves,
+ child_saves,
+ parent_deletes,
+ child_deletes,
+ after_save,
+ before_delete,
+ ):
+ uow.dependencies.update(
+ [
+ (parent_saves, after_save),
+ (child_saves, after_save),
+ (after_save, child_deletes),
+ # a rowswitch on the parent from deleted to saved
+ # can make this one occur, as the "save" may remove
+ # an element from the
+ # "deleted" list before we have a chance to
+ # process its child rows
+ (before_delete, parent_saves),
+ (before_delete, parent_deletes),
+ (before_delete, child_deletes),
+ (before_delete, child_saves),
+ ]
+ )
+
+ def per_state_dependencies(
+ self,
+ uow,
+ save_parent,
+ delete_parent,
+ child_action,
+ after_save,
+ before_delete,
+ isdelete,
+ childisdelete,
+ ):
+ if not isdelete:
+ if childisdelete:
+ uow.dependencies.update(
+ [(save_parent, after_save), (after_save, child_action)]
+ )
+ else:
+ uow.dependencies.update(
+ [(save_parent, after_save), (child_action, after_save)]
+ )
+ else:
+ uow.dependencies.update(
+ [(before_delete, child_action), (before_delete, delete_parent)]
+ )
+
+ def presort_deletes(self, uowcommit, states):
+ # TODO: no tests fail if this whole
+ # thing is removed !!!!
+ if not self.passive_deletes:
+ # if no passive deletes, load history on
+ # the collection, so that prop_has_changes()
+ # returns True
+ for state in states:
+ uowcommit.get_attribute_history(
+ state, self.key, self._passive_delete_flag
+ )
+
+ def presort_saves(self, uowcommit, states):
+ if not self.passive_updates:
+ # if no passive updates, load history on
+ # each collection where parent has changed PK,
+ # so that prop_has_changes() returns True
+ for state in states:
+ if self._pks_changed(uowcommit, state):
+ history = uowcommit.get_attribute_history(
+ state, self.key, attributes.PASSIVE_OFF
+ )
+
+ if not self.cascade.delete_orphan:
+ return
+
+ # check for child items removed from the collection
+ # if delete_orphan check is turned on.
+ for state in states:
+ history = uowcommit.get_attribute_history(
+ state, self.key, attributes.PASSIVE_NO_INITIALIZE
+ )
+ if history:
+ for child in history.deleted:
+ if self.hasparent(child) is False:
+ uowcommit.register_object(
+ child,
+ isdelete=True,
+ operation="delete",
+ prop=self.prop,
+ )
+ for c, m, st_, dct_ in self.mapper.cascade_iterator(
+ "delete", child
+ ):
+ uowcommit.register_object(st_, isdelete=True)
+
+ def process_deletes(self, uowcommit, states):
+ secondary_delete = []
+ secondary_insert = []
+ secondary_update = []
+
+ processed = self._get_reversed_processed_set(uowcommit)
+ tmp = set()
+ for state in states:
+ # this history should be cached already, as
+ # we loaded it in preprocess_deletes
+ history = uowcommit.get_attribute_history(
+ state, self.key, self._passive_delete_flag
+ )
+ if history:
+ for child in history.non_added():
+ if child is None or (
+ processed is not None and (state, child) in processed
+ ):
+ continue
+ associationrow = {}
+ if not self._synchronize(
+ state,
+ child,
+ associationrow,
+ False,
+ uowcommit,
+ "delete",
+ ):
+ continue
+ secondary_delete.append(associationrow)
+
+ tmp.update((c, state) for c in history.non_added())
+
+ if processed is not None:
+ processed.update(tmp)
+
+ self._run_crud(
+ uowcommit, secondary_insert, secondary_update, secondary_delete
+ )
+
+ def process_saves(self, uowcommit, states):
+ secondary_delete = []
+ secondary_insert = []
+ secondary_update = []
+
+ processed = self._get_reversed_processed_set(uowcommit)
+ tmp = set()
+
+ for state in states:
+ need_cascade_pks = not self.passive_updates and self._pks_changed(
+ uowcommit, state
+ )
+ if need_cascade_pks:
+ passive = (
+ attributes.PASSIVE_OFF
+ | attributes.INCLUDE_PENDING_MUTATIONS
+ )
+ else:
+ passive = (
+ attributes.PASSIVE_NO_INITIALIZE
+ | attributes.INCLUDE_PENDING_MUTATIONS
+ )
+ history = uowcommit.get_attribute_history(state, self.key, passive)
+ if history:
+ for child in history.added:
+ if processed is not None and (state, child) in processed:
+ continue
+ associationrow = {}
+ if not self._synchronize(
+ state, child, associationrow, False, uowcommit, "add"
+ ):
+ continue
+ secondary_insert.append(associationrow)
+ for child in history.deleted:
+ if processed is not None and (state, child) in processed:
+ continue
+ associationrow = {}
+ if not self._synchronize(
+ state,
+ child,
+ associationrow,
+ False,
+ uowcommit,
+ "delete",
+ ):
+ continue
+ secondary_delete.append(associationrow)
+
+ tmp.update((c, state) for c in history.added + history.deleted)
+
+ if need_cascade_pks:
+ for child in history.unchanged:
+ associationrow = {}
+ sync.update(
+ state,
+ self.parent,
+ associationrow,
+ "old_",
+ self.prop.synchronize_pairs,
+ )
+ sync.update(
+ child,
+ self.mapper,
+ associationrow,
+ "old_",
+ self.prop.secondary_synchronize_pairs,
+ )
+
+ secondary_update.append(associationrow)
+
+ if processed is not None:
+ processed.update(tmp)
+
+ self._run_crud(
+ uowcommit, secondary_insert, secondary_update, secondary_delete
+ )
+
+ def _run_crud(
+ self, uowcommit, secondary_insert, secondary_update, secondary_delete
+ ):
+ connection = uowcommit.transaction.connection(self.mapper)
+
+ if secondary_delete:
+ associationrow = secondary_delete[0]
+ statement = self.secondary.delete().where(
+ sql.and_(
+ *[
+ c == sql.bindparam(c.key, type_=c.type)
+ for c in self.secondary.c
+ if c.key in associationrow
+ ]
+ )
+ )
+ result = connection.execute(statement, secondary_delete)
+
+ if (
+ result.supports_sane_multi_rowcount()
+ ) and result.rowcount != len(secondary_delete):
+ raise exc.StaleDataError(
+ "DELETE statement on table '%s' expected to delete "
+ "%d row(s); Only %d were matched."
+ % (
+ self.secondary.description,
+ len(secondary_delete),
+ result.rowcount,
+ )
+ )
+
+ if secondary_update:
+ associationrow = secondary_update[0]
+ statement = self.secondary.update().where(
+ sql.and_(
+ *[
+ c == sql.bindparam("old_" + c.key, type_=c.type)
+ for c in self.secondary.c
+ if c.key in associationrow
+ ]
+ )
+ )
+ result = connection.execute(statement, secondary_update)
+
+ if (
+ result.supports_sane_multi_rowcount()
+ ) and result.rowcount != len(secondary_update):
+ raise exc.StaleDataError(
+ "UPDATE statement on table '%s' expected to update "
+ "%d row(s); Only %d were matched."
+ % (
+ self.secondary.description,
+ len(secondary_update),
+ result.rowcount,
+ )
+ )
+
+ if secondary_insert:
+ statement = self.secondary.insert()
+ connection.execute(statement, secondary_insert)
+
+ def _synchronize(
+ self, state, child, associationrow, clearkeys, uowcommit, operation
+ ):
+ # this checks for None if uselist=True
+ self._verify_canload(child)
+
+ # but if uselist=False we get here. If child is None,
+ # no association row can be generated, so return.
+ if child is None:
+ return False
+
+ if child is not None and not uowcommit.session._contains_state(child):
+ if not child.deleted:
+ util.warn(
+ "Object of type %s not in session, %s "
+ "operation along '%s' won't proceed"
+ % (mapperutil.state_class_str(child), operation, self.prop)
+ )
+ return False
+
+ sync.populate_dict(
+ state, self.parent, associationrow, self.prop.synchronize_pairs
+ )
+ sync.populate_dict(
+ child,
+ self.mapper,
+ associationrow,
+ self.prop.secondary_synchronize_pairs,
+ )
+
+ return True
+
+ def _pks_changed(self, uowcommit, state):
+ return sync.source_modified(
+ uowcommit, state, self.parent, self.prop.synchronize_pairs
+ )
+
+
+_direction_to_processor = {
+ ONETOMANY: OneToManyDP,
+ MANYTOONE: ManyToOneDP,
+ MANYTOMANY: ManyToManyDP,
+}
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/descriptor_props.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/descriptor_props.py
new file mode 100644
index 0000000..a3650f5
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/descriptor_props.py
@@ -0,0 +1,1074 @@
+# orm/descriptor_props.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""Descriptor properties are more "auxiliary" properties
+that exist as configurational elements, but don't participate
+as actively in the load/persist ORM loop.
+
+"""
+from __future__ import annotations
+
+from dataclasses import is_dataclass
+import inspect
+import itertools
+import operator
+import typing
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import NoReturn
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import attributes
+from . import util as orm_util
+from .base import _DeclarativeMapped
+from .base import LoaderCallableStatus
+from .base import Mapped
+from .base import PassiveFlag
+from .base import SQLORMOperations
+from .interfaces import _AttributeOptions
+from .interfaces import _IntrospectsAnnotations
+from .interfaces import _MapsColumns
+from .interfaces import MapperProperty
+from .interfaces import PropComparator
+from .util import _none_set
+from .util import de_stringify_annotation
+from .. import event
+from .. import exc as sa_exc
+from .. import schema
+from .. import sql
+from .. import util
+from ..sql import expression
+from ..sql import operators
+from ..sql.elements import BindParameter
+from ..util.typing import is_fwd_ref
+from ..util.typing import is_pep593
+from ..util.typing import typing_get_args
+
+if typing.TYPE_CHECKING:
+ from ._typing import _InstanceDict
+ from ._typing import _RegistryType
+ from .attributes import History
+ from .attributes import InstrumentedAttribute
+ from .attributes import QueryableAttribute
+ from .context import ORMCompileState
+ from .decl_base import _ClassScanMapperConfig
+ from .mapper import Mapper
+ from .properties import ColumnProperty
+ from .properties import MappedColumn
+ from .state import InstanceState
+ from ..engine.base import Connection
+ from ..engine.row import Row
+ from ..sql._typing import _DMLColumnArgument
+ from ..sql._typing import _InfoType
+ from ..sql.elements import ClauseList
+ from ..sql.elements import ColumnElement
+ from ..sql.operators import OperatorType
+ from ..sql.schema import Column
+ from ..sql.selectable import Select
+ from ..util.typing import _AnnotationScanType
+ from ..util.typing import CallableReference
+ from ..util.typing import DescriptorReference
+ from ..util.typing import RODescriptorReference
+
+_T = TypeVar("_T", bound=Any)
+_PT = TypeVar("_PT", bound=Any)
+
+
+class DescriptorProperty(MapperProperty[_T]):
+ """:class:`.MapperProperty` which proxies access to a
+ user-defined descriptor."""
+
+ doc: Optional[str] = None
+
+ uses_objects = False
+ _links_to_entity = False
+
+ descriptor: DescriptorReference[Any]
+
+ def get_history(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ ) -> History:
+ raise NotImplementedError()
+
+ def instrument_class(self, mapper: Mapper[Any]) -> None:
+ prop = self
+
+ class _ProxyImpl(attributes.AttributeImpl):
+ accepts_scalar_loader = False
+ load_on_unexpire = True
+ collection = False
+
+ @property
+ def uses_objects(self) -> bool: # type: ignore
+ return prop.uses_objects
+
+ def __init__(self, key: str):
+ self.key = key
+
+ def get_history(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ ) -> History:
+ return prop.get_history(state, dict_, passive)
+
+ if self.descriptor is None:
+ desc = getattr(mapper.class_, self.key, None)
+ if mapper._is_userland_descriptor(self.key, desc):
+ self.descriptor = desc
+
+ if self.descriptor is None:
+
+ def fset(obj: Any, value: Any) -> None:
+ setattr(obj, self.name, value)
+
+ def fdel(obj: Any) -> None:
+ delattr(obj, self.name)
+
+ def fget(obj: Any) -> Any:
+ return getattr(obj, self.name)
+
+ self.descriptor = property(fget=fget, fset=fset, fdel=fdel)
+
+ proxy_attr = attributes.create_proxied_attribute(self.descriptor)(
+ self.parent.class_,
+ self.key,
+ self.descriptor,
+ lambda: self._comparator_factory(mapper),
+ doc=self.doc,
+ original_property=self,
+ )
+ proxy_attr.impl = _ProxyImpl(self.key)
+ mapper.class_manager.instrument_attribute(self.key, proxy_attr)
+
+
+_CompositeAttrType = Union[
+ str,
+ "Column[_T]",
+ "MappedColumn[_T]",
+ "InstrumentedAttribute[_T]",
+ "Mapped[_T]",
+]
+
+
+_CC = TypeVar("_CC", bound=Any)
+
+
+_composite_getters: weakref.WeakKeyDictionary[
+ Type[Any], Callable[[Any], Tuple[Any, ...]]
+] = weakref.WeakKeyDictionary()
+
+
+class CompositeProperty(
+ _MapsColumns[_CC], _IntrospectsAnnotations, DescriptorProperty[_CC]
+):
+ """Defines a "composite" mapped attribute, representing a collection
+ of columns as one attribute.
+
+ :class:`.CompositeProperty` is constructed using the :func:`.composite`
+ function.
+
+ .. seealso::
+
+ :ref:`mapper_composite`
+
+ """
+
+ composite_class: Union[Type[_CC], Callable[..., _CC]]
+ attrs: Tuple[_CompositeAttrType[Any], ...]
+
+ _generated_composite_accessor: CallableReference[
+ Optional[Callable[[_CC], Tuple[Any, ...]]]
+ ]
+
+ comparator_factory: Type[Comparator[_CC]]
+
+ def __init__(
+ self,
+ _class_or_attr: Union[
+ None, Type[_CC], Callable[..., _CC], _CompositeAttrType[Any]
+ ] = None,
+ *attrs: _CompositeAttrType[Any],
+ attribute_options: Optional[_AttributeOptions] = None,
+ active_history: bool = False,
+ deferred: bool = False,
+ group: Optional[str] = None,
+ comparator_factory: Optional[Type[Comparator[_CC]]] = None,
+ info: Optional[_InfoType] = None,
+ **kwargs: Any,
+ ):
+ super().__init__(attribute_options=attribute_options)
+
+ if isinstance(_class_or_attr, (Mapped, str, sql.ColumnElement)):
+ self.attrs = (_class_or_attr,) + attrs
+ # will initialize within declarative_scan
+ self.composite_class = None # type: ignore
+ else:
+ self.composite_class = _class_or_attr # type: ignore
+ self.attrs = attrs
+
+ self.active_history = active_history
+ self.deferred = deferred
+ self.group = group
+ self.comparator_factory = (
+ comparator_factory
+ if comparator_factory is not None
+ else self.__class__.Comparator
+ )
+ self._generated_composite_accessor = None
+ if info is not None:
+ self.info.update(info)
+
+ util.set_creation_order(self)
+ self._create_descriptor()
+ self._init_accessor()
+
+ def instrument_class(self, mapper: Mapper[Any]) -> None:
+ super().instrument_class(mapper)
+ self._setup_event_handlers()
+
+ def _composite_values_from_instance(self, value: _CC) -> Tuple[Any, ...]:
+ if self._generated_composite_accessor:
+ return self._generated_composite_accessor(value)
+ else:
+ try:
+ accessor = value.__composite_values__
+ except AttributeError as ae:
+ raise sa_exc.InvalidRequestError(
+ f"Composite class {self.composite_class.__name__} is not "
+ f"a dataclass and does not define a __composite_values__()"
+ " method; can't get state"
+ ) from ae
+ else:
+ return accessor() # type: ignore
+
+ def do_init(self) -> None:
+ """Initialization which occurs after the :class:`.Composite`
+ has been associated with its parent mapper.
+
+ """
+ self._setup_arguments_on_columns()
+
+ _COMPOSITE_FGET = object()
+
+ def _create_descriptor(self) -> None:
+ """Create the Python descriptor that will serve as
+ the access point on instances of the mapped class.
+
+ """
+
+ def fget(instance: Any) -> Any:
+ dict_ = attributes.instance_dict(instance)
+ state = attributes.instance_state(instance)
+
+ if self.key not in dict_:
+ # key not present. Iterate through related
+ # attributes, retrieve their values. This
+ # ensures they all load.
+ values = [
+ getattr(instance, key) for key in self._attribute_keys
+ ]
+
+ # current expected behavior here is that the composite is
+ # created on access if the object is persistent or if
+ # col attributes have non-None. This would be better
+ # if the composite were created unconditionally,
+ # but that would be a behavioral change.
+ if self.key not in dict_ and (
+ state.key is not None or not _none_set.issuperset(values)
+ ):
+ dict_[self.key] = self.composite_class(*values)
+ state.manager.dispatch.refresh(
+ state, self._COMPOSITE_FGET, [self.key]
+ )
+
+ return dict_.get(self.key, None)
+
+ def fset(instance: Any, value: Any) -> None:
+ dict_ = attributes.instance_dict(instance)
+ state = attributes.instance_state(instance)
+ attr = state.manager[self.key]
+
+ if attr.dispatch._active_history:
+ previous = fget(instance)
+ else:
+ previous = dict_.get(self.key, LoaderCallableStatus.NO_VALUE)
+
+ for fn in attr.dispatch.set:
+ value = fn(state, value, previous, attr.impl)
+ dict_[self.key] = value
+ if value is None:
+ for key in self._attribute_keys:
+ setattr(instance, key, None)
+ else:
+ for key, value in zip(
+ self._attribute_keys,
+ self._composite_values_from_instance(value),
+ ):
+ setattr(instance, key, value)
+
+ def fdel(instance: Any) -> None:
+ state = attributes.instance_state(instance)
+ dict_ = attributes.instance_dict(instance)
+ attr = state.manager[self.key]
+
+ if attr.dispatch._active_history:
+ previous = fget(instance)
+ dict_.pop(self.key, None)
+ else:
+ previous = dict_.pop(self.key, LoaderCallableStatus.NO_VALUE)
+
+ attr = state.manager[self.key]
+ attr.dispatch.remove(state, previous, attr.impl)
+ for key in self._attribute_keys:
+ setattr(instance, key, None)
+
+ self.descriptor = property(fget, fset, fdel)
+
+ @util.preload_module("sqlalchemy.orm.properties")
+ def declarative_scan(
+ self,
+ decl_scan: _ClassScanMapperConfig,
+ registry: _RegistryType,
+ cls: Type[Any],
+ originating_module: Optional[str],
+ key: str,
+ mapped_container: Optional[Type[Mapped[Any]]],
+ annotation: Optional[_AnnotationScanType],
+ extracted_mapped_annotation: Optional[_AnnotationScanType],
+ is_dataclass_field: bool,
+ ) -> None:
+ MappedColumn = util.preloaded.orm_properties.MappedColumn
+ if (
+ self.composite_class is None
+ and extracted_mapped_annotation is None
+ ):
+ self._raise_for_required(key, cls)
+ argument = extracted_mapped_annotation
+
+ if is_pep593(argument):
+ argument = typing_get_args(argument)[0]
+
+ if argument and self.composite_class is None:
+ if isinstance(argument, str) or is_fwd_ref(
+ argument, check_generic=True
+ ):
+ if originating_module is None:
+ str_arg = (
+ argument.__forward_arg__
+ if hasattr(argument, "__forward_arg__")
+ else str(argument)
+ )
+ raise sa_exc.ArgumentError(
+ f"Can't use forward ref {argument} for composite "
+ f"class argument; set up the type as Mapped[{str_arg}]"
+ )
+ argument = de_stringify_annotation(
+ cls, argument, originating_module, include_generic=True
+ )
+
+ self.composite_class = argument
+
+ if is_dataclass(self.composite_class):
+ self._setup_for_dataclass(registry, cls, originating_module, key)
+ else:
+ for attr in self.attrs:
+ if (
+ isinstance(attr, (MappedColumn, schema.Column))
+ and attr.name is None
+ ):
+ raise sa_exc.ArgumentError(
+ "Composite class column arguments must be named "
+ "unless a dataclass is used"
+ )
+ self._init_accessor()
+
+ def _init_accessor(self) -> None:
+ if is_dataclass(self.composite_class) and not hasattr(
+ self.composite_class, "__composite_values__"
+ ):
+ insp = inspect.signature(self.composite_class)
+ getter = operator.attrgetter(
+ *[p.name for p in insp.parameters.values()]
+ )
+ if len(insp.parameters) == 1:
+ self._generated_composite_accessor = lambda obj: (getter(obj),)
+ else:
+ self._generated_composite_accessor = getter
+
+ if (
+ self.composite_class is not None
+ and isinstance(self.composite_class, type)
+ and self.composite_class not in _composite_getters
+ ):
+ if self._generated_composite_accessor is not None:
+ _composite_getters[self.composite_class] = (
+ self._generated_composite_accessor
+ )
+ elif hasattr(self.composite_class, "__composite_values__"):
+ _composite_getters[self.composite_class] = (
+ lambda obj: obj.__composite_values__()
+ )
+
+ @util.preload_module("sqlalchemy.orm.properties")
+ @util.preload_module("sqlalchemy.orm.decl_base")
+ def _setup_for_dataclass(
+ self,
+ registry: _RegistryType,
+ cls: Type[Any],
+ originating_module: Optional[str],
+ key: str,
+ ) -> None:
+ MappedColumn = util.preloaded.orm_properties.MappedColumn
+
+ decl_base = util.preloaded.orm_decl_base
+
+ insp = inspect.signature(self.composite_class)
+ for param, attr in itertools.zip_longest(
+ insp.parameters.values(), self.attrs
+ ):
+ if param is None:
+ raise sa_exc.ArgumentError(
+ f"number of composite attributes "
+ f"{len(self.attrs)} exceeds "
+ f"that of the number of attributes in class "
+ f"{self.composite_class.__name__} {len(insp.parameters)}"
+ )
+ if attr is None:
+ # fill in missing attr spots with empty MappedColumn
+ attr = MappedColumn()
+ self.attrs += (attr,)
+
+ if isinstance(attr, MappedColumn):
+ attr.declarative_scan_for_composite(
+ registry,
+ cls,
+ originating_module,
+ key,
+ param.name,
+ param.annotation,
+ )
+ elif isinstance(attr, schema.Column):
+ decl_base._undefer_column_name(param.name, attr)
+
+ @util.memoized_property
+ def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]:
+ return [getattr(self.parent.class_, prop.key) for prop in self.props]
+
+ @util.memoized_property
+ @util.preload_module("orm.properties")
+ def props(self) -> Sequence[MapperProperty[Any]]:
+ props = []
+ MappedColumn = util.preloaded.orm_properties.MappedColumn
+
+ for attr in self.attrs:
+ if isinstance(attr, str):
+ prop = self.parent.get_property(attr, _configure_mappers=False)
+ elif isinstance(attr, schema.Column):
+ prop = self.parent._columntoproperty[attr]
+ elif isinstance(attr, MappedColumn):
+ prop = self.parent._columntoproperty[attr.column]
+ elif isinstance(attr, attributes.InstrumentedAttribute):
+ prop = attr.property
+ else:
+ prop = None
+
+ if not isinstance(prop, MapperProperty):
+ raise sa_exc.ArgumentError(
+ "Composite expects Column objects or mapped "
+ f"attributes/attribute names as arguments, got: {attr!r}"
+ )
+
+ props.append(prop)
+ return props
+
+ @util.non_memoized_property
+ @util.preload_module("orm.properties")
+ def columns(self) -> Sequence[Column[Any]]:
+ MappedColumn = util.preloaded.orm_properties.MappedColumn
+ return [
+ a.column if isinstance(a, MappedColumn) else a
+ for a in self.attrs
+ if isinstance(a, (schema.Column, MappedColumn))
+ ]
+
+ @property
+ def mapper_property_to_assign(self) -> Optional[MapperProperty[_CC]]:
+ return self
+
+ @property
+ def columns_to_assign(self) -> List[Tuple[schema.Column[Any], int]]:
+ return [(c, 0) for c in self.columns if c.table is None]
+
+ @util.preload_module("orm.properties")
+ def _setup_arguments_on_columns(self) -> None:
+ """Propagate configuration arguments made on this composite
+ to the target columns, for those that apply.
+
+ """
+ ColumnProperty = util.preloaded.orm_properties.ColumnProperty
+
+ for prop in self.props:
+ if not isinstance(prop, ColumnProperty):
+ continue
+ else:
+ cprop = prop
+
+ cprop.active_history = self.active_history
+ if self.deferred:
+ cprop.deferred = self.deferred
+ cprop.strategy_key = (("deferred", True), ("instrument", True))
+ cprop.group = self.group
+
+ def _setup_event_handlers(self) -> None:
+ """Establish events that populate/expire the composite attribute."""
+
+ def load_handler(
+ state: InstanceState[Any], context: ORMCompileState
+ ) -> None:
+ _load_refresh_handler(state, context, None, is_refresh=False)
+
+ def refresh_handler(
+ state: InstanceState[Any],
+ context: ORMCompileState,
+ to_load: Optional[Sequence[str]],
+ ) -> None:
+ # note this corresponds to sqlalchemy.ext.mutable load_attrs()
+
+ if not to_load or (
+ {self.key}.union(self._attribute_keys)
+ ).intersection(to_load):
+ _load_refresh_handler(state, context, to_load, is_refresh=True)
+
+ def _load_refresh_handler(
+ state: InstanceState[Any],
+ context: ORMCompileState,
+ to_load: Optional[Sequence[str]],
+ is_refresh: bool,
+ ) -> None:
+ dict_ = state.dict
+
+ # if context indicates we are coming from the
+ # fget() handler, this already set the value; skip the
+ # handler here. (other handlers like mutablecomposite will still
+ # want to catch it)
+ # there's an insufficiency here in that the fget() handler
+ # really should not be using the refresh event and there should
+ # be some other event that mutablecomposite can subscribe
+ # towards for this.
+
+ if (
+ not is_refresh or context is self._COMPOSITE_FGET
+ ) and self.key in dict_:
+ return
+
+ # if column elements aren't loaded, skip.
+ # __get__() will initiate a load for those
+ # columns
+ for k in self._attribute_keys:
+ if k not in dict_:
+ return
+
+ dict_[self.key] = self.composite_class(
+ *[state.dict[key] for key in self._attribute_keys]
+ )
+
+ def expire_handler(
+ state: InstanceState[Any], keys: Optional[Sequence[str]]
+ ) -> None:
+ if keys is None or set(self._attribute_keys).intersection(keys):
+ state.dict.pop(self.key, None)
+
+ def insert_update_handler(
+ mapper: Mapper[Any],
+ connection: Connection,
+ state: InstanceState[Any],
+ ) -> None:
+ """After an insert or update, some columns may be expired due
+ to server side defaults, or re-populated due to client side
+ defaults. Pop out the composite value here so that it
+ recreates.
+
+ """
+
+ state.dict.pop(self.key, None)
+
+ event.listen(
+ self.parent, "after_insert", insert_update_handler, raw=True
+ )
+ event.listen(
+ self.parent, "after_update", insert_update_handler, raw=True
+ )
+ event.listen(
+ self.parent, "load", load_handler, raw=True, propagate=True
+ )
+ event.listen(
+ self.parent, "refresh", refresh_handler, raw=True, propagate=True
+ )
+ event.listen(
+ self.parent, "expire", expire_handler, raw=True, propagate=True
+ )
+
+ proxy_attr = self.parent.class_manager[self.key]
+ proxy_attr.impl.dispatch = proxy_attr.dispatch # type: ignore
+ proxy_attr.impl.dispatch._active_history = self.active_history
+
+ # TODO: need a deserialize hook here
+
+ @util.memoized_property
+ def _attribute_keys(self) -> Sequence[str]:
+ return [prop.key for prop in self.props]
+
+ def _populate_composite_bulk_save_mappings_fn(
+ self,
+ ) -> Callable[[Dict[str, Any]], None]:
+ if self._generated_composite_accessor:
+ get_values = self._generated_composite_accessor
+ else:
+
+ def get_values(val: Any) -> Tuple[Any]:
+ return val.__composite_values__() # type: ignore
+
+ attrs = [prop.key for prop in self.props]
+
+ def populate(dest_dict: Dict[str, Any]) -> None:
+ dest_dict.update(
+ {
+ key: val
+ for key, val in zip(
+ attrs, get_values(dest_dict.pop(self.key))
+ )
+ }
+ )
+
+ return populate
+
+ def get_history(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ ) -> History:
+ """Provided for userland code that uses attributes.get_history()."""
+
+ added: List[Any] = []
+ deleted: List[Any] = []
+
+ has_history = False
+ for prop in self.props:
+ key = prop.key
+ hist = state.manager[key].impl.get_history(state, dict_)
+ if hist.has_changes():
+ has_history = True
+
+ non_deleted = hist.non_deleted()
+ if non_deleted:
+ added.extend(non_deleted)
+ else:
+ added.append(None)
+ if hist.deleted:
+ deleted.extend(hist.deleted)
+ else:
+ deleted.append(None)
+
+ if has_history:
+ return attributes.History(
+ [self.composite_class(*added)],
+ (),
+ [self.composite_class(*deleted)],
+ )
+ else:
+ return attributes.History((), [self.composite_class(*added)], ())
+
+ def _comparator_factory(
+ self, mapper: Mapper[Any]
+ ) -> Composite.Comparator[_CC]:
+ return self.comparator_factory(self, mapper)
+
+ class CompositeBundle(orm_util.Bundle[_T]):
+ def __init__(
+ self,
+ property_: Composite[_T],
+ expr: ClauseList,
+ ):
+ self.property = property_
+ super().__init__(property_.key, *expr)
+
+ def create_row_processor(
+ self,
+ query: Select[Any],
+ procs: Sequence[Callable[[Row[Any]], Any]],
+ labels: Sequence[str],
+ ) -> Callable[[Row[Any]], Any]:
+ def proc(row: Row[Any]) -> Any:
+ return self.property.composite_class(
+ *[proc(row) for proc in procs]
+ )
+
+ return proc
+
+ class Comparator(PropComparator[_PT]):
+ """Produce boolean, comparison, and other operators for
+ :class:`.Composite` attributes.
+
+ See the example in :ref:`composite_operations` for an overview
+ of usage , as well as the documentation for :class:`.PropComparator`.
+
+ .. seealso::
+
+ :class:`.PropComparator`
+
+ :class:`.ColumnOperators`
+
+ :ref:`types_operators`
+
+ :attr:`.TypeEngine.comparator_factory`
+
+ """
+
+ # https://github.com/python/mypy/issues/4266
+ __hash__ = None # type: ignore
+
+ prop: RODescriptorReference[Composite[_PT]]
+
+ @util.memoized_property
+ def clauses(self) -> ClauseList:
+ return expression.ClauseList(
+ group=False, *self._comparable_elements
+ )
+
+ def __clause_element__(self) -> CompositeProperty.CompositeBundle[_PT]:
+ return self.expression
+
+ @util.memoized_property
+ def expression(self) -> CompositeProperty.CompositeBundle[_PT]:
+ clauses = self.clauses._annotate(
+ {
+ "parententity": self._parententity,
+ "parentmapper": self._parententity,
+ "proxy_key": self.prop.key,
+ }
+ )
+ return CompositeProperty.CompositeBundle(self.prop, clauses)
+
+ def _bulk_update_tuples(
+ self, value: Any
+ ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
+ if isinstance(value, BindParameter):
+ value = value.value
+
+ values: Sequence[Any]
+
+ if value is None:
+ values = [None for key in self.prop._attribute_keys]
+ elif isinstance(self.prop.composite_class, type) and isinstance(
+ value, self.prop.composite_class
+ ):
+ values = self.prop._composite_values_from_instance(value)
+ else:
+ raise sa_exc.ArgumentError(
+ "Can't UPDATE composite attribute %s to %r"
+ % (self.prop, value)
+ )
+
+ return list(zip(self._comparable_elements, values))
+
+ @util.memoized_property
+ def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]:
+ if self._adapt_to_entity:
+ return [
+ getattr(self._adapt_to_entity.entity, prop.key)
+ for prop in self.prop._comparable_elements
+ ]
+ else:
+ return self.prop._comparable_elements
+
+ def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
+ return self._compare(operators.eq, other)
+
+ def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
+ return self._compare(operators.ne, other)
+
+ def __lt__(self, other: Any) -> ColumnElement[bool]:
+ return self._compare(operators.lt, other)
+
+ def __gt__(self, other: Any) -> ColumnElement[bool]:
+ return self._compare(operators.gt, other)
+
+ def __le__(self, other: Any) -> ColumnElement[bool]:
+ return self._compare(operators.le, other)
+
+ def __ge__(self, other: Any) -> ColumnElement[bool]:
+ return self._compare(operators.ge, other)
+
+ # what might be interesting would be if we create
+ # an instance of the composite class itself with
+ # the columns as data members, then use "hybrid style" comparison
+ # to create these comparisons. then your Point.__eq__() method could
+ # be where comparison behavior is defined for SQL also. Likely
+ # not a good choice for default behavior though, not clear how it would
+ # work w/ dataclasses, etc. also no demand for any of this anyway.
+ def _compare(
+ self, operator: OperatorType, other: Any
+ ) -> ColumnElement[bool]:
+ values: Sequence[Any]
+ if other is None:
+ values = [None] * len(self.prop._comparable_elements)
+ else:
+ values = self.prop._composite_values_from_instance(other)
+ comparisons = [
+ operator(a, b)
+ for a, b in zip(self.prop._comparable_elements, values)
+ ]
+ if self._adapt_to_entity:
+ assert self.adapter is not None
+ comparisons = [self.adapter(x) for x in comparisons]
+ return sql.and_(*comparisons)
+
+ def __str__(self) -> str:
+ return str(self.parent.class_.__name__) + "." + self.key
+
+
+class Composite(CompositeProperty[_T], _DeclarativeMapped[_T]):
+ """Declarative-compatible front-end for the :class:`.CompositeProperty`
+ class.
+
+ Public constructor is the :func:`_orm.composite` function.
+
+ .. versionchanged:: 2.0 Added :class:`_orm.Composite` as a Declarative
+ compatible subclass of :class:`_orm.CompositeProperty`.
+
+ .. seealso::
+
+ :ref:`mapper_composite`
+
+ """
+
+ inherit_cache = True
+ """:meta private:"""
+
+
+class ConcreteInheritedProperty(DescriptorProperty[_T]):
+ """A 'do nothing' :class:`.MapperProperty` that disables
+ an attribute on a concrete subclass that is only present
+ on the inherited mapper, not the concrete classes' mapper.
+
+ Cases where this occurs include:
+
+ * When the superclass mapper is mapped against a
+ "polymorphic union", which includes all attributes from
+ all subclasses.
+ * When a relationship() is configured on an inherited mapper,
+ but not on the subclass mapper. Concrete mappers require
+ that relationship() is configured explicitly on each
+ subclass.
+
+ """
+
+ def _comparator_factory(
+ self, mapper: Mapper[Any]
+ ) -> Type[PropComparator[_T]]:
+ comparator_callable = None
+
+ for m in self.parent.iterate_to_root():
+ p = m._props[self.key]
+ if getattr(p, "comparator_factory", None) is not None:
+ comparator_callable = p.comparator_factory
+ break
+ assert comparator_callable is not None
+ return comparator_callable(p, mapper) # type: ignore
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ def warn() -> NoReturn:
+ raise AttributeError(
+ "Concrete %s does not implement "
+ "attribute %r at the instance level. Add "
+ "this property explicitly to %s."
+ % (self.parent, self.key, self.parent)
+ )
+
+ class NoninheritedConcreteProp:
+ def __set__(s: Any, obj: Any, value: Any) -> NoReturn:
+ warn()
+
+ def __delete__(s: Any, obj: Any) -> NoReturn:
+ warn()
+
+ def __get__(s: Any, obj: Any, owner: Any) -> Any:
+ if obj is None:
+ return self.descriptor
+ warn()
+
+ self.descriptor = NoninheritedConcreteProp()
+
+
+class SynonymProperty(DescriptorProperty[_T]):
+ """Denote an attribute name as a synonym to a mapped property,
+ in that the attribute will mirror the value and expression behavior
+ of another attribute.
+
+ :class:`.Synonym` is constructed using the :func:`_orm.synonym`
+ function.
+
+ .. seealso::
+
+ :ref:`synonyms` - Overview of synonyms
+
+ """
+
+ comparator_factory: Optional[Type[PropComparator[_T]]]
+
+ def __init__(
+ self,
+ name: str,
+ map_column: Optional[bool] = None,
+ descriptor: Optional[Any] = None,
+ comparator_factory: Optional[Type[PropComparator[_T]]] = None,
+ attribute_options: Optional[_AttributeOptions] = None,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+ ):
+ super().__init__(attribute_options=attribute_options)
+
+ self.name = name
+ self.map_column = map_column
+ self.descriptor = descriptor
+ self.comparator_factory = comparator_factory
+ if doc:
+ self.doc = doc
+ elif descriptor and descriptor.__doc__:
+ self.doc = descriptor.__doc__
+ else:
+ self.doc = None
+ if info:
+ self.info.update(info)
+
+ util.set_creation_order(self)
+
+ if not TYPE_CHECKING:
+
+ @property
+ def uses_objects(self) -> bool:
+ return getattr(self.parent.class_, self.name).impl.uses_objects
+
+ # TODO: when initialized, check _proxied_object,
+ # emit a warning if its not a column-based property
+
+ @util.memoized_property
+ def _proxied_object(
+ self,
+ ) -> Union[MapperProperty[_T], SQLORMOperations[_T]]:
+ attr = getattr(self.parent.class_, self.name)
+ if not hasattr(attr, "property") or not isinstance(
+ attr.property, MapperProperty
+ ):
+ # attribute is a non-MapperProprerty proxy such as
+ # hybrid or association proxy
+ if isinstance(attr, attributes.QueryableAttribute):
+ return attr.comparator
+ elif isinstance(attr, SQLORMOperations):
+ # assocaition proxy comes here
+ return attr
+
+ raise sa_exc.InvalidRequestError(
+ """synonym() attribute "%s.%s" only supports """
+ """ORM mapped attributes, got %r"""
+ % (self.parent.class_.__name__, self.name, attr)
+ )
+ return attr.property
+
+ def _comparator_factory(self, mapper: Mapper[Any]) -> SQLORMOperations[_T]:
+ prop = self._proxied_object
+
+ if isinstance(prop, MapperProperty):
+ if self.comparator_factory:
+ comp = self.comparator_factory(prop, mapper)
+ else:
+ comp = prop.comparator_factory(prop, mapper)
+ return comp
+ else:
+ return prop
+
+ def get_history(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ ) -> History:
+ attr: QueryableAttribute[Any] = getattr(self.parent.class_, self.name)
+ return attr.impl.get_history(state, dict_, passive=passive)
+
+ @util.preload_module("sqlalchemy.orm.properties")
+ def set_parent(self, parent: Mapper[Any], init: bool) -> None:
+ properties = util.preloaded.orm_properties
+
+ if self.map_column:
+ # implement the 'map_column' option.
+ if self.key not in parent.persist_selectable.c:
+ raise sa_exc.ArgumentError(
+ "Can't compile synonym '%s': no column on table "
+ "'%s' named '%s'"
+ % (
+ self.name,
+ parent.persist_selectable.description,
+ self.key,
+ )
+ )
+ elif (
+ parent.persist_selectable.c[self.key]
+ in parent._columntoproperty
+ and parent._columntoproperty[
+ parent.persist_selectable.c[self.key]
+ ].key
+ == self.name
+ ):
+ raise sa_exc.ArgumentError(
+ "Can't call map_column=True for synonym %r=%r, "
+ "a ColumnProperty already exists keyed to the name "
+ "%r for column %r"
+ % (self.key, self.name, self.name, self.key)
+ )
+ p: ColumnProperty[Any] = properties.ColumnProperty(
+ parent.persist_selectable.c[self.key]
+ )
+ parent._configure_property(self.name, p, init=init, setparent=True)
+ p._mapped_by_synonym = self.key
+
+ self.parent = parent
+
+
+class Synonym(SynonymProperty[_T], _DeclarativeMapped[_T]):
+ """Declarative front-end for the :class:`.SynonymProperty` class.
+
+ Public constructor is the :func:`_orm.synonym` function.
+
+ .. versionchanged:: 2.0 Added :class:`_orm.Synonym` as a Declarative
+ compatible subclass for :class:`_orm.SynonymProperty`
+
+ .. seealso::
+
+ :ref:`synonyms` - Overview of synonyms
+
+ """
+
+ inherit_cache = True
+ """:meta private:"""
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/dynamic.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/dynamic.py
new file mode 100644
index 0000000..7496e5c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/dynamic.py
@@ -0,0 +1,298 @@
+# orm/dynamic.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+
+"""Dynamic collection API.
+
+Dynamic collections act like Query() objects for read operations and support
+basic add/delete mutation.
+
+.. legacy:: the "dynamic" loader is a legacy feature, superseded by the
+ "write_only" loader.
+
+
+"""
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import attributes
+from . import exc as orm_exc
+from . import relationships
+from . import util as orm_util
+from .base import PassiveFlag
+from .query import Query
+from .session import object_session
+from .writeonly import AbstractCollectionWriter
+from .writeonly import WriteOnlyAttributeImpl
+from .writeonly import WriteOnlyHistory
+from .writeonly import WriteOnlyLoader
+from .. import util
+from ..engine import result
+
+
+if TYPE_CHECKING:
+ from . import QueryableAttribute
+ from .mapper import Mapper
+ from .relationships import _RelationshipOrderByArg
+ from .session import Session
+ from .state import InstanceState
+ from .util import AliasedClass
+ from ..event import _Dispatch
+ from ..sql.elements import ColumnElement
+
+_T = TypeVar("_T", bound=Any)
+
+
+class DynamicCollectionHistory(WriteOnlyHistory[_T]):
+ def __init__(
+ self,
+ attr: DynamicAttributeImpl,
+ state: InstanceState[_T],
+ passive: PassiveFlag,
+ apply_to: Optional[DynamicCollectionHistory[_T]] = None,
+ ) -> None:
+ if apply_to:
+ coll = AppenderQuery(attr, state).autoflush(False)
+ self.unchanged_items = util.OrderedIdentitySet(coll)
+ self.added_items = apply_to.added_items
+ self.deleted_items = apply_to.deleted_items
+ self._reconcile_collection = True
+ else:
+ self.deleted_items = util.OrderedIdentitySet()
+ self.added_items = util.OrderedIdentitySet()
+ self.unchanged_items = util.OrderedIdentitySet()
+ self._reconcile_collection = False
+
+
+class DynamicAttributeImpl(WriteOnlyAttributeImpl):
+ _supports_dynamic_iteration = True
+ collection_history_cls = DynamicCollectionHistory[Any]
+ query_class: Type[AppenderMixin[Any]] # type: ignore[assignment]
+
+ def __init__(
+ self,
+ class_: Union[Type[Any], AliasedClass[Any]],
+ key: str,
+ dispatch: _Dispatch[QueryableAttribute[Any]],
+ target_mapper: Mapper[_T],
+ order_by: _RelationshipOrderByArg,
+ query_class: Optional[Type[AppenderMixin[_T]]] = None,
+ **kw: Any,
+ ) -> None:
+ attributes.AttributeImpl.__init__(
+ self, class_, key, None, dispatch, **kw
+ )
+ self.target_mapper = target_mapper
+ if order_by:
+ self.order_by = tuple(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)
+
+
+@relationships.RelationshipProperty.strategy_for(lazy="dynamic")
+class DynaLoader(WriteOnlyLoader):
+ impl_class = DynamicAttributeImpl
+
+
+class AppenderMixin(AbstractCollectionWriter[_T]):
+ """A mixin that expects to be mixing in a Query class with
+ AbstractAppender.
+
+
+ """
+
+ query_class: Optional[Type[Query[_T]]] = None
+ _order_by_clauses: Tuple[ColumnElement[Any], ...]
+
+ def __init__(
+ self, attr: DynamicAttributeImpl, state: InstanceState[_T]
+ ) -> None:
+ Query.__init__(
+ self, # type: ignore[arg-type]
+ attr.target_mapper,
+ None,
+ )
+ super().__init__(attr, state)
+
+ @property
+ def session(self) -> Optional[Session]:
+ sess = object_session(self.instance)
+ if sess is not None and sess.autoflush and self.instance in sess:
+ sess.flush()
+ if not orm_util.has_identity(self.instance):
+ return None
+ else:
+ return sess
+
+ @session.setter
+ def session(self, session: Session) -> None:
+ self.sess = session
+
+ def _iter(self) -> Union[result.ScalarResult[_T], result.Result[_T]]:
+ sess = self.session
+ if sess is None:
+ state = attributes.instance_state(self.instance)
+ if state.detached:
+ util.warn(
+ "Instance %s is detached, dynamic relationship cannot "
+ "return a correct result. This warning will become "
+ "a DetachedInstanceError in a future release."
+ % (orm_util.state_str(state))
+ )
+
+ return result.IteratorResult(
+ result.SimpleResultMetaData([self.attr.class_.__name__]),
+ self.attr._get_collection_history( # type: ignore[arg-type]
+ attributes.instance_state(self.instance),
+ PassiveFlag.PASSIVE_NO_INITIALIZE,
+ ).added_items,
+ _source_supports_scalars=True,
+ ).scalars()
+ else:
+ return self._generate(sess)._iter()
+
+ if TYPE_CHECKING:
+
+ def __iter__(self) -> Iterator[_T]: ...
+
+ def __getitem__(self, index: Any) -> Union[_T, List[_T]]:
+ sess = self.session
+ if sess is None:
+ return self.attr._get_collection_history(
+ attributes.instance_state(self.instance),
+ PassiveFlag.PASSIVE_NO_INITIALIZE,
+ ).indexed(index)
+ else:
+ return self._generate(sess).__getitem__(index) # type: ignore[no-any-return] # noqa: E501
+
+ def count(self) -> int:
+ sess = self.session
+ if sess is None:
+ return len(
+ self.attr._get_collection_history(
+ attributes.instance_state(self.instance),
+ PassiveFlag.PASSIVE_NO_INITIALIZE,
+ ).added_items
+ )
+ else:
+ return self._generate(sess).count()
+
+ def _generate(
+ self,
+ sess: Optional[Session] = None,
+ ) -> Query[_T]:
+ # 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
+
+ return query
+
+ def add_all(self, iterator: Iterable[_T]) -> None:
+ """Add an iterable of items to this :class:`_orm.AppenderQuery`.
+
+ The given items will be persisted to the database in terms of
+ the parent instance's collection on the next flush.
+
+ This method is provided to assist in delivering forwards-compatibility
+ with the :class:`_orm.WriteOnlyCollection` collection class.
+
+ .. versionadded:: 2.0
+
+ """
+ self._add_all_impl(iterator)
+
+ def add(self, item: _T) -> None:
+ """Add an item to this :class:`_orm.AppenderQuery`.
+
+ The given item will be persisted to the database in terms of
+ the parent instance's collection on the next flush.
+
+ This method is provided to assist in delivering forwards-compatibility
+ with the :class:`_orm.WriteOnlyCollection` collection class.
+
+ .. versionadded:: 2.0
+
+ """
+ self._add_all_impl([item])
+
+ def extend(self, iterator: Iterable[_T]) -> None:
+ """Add an iterable of items to this :class:`_orm.AppenderQuery`.
+
+ The given items will be persisted to the database in terms of
+ the parent instance's collection on the next flush.
+
+ """
+ self._add_all_impl(iterator)
+
+ def append(self, item: _T) -> None:
+ """Append an item to this :class:`_orm.AppenderQuery`.
+
+ The given item will be persisted to the database in terms of
+ the parent instance's collection on the next flush.
+
+ """
+ self._add_all_impl([item])
+
+ def remove(self, item: _T) -> None:
+ """Remove an item from this :class:`_orm.AppenderQuery`.
+
+ The given item will be removed from the parent instance's collection on
+ the next flush.
+
+ """
+ self._remove_impl(item)
+
+
+class AppenderQuery(AppenderMixin[_T], Query[_T]): # type: ignore[misc]
+ """A dynamic query that supports basic collection storage operations.
+
+ Methods on :class:`.AppenderQuery` include all methods of
+ :class:`_orm.Query`, plus additional methods used for collection
+ persistence.
+
+
+ """
+
+
+def mixin_user_query(cls: Any) -> type[AppenderMixin[Any]]:
+ """Return a new class with AppenderQuery functionality layered over."""
+ name = "Appender" + cls.__name__
+ return type(name, (AppenderMixin, cls), {"query_class": cls})
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/evaluator.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/evaluator.py
new file mode 100644
index 0000000..f264454
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/evaluator.py
@@ -0,0 +1,368 @@
+# orm/evaluator.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: ignore-errors
+
+"""Evaluation functions used **INTERNALLY** by ORM DML use cases.
+
+
+This module is **private, for internal use by SQLAlchemy**.
+
+.. versionchanged:: 2.0.4 renamed ``EvaluatorCompiler`` to
+ ``_EvaluatorCompiler``.
+
+"""
+
+
+from __future__ import annotations
+
+from typing import Type
+
+from . import exc as orm_exc
+from .base import LoaderCallableStatus
+from .base import PassiveFlag
+from .. import exc
+from .. import inspect
+from ..sql import and_
+from ..sql import operators
+from ..sql.sqltypes import Integer
+from ..sql.sqltypes import Numeric
+from ..util import warn_deprecated
+
+
+class UnevaluatableError(exc.InvalidRequestError):
+ pass
+
+
+class _NoObject(operators.ColumnOperators):
+ def operate(self, *arg, **kw):
+ return None
+
+ def reverse_operate(self, *arg, **kw):
+ return None
+
+
+class _ExpiredObject(operators.ColumnOperators):
+ def operate(self, *arg, **kw):
+ return self
+
+ def reverse_operate(self, *arg, **kw):
+ return self
+
+
+_NO_OBJECT = _NoObject()
+_EXPIRED_OBJECT = _ExpiredObject()
+
+
+class _EvaluatorCompiler:
+ def __init__(self, target_cls=None):
+ self.target_cls = target_cls
+
+ def process(self, clause, *clauses):
+ if clauses:
+ clause = and_(clause, *clauses)
+
+ meth = getattr(self, f"visit_{clause.__visit_name__}", None)
+ if not meth:
+ raise UnevaluatableError(
+ f"Cannot evaluate {type(clause).__name__}"
+ )
+ return meth(clause)
+
+ def visit_grouping(self, clause):
+ return self.process(clause.element)
+
+ def visit_null(self, clause):
+ return lambda obj: None
+
+ def visit_false(self, clause):
+ return lambda obj: False
+
+ def visit_true(self, clause):
+ return lambda obj: True
+
+ def visit_column(self, clause):
+ try:
+ parentmapper = clause._annotations["parentmapper"]
+ except KeyError as ke:
+ raise UnevaluatableError(
+ f"Cannot evaluate column: {clause}"
+ ) from ke
+
+ if self.target_cls and not issubclass(
+ self.target_cls, parentmapper.class_
+ ):
+ raise UnevaluatableError(
+ "Can't evaluate criteria against "
+ f"alternate class {parentmapper.class_}"
+ )
+
+ parentmapper._check_configure()
+
+ # we'd like to use "proxy_key" annotation to get the "key", however
+ # in relationship primaryjoin cases proxy_key is sometimes deannotated
+ # and sometimes apparently not present in the first place (?).
+ # While I can stop it from being deannotated (though need to see if
+ # this breaks other things), not sure right now about cases where it's
+ # not there in the first place. can fix at some later point.
+ # key = clause._annotations["proxy_key"]
+
+ # for now, use the old way
+ try:
+ key = parentmapper._columntoproperty[clause].key
+ except orm_exc.UnmappedColumnError as err:
+ raise UnevaluatableError(
+ f"Cannot evaluate expression: {err}"
+ ) from err
+
+ # note this used to fall back to a simple `getattr(obj, key)` evaluator
+ # if impl was None; as of #8656, we ensure mappers are configured
+ # so that impl is available
+ impl = parentmapper.class_manager[key].impl
+
+ def get_corresponding_attr(obj):
+ if obj is None:
+ return _NO_OBJECT
+ state = inspect(obj)
+ dict_ = state.dict
+
+ value = impl.get(
+ state, dict_, passive=PassiveFlag.PASSIVE_NO_FETCH
+ )
+ if value is LoaderCallableStatus.PASSIVE_NO_RESULT:
+ return _EXPIRED_OBJECT
+ return value
+
+ return get_corresponding_attr
+
+ def visit_tuple(self, clause):
+ return self.visit_clauselist(clause)
+
+ def visit_expression_clauselist(self, clause):
+ return self.visit_clauselist(clause)
+
+ def visit_clauselist(self, clause):
+ evaluators = [self.process(clause) for clause in clause.clauses]
+
+ dispatch = (
+ f"visit_{clause.operator.__name__.rstrip('_')}_clauselist_op"
+ )
+ meth = getattr(self, dispatch, None)
+ if meth:
+ return meth(clause.operator, evaluators, clause)
+ else:
+ raise UnevaluatableError(
+ f"Cannot evaluate clauselist with operator {clause.operator}"
+ )
+
+ def visit_binary(self, clause):
+ eval_left = self.process(clause.left)
+ eval_right = self.process(clause.right)
+
+ dispatch = f"visit_{clause.operator.__name__.rstrip('_')}_binary_op"
+ meth = getattr(self, dispatch, None)
+ if meth:
+ return meth(clause.operator, eval_left, eval_right, clause)
+ else:
+ raise UnevaluatableError(
+ f"Cannot evaluate {type(clause).__name__} with "
+ f"operator {clause.operator}"
+ )
+
+ def visit_or_clauselist_op(self, operator, evaluators, clause):
+ def evaluate(obj):
+ has_null = False
+ for sub_evaluate in evaluators:
+ value = sub_evaluate(obj)
+ if value is _EXPIRED_OBJECT:
+ return _EXPIRED_OBJECT
+ elif value:
+ return True
+ has_null = has_null or value is None
+ if has_null:
+ return None
+ return False
+
+ return evaluate
+
+ def visit_and_clauselist_op(self, operator, evaluators, clause):
+ def evaluate(obj):
+ for sub_evaluate in evaluators:
+ value = sub_evaluate(obj)
+ if value is _EXPIRED_OBJECT:
+ return _EXPIRED_OBJECT
+
+ if not value:
+ if value is None or value is _NO_OBJECT:
+ return None
+ return False
+ return True
+
+ return evaluate
+
+ def visit_comma_op_clauselist_op(self, operator, evaluators, clause):
+ def evaluate(obj):
+ values = []
+ for sub_evaluate in evaluators:
+ value = sub_evaluate(obj)
+ if value is _EXPIRED_OBJECT:
+ return _EXPIRED_OBJECT
+ elif value is None or value is _NO_OBJECT:
+ return None
+ values.append(value)
+ return tuple(values)
+
+ return evaluate
+
+ def visit_custom_op_binary_op(
+ self, operator, eval_left, eval_right, clause
+ ):
+ if operator.python_impl:
+ return self._straight_evaluate(
+ operator, eval_left, eval_right, clause
+ )
+ else:
+ raise UnevaluatableError(
+ f"Custom operator {operator.opstring!r} can't be evaluated "
+ "in Python unless it specifies a callable using "
+ "`.python_impl`."
+ )
+
+ def visit_is_binary_op(self, operator, eval_left, eval_right, clause):
+ def evaluate(obj):
+ left_val = eval_left(obj)
+ right_val = eval_right(obj)
+ if left_val is _EXPIRED_OBJECT or right_val is _EXPIRED_OBJECT:
+ return _EXPIRED_OBJECT
+ return left_val == right_val
+
+ return evaluate
+
+ def visit_is_not_binary_op(self, operator, eval_left, eval_right, clause):
+ def evaluate(obj):
+ left_val = eval_left(obj)
+ right_val = eval_right(obj)
+ if left_val is _EXPIRED_OBJECT or right_val is _EXPIRED_OBJECT:
+ return _EXPIRED_OBJECT
+ return left_val != right_val
+
+ return evaluate
+
+ def _straight_evaluate(self, operator, eval_left, eval_right, clause):
+ def evaluate(obj):
+ left_val = eval_left(obj)
+ right_val = eval_right(obj)
+ if left_val is _EXPIRED_OBJECT or right_val is _EXPIRED_OBJECT:
+ return _EXPIRED_OBJECT
+ elif left_val is None or right_val is None:
+ return None
+
+ return operator(eval_left(obj), eval_right(obj))
+
+ return evaluate
+
+ def _straight_evaluate_numeric_only(
+ self, operator, eval_left, eval_right, clause
+ ):
+ if clause.left.type._type_affinity not in (
+ Numeric,
+ Integer,
+ ) or clause.right.type._type_affinity not in (Numeric, Integer):
+ raise UnevaluatableError(
+ f'Cannot evaluate math operator "{operator.__name__}" for '
+ f"datatypes {clause.left.type}, {clause.right.type}"
+ )
+
+ return self._straight_evaluate(operator, eval_left, eval_right, clause)
+
+ visit_add_binary_op = _straight_evaluate_numeric_only
+ visit_mul_binary_op = _straight_evaluate_numeric_only
+ visit_sub_binary_op = _straight_evaluate_numeric_only
+ visit_mod_binary_op = _straight_evaluate_numeric_only
+ visit_truediv_binary_op = _straight_evaluate_numeric_only
+ visit_lt_binary_op = _straight_evaluate
+ visit_le_binary_op = _straight_evaluate
+ visit_ne_binary_op = _straight_evaluate
+ visit_gt_binary_op = _straight_evaluate
+ visit_ge_binary_op = _straight_evaluate
+ visit_eq_binary_op = _straight_evaluate
+
+ def visit_in_op_binary_op(self, operator, eval_left, eval_right, clause):
+ return self._straight_evaluate(
+ lambda a, b: a in b if a is not _NO_OBJECT else None,
+ eval_left,
+ eval_right,
+ clause,
+ )
+
+ def visit_not_in_op_binary_op(
+ self, operator, eval_left, eval_right, clause
+ ):
+ return self._straight_evaluate(
+ lambda a, b: a not in b if a is not _NO_OBJECT else None,
+ eval_left,
+ eval_right,
+ clause,
+ )
+
+ def visit_concat_op_binary_op(
+ self, operator, eval_left, eval_right, clause
+ ):
+ return self._straight_evaluate(
+ lambda a, b: a + b, eval_left, eval_right, clause
+ )
+
+ def visit_startswith_op_binary_op(
+ self, operator, eval_left, eval_right, clause
+ ):
+ return self._straight_evaluate(
+ lambda a, b: a.startswith(b), eval_left, eval_right, clause
+ )
+
+ def visit_endswith_op_binary_op(
+ self, operator, eval_left, eval_right, clause
+ ):
+ return self._straight_evaluate(
+ lambda a, b: a.endswith(b), eval_left, eval_right, clause
+ )
+
+ def visit_unary(self, clause):
+ eval_inner = self.process(clause.element)
+ if clause.operator is operators.inv:
+
+ def evaluate(obj):
+ value = eval_inner(obj)
+ if value is _EXPIRED_OBJECT:
+ return _EXPIRED_OBJECT
+ elif value is None:
+ return None
+ return not value
+
+ return evaluate
+ raise UnevaluatableError(
+ f"Cannot evaluate {type(clause).__name__} "
+ f"with operator {clause.operator}"
+ )
+
+ def visit_bindparam(self, clause):
+ if clause.callable:
+ val = clause.callable()
+ else:
+ val = clause.value
+ return lambda obj: val
+
+
+def __getattr__(name: str) -> Type[_EvaluatorCompiler]:
+ if name == "EvaluatorCompiler":
+ warn_deprecated(
+ "Direct use of 'EvaluatorCompiler' is not supported, and this "
+ "name will be removed in a future release. "
+ "'_EvaluatorCompiler' is for internal use only",
+ "2.0",
+ )
+ return _EvaluatorCompiler
+ else:
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/events.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/events.py
new file mode 100644
index 0000000..1cd51bf
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/events.py
@@ -0,0 +1,3259 @@
+# orm/events.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""ORM event interfaces.
+
+"""
+from __future__ import annotations
+
+from typing import Any
+from typing import Callable
+from typing import Collection
+from typing import Dict
+from typing import Generic
+from typing import Iterable
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import instrumentation
+from . import interfaces
+from . import mapperlib
+from .attributes import QueryableAttribute
+from .base import _mapper_or_none
+from .base import NO_KEY
+from .instrumentation import ClassManager
+from .instrumentation import InstrumentationFactory
+from .query import BulkDelete
+from .query import BulkUpdate
+from .query import Query
+from .scoping import scoped_session
+from .session import Session
+from .session import sessionmaker
+from .. import event
+from .. import exc
+from .. import util
+from ..event import EventTarget
+from ..event.registry import _ET
+from ..util.compat import inspect_getfullargspec
+
+if TYPE_CHECKING:
+ from weakref import ReferenceType
+
+ from ._typing import _InstanceDict
+ from ._typing import _InternalEntityType
+ from ._typing import _O
+ from ._typing import _T
+ from .attributes import Event
+ from .base import EventConstants
+ from .session import ORMExecuteState
+ from .session import SessionTransaction
+ from .unitofwork import UOWTransaction
+ from ..engine import Connection
+ from ..event.base import _Dispatch
+ from ..event.base import _HasEventsDispatch
+ from ..event.registry import _EventKey
+ from ..orm.collections import CollectionAdapter
+ from ..orm.context import QueryContext
+ from ..orm.decl_api import DeclarativeAttributeIntercept
+ from ..orm.decl_api import DeclarativeMeta
+ from ..orm.mapper import Mapper
+ from ..orm.state import InstanceState
+
+_KT = TypeVar("_KT", bound=Any)
+_ET2 = TypeVar("_ET2", bound=EventTarget)
+
+
+class InstrumentationEvents(event.Events[InstrumentationFactory]):
+ """Events related to class instrumentation events.
+
+ The listeners here support being established against
+ any new style class, that is any object that is a subclass
+ of 'type'. Events will then be fired off for events
+ against that class. If the "propagate=True" flag is passed
+ to event.listen(), the event will fire off for subclasses
+ of that class as well.
+
+ The Python ``type`` builtin is also accepted as a target,
+ which when used has the effect of events being emitted
+ for all classes.
+
+ Note the "propagate" flag here is defaulted to ``True``,
+ unlike the other class level events where it defaults
+ to ``False``. This means that new subclasses will also
+ be the subject of these events, when a listener
+ is established on a superclass.
+
+ """
+
+ _target_class_doc = "SomeBaseClass"
+ _dispatch_target = InstrumentationFactory
+
+ @classmethod
+ def _accept_with(
+ cls,
+ target: Union[
+ InstrumentationFactory,
+ Type[InstrumentationFactory],
+ ],
+ identifier: str,
+ ) -> Optional[
+ Union[
+ InstrumentationFactory,
+ Type[InstrumentationFactory],
+ ]
+ ]:
+ if isinstance(target, type):
+ return _InstrumentationEventsHold(target) # type: ignore [return-value] # noqa: E501
+ else:
+ return None
+
+ @classmethod
+ def _listen(
+ cls, event_key: _EventKey[_T], propagate: bool = True, **kw: Any
+ ) -> None:
+ target, identifier, fn = (
+ event_key.dispatch_target,
+ event_key.identifier,
+ event_key._listen_fn,
+ )
+
+ def listen(target_cls: type, *arg: Any) -> Optional[Any]:
+ listen_cls = target()
+
+ # if weakref were collected, however this is not something
+ # that normally happens. it was occurring during test teardown
+ # between mapper/registry/instrumentation_manager, however this
+ # interaction was changed to not rely upon the event system.
+ if listen_cls is None:
+ return None
+
+ if propagate and issubclass(target_cls, listen_cls):
+ return fn(target_cls, *arg)
+ elif not propagate and target_cls is listen_cls:
+ return fn(target_cls, *arg)
+ else:
+ return None
+
+ def remove(ref: ReferenceType[_T]) -> None:
+ key = event.registry._EventKey( # type: ignore [type-var]
+ None,
+ identifier,
+ listen,
+ instrumentation._instrumentation_factory,
+ )
+ getattr(
+ instrumentation._instrumentation_factory.dispatch, identifier
+ ).remove(key)
+
+ target = weakref.ref(target.class_, remove)
+
+ event_key.with_dispatch_target(
+ instrumentation._instrumentation_factory
+ ).with_wrapper(listen).base_listen(**kw)
+
+ @classmethod
+ def _clear(cls) -> None:
+ super()._clear()
+ instrumentation._instrumentation_factory.dispatch._clear()
+
+ def class_instrument(self, cls: ClassManager[_O]) -> None:
+ """Called after the given class is instrumented.
+
+ To get at the :class:`.ClassManager`, use
+ :func:`.manager_of_class`.
+
+ """
+
+ def class_uninstrument(self, cls: ClassManager[_O]) -> None:
+ """Called before the given class is uninstrumented.
+
+ To get at the :class:`.ClassManager`, use
+ :func:`.manager_of_class`.
+
+ """
+
+ def attribute_instrument(
+ self, cls: ClassManager[_O], key: _KT, inst: _O
+ ) -> None:
+ """Called when an attribute is instrumented."""
+
+
+class _InstrumentationEventsHold:
+ """temporary marker object used to transfer from _accept_with() to
+ _listen() on the InstrumentationEvents class.
+
+ """
+
+ def __init__(self, class_: type) -> None:
+ self.class_ = class_
+
+ dispatch = event.dispatcher(InstrumentationEvents)
+
+
+class InstanceEvents(event.Events[ClassManager[Any]]):
+ """Define events specific to object lifecycle.
+
+ e.g.::
+
+ from sqlalchemy import event
+
+ def my_load_listener(target, context):
+ print("on load!")
+
+ event.listen(SomeClass, 'load', my_load_listener)
+
+ Available targets include:
+
+ * mapped classes
+ * unmapped superclasses of mapped or to-be-mapped classes
+ (using the ``propagate=True`` flag)
+ * :class:`_orm.Mapper` objects
+ * the :class:`_orm.Mapper` class itself indicates listening for all
+ mappers.
+
+ Instance events are closely related to mapper events, but
+ are more specific to the instance and its instrumentation,
+ rather than its system of persistence.
+
+ When using :class:`.InstanceEvents`, several modifiers are
+ available to the :func:`.event.listen` function.
+
+ :param propagate=False: When True, the event listener should
+ be applied to all inheriting classes as well as the
+ class which is the target of this listener.
+ :param raw=False: When True, the "target" argument passed
+ to applicable event listener functions will be the
+ instance's :class:`.InstanceState` management
+ object, rather than the mapped instance itself.
+ :param restore_load_context=False: Applies to the
+ :meth:`.InstanceEvents.load` and :meth:`.InstanceEvents.refresh`
+ events. Restores the loader context of the object when the event
+ hook is complete, so that ongoing eager load operations continue
+ to target the object appropriately. A warning is emitted if the
+ object is moved to a new loader context from within one of these
+ events if this flag is not set.
+
+ .. versionadded:: 1.3.14
+
+
+ """
+
+ _target_class_doc = "SomeClass"
+
+ _dispatch_target = ClassManager
+
+ @classmethod
+ def _new_classmanager_instance(
+ cls,
+ class_: Union[DeclarativeAttributeIntercept, DeclarativeMeta, type],
+ classmanager: ClassManager[_O],
+ ) -> None:
+ _InstanceEventsHold.populate(class_, classmanager)
+
+ @classmethod
+ @util.preload_module("sqlalchemy.orm")
+ def _accept_with(
+ cls,
+ target: Union[
+ ClassManager[Any],
+ Type[ClassManager[Any]],
+ ],
+ identifier: str,
+ ) -> Optional[Union[ClassManager[Any], Type[ClassManager[Any]]]]:
+ orm = util.preloaded.orm
+
+ if isinstance(target, ClassManager):
+ return target
+ elif isinstance(target, mapperlib.Mapper):
+ return target.class_manager
+ elif target is orm.mapper: # type: ignore [attr-defined]
+ util.warn_deprecated(
+ "The `sqlalchemy.orm.mapper()` symbol is deprecated and "
+ "will be removed in a future release. For the mapper-wide "
+ "event target, use the 'sqlalchemy.orm.Mapper' class.",
+ "2.0",
+ )
+ return ClassManager
+ elif isinstance(target, type):
+ if issubclass(target, mapperlib.Mapper):
+ return ClassManager
+ else:
+ manager = instrumentation.opt_manager_of_class(target)
+ if manager:
+ return manager
+ else:
+ return _InstanceEventsHold(target) # type: ignore [return-value] # noqa: E501
+ return None
+
+ @classmethod
+ def _listen(
+ cls,
+ event_key: _EventKey[ClassManager[Any]],
+ raw: bool = False,
+ propagate: bool = False,
+ restore_load_context: bool = False,
+ **kw: Any,
+ ) -> None:
+ target, fn = (event_key.dispatch_target, event_key._listen_fn)
+
+ if not raw or restore_load_context:
+
+ def wrap(
+ state: InstanceState[_O], *arg: Any, **kw: Any
+ ) -> Optional[Any]:
+ if not raw:
+ target: Any = state.obj()
+ else:
+ target = state
+ if restore_load_context:
+ runid = state.runid
+ try:
+ return fn(target, *arg, **kw)
+ finally:
+ if restore_load_context:
+ state.runid = runid
+
+ event_key = event_key.with_wrapper(wrap)
+
+ event_key.base_listen(propagate=propagate, **kw)
+
+ if propagate:
+ for mgr in target.subclass_managers(True):
+ event_key.with_dispatch_target(mgr).base_listen(propagate=True)
+
+ @classmethod
+ def _clear(cls) -> None:
+ super()._clear()
+ _InstanceEventsHold._clear()
+
+ def first_init(self, manager: ClassManager[_O], cls: Type[_O]) -> None:
+ """Called when the first instance of a particular mapping is called.
+
+ This event is called when the ``__init__`` method of a class
+ is called the first time for that particular class. The event
+ invokes before ``__init__`` actually proceeds as well as before
+ the :meth:`.InstanceEvents.init` event is invoked.
+
+ """
+
+ def init(self, target: _O, args: Any, kwargs: Any) -> None:
+ """Receive an instance when its constructor is called.
+
+ This method is only called during a userland construction of
+ an object, in conjunction with the object's constructor, e.g.
+ its ``__init__`` method. It is not called when an object is
+ loaded from the database; see the :meth:`.InstanceEvents.load`
+ event in order to intercept a database load.
+
+ The event is called before the actual ``__init__`` constructor
+ of the object is called. The ``kwargs`` dictionary may be
+ modified in-place in order to affect what is passed to
+ ``__init__``.
+
+ :param target: the mapped instance. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :param args: positional arguments passed to the ``__init__`` method.
+ This is passed as a tuple and is currently immutable.
+ :param kwargs: keyword arguments passed to the ``__init__`` method.
+ This structure *can* be altered in place.
+
+ .. seealso::
+
+ :meth:`.InstanceEvents.init_failure`
+
+ :meth:`.InstanceEvents.load`
+
+ """
+
+ def init_failure(self, target: _O, args: Any, kwargs: Any) -> None:
+ """Receive an instance when its constructor has been called,
+ and raised an exception.
+
+ This method is only called during a userland construction of
+ an object, in conjunction with the object's constructor, e.g.
+ its ``__init__`` method. It is not called when an object is loaded
+ from the database.
+
+ The event is invoked after an exception raised by the ``__init__``
+ method is caught. After the event
+ is invoked, the original exception is re-raised outwards, so that
+ the construction of the object still raises an exception. The
+ actual exception and stack trace raised should be present in
+ ``sys.exc_info()``.
+
+ :param target: the mapped instance. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :param args: positional arguments that were passed to the ``__init__``
+ method.
+ :param kwargs: keyword arguments that were passed to the ``__init__``
+ method.
+
+ .. seealso::
+
+ :meth:`.InstanceEvents.init`
+
+ :meth:`.InstanceEvents.load`
+
+ """
+
+ def _sa_event_merge_wo_load(
+ self, target: _O, context: QueryContext
+ ) -> None:
+ """receive an object instance after it was the subject of a merge()
+ call, when load=False was passed.
+
+ The target would be the already-loaded object in the Session which
+ would have had its attributes overwritten by the incoming object. This
+ overwrite operation does not use attribute events, instead just
+ populating dict directly. Therefore the purpose of this event is so
+ that extensions like sqlalchemy.ext.mutable know that object state has
+ changed and incoming state needs to be set up for "parents" etc.
+
+ This functionality is acceptable to be made public in a later release.
+
+ .. versionadded:: 1.4.41
+
+ """
+
+ def load(self, target: _O, context: QueryContext) -> None:
+ """Receive an object instance after it has been created via
+ ``__new__``, and after initial attribute population has
+ occurred.
+
+ This typically occurs when the instance is created based on
+ incoming result rows, and is only called once for that
+ instance's lifetime.
+
+ .. warning::
+
+ During a result-row load, this event is invoked when the
+ first row received for this instance is processed. When using
+ eager loading with collection-oriented attributes, the additional
+ rows that are to be loaded / processed in order to load subsequent
+ collection items have not occurred yet. This has the effect
+ both that collections will not be fully loaded, as well as that
+ if an operation occurs within this event handler that emits
+ another database load operation for the object, the "loading
+ context" for the object can change and interfere with the
+ existing eager loaders still in progress.
+
+ Examples of what can cause the "loading context" to change within
+ the event handler include, but are not necessarily limited to:
+
+ * accessing deferred attributes that weren't part of the row,
+ will trigger an "undefer" operation and refresh the object
+
+ * accessing attributes on a joined-inheritance subclass that
+ weren't part of the row, will trigger a refresh operation.
+
+ As of SQLAlchemy 1.3.14, a warning is emitted when this occurs. The
+ :paramref:`.InstanceEvents.restore_load_context` option may be
+ used on the event to prevent this warning; this will ensure that
+ the existing loading context is maintained for the object after the
+ event is called::
+
+ @event.listens_for(
+ SomeClass, "load", restore_load_context=True)
+ def on_load(instance, context):
+ instance.some_unloaded_attribute
+
+ .. versionchanged:: 1.3.14 Added
+ :paramref:`.InstanceEvents.restore_load_context`
+ and :paramref:`.SessionEvents.restore_load_context` flags which
+ apply to "on load" events, which will ensure that the loading
+ context for an object is restored when the event hook is
+ complete; a warning is emitted if the load context of the object
+ changes without this flag being set.
+
+
+ The :meth:`.InstanceEvents.load` event is also available in a
+ class-method decorator format called :func:`_orm.reconstructor`.
+
+ :param target: the mapped instance. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :param context: the :class:`.QueryContext` corresponding to the
+ current :class:`_query.Query` in progress. This argument may be
+ ``None`` if the load does not correspond to a :class:`_query.Query`,
+ such as during :meth:`.Session.merge`.
+
+ .. seealso::
+
+ :ref:`mapped_class_load_events`
+
+ :meth:`.InstanceEvents.init`
+
+ :meth:`.InstanceEvents.refresh`
+
+ :meth:`.SessionEvents.loaded_as_persistent`
+
+ """
+
+ def refresh(
+ self, target: _O, context: QueryContext, attrs: Optional[Iterable[str]]
+ ) -> None:
+ """Receive an object instance after one or more attributes have
+ been refreshed from a query.
+
+ Contrast this to the :meth:`.InstanceEvents.load` method, which
+ is invoked when the object is first loaded from a query.
+
+ .. note:: This event is invoked within the loader process before
+ eager loaders may have been completed, and the object's state may
+ not be complete. Additionally, invoking row-level refresh
+ operations on the object will place the object into a new loader
+ context, interfering with the existing load context. See the note
+ on :meth:`.InstanceEvents.load` for background on making use of the
+ :paramref:`.InstanceEvents.restore_load_context` parameter, in
+ order to resolve this scenario.
+
+ :param target: the mapped instance. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :param context: the :class:`.QueryContext` corresponding to the
+ current :class:`_query.Query` in progress.
+ :param attrs: sequence of attribute names which
+ were populated, or None if all column-mapped, non-deferred
+ attributes were populated.
+
+ .. seealso::
+
+ :ref:`mapped_class_load_events`
+
+ :meth:`.InstanceEvents.load`
+
+ """
+
+ def refresh_flush(
+ self,
+ target: _O,
+ flush_context: UOWTransaction,
+ attrs: Optional[Iterable[str]],
+ ) -> None:
+ """Receive an object instance after one or more attributes that
+ contain a column-level default or onupdate handler have been refreshed
+ during persistence of the object's state.
+
+ This event is the same as :meth:`.InstanceEvents.refresh` except
+ it is invoked within the unit of work flush process, and includes
+ only non-primary-key columns that have column level default or
+ onupdate handlers, including Python callables as well as server side
+ defaults and triggers which may be fetched via the RETURNING clause.
+
+ .. note::
+
+ While the :meth:`.InstanceEvents.refresh_flush` event is triggered
+ for an object that was INSERTed as well as for an object that was
+ UPDATEd, the event is geared primarily towards the UPDATE process;
+ it is mostly an internal artifact that INSERT actions can also
+ trigger this event, and note that **primary key columns for an
+ INSERTed row are explicitly omitted** from this event. In order to
+ intercept the newly INSERTed state of an object, the
+ :meth:`.SessionEvents.pending_to_persistent` and
+ :meth:`.MapperEvents.after_insert` are better choices.
+
+ :param target: the mapped instance. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :param flush_context: Internal :class:`.UOWTransaction` object
+ which handles the details of the flush.
+ :param attrs: sequence of attribute names which
+ were populated.
+
+ .. seealso::
+
+ :ref:`mapped_class_load_events`
+
+ :ref:`orm_server_defaults`
+
+ :ref:`metadata_defaults_toplevel`
+
+ """
+
+ def expire(self, target: _O, attrs: Optional[Iterable[str]]) -> None:
+ """Receive an object instance after its attributes or some subset
+ have been expired.
+
+ 'keys' is a list of attribute names. If None, the entire
+ state was expired.
+
+ :param target: the mapped instance. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :param attrs: sequence of attribute
+ names which were expired, or None if all attributes were
+ expired.
+
+ """
+
+ def pickle(self, target: _O, state_dict: _InstanceDict) -> None:
+ """Receive an object instance when its associated state is
+ being pickled.
+
+ :param target: the mapped instance. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :param state_dict: the dictionary returned by
+ :class:`.InstanceState.__getstate__`, containing the state
+ to be pickled.
+
+ """
+
+ def unpickle(self, target: _O, state_dict: _InstanceDict) -> None:
+ """Receive an object instance after its associated state has
+ been unpickled.
+
+ :param target: the mapped instance. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :param state_dict: the dictionary sent to
+ :class:`.InstanceState.__setstate__`, containing the state
+ dictionary which was pickled.
+
+ """
+
+
+class _EventsHold(event.RefCollection[_ET]):
+ """Hold onto listeners against unmapped, uninstrumented classes.
+
+ Establish _listen() for that class' mapper/instrumentation when
+ those objects are created for that class.
+
+ """
+
+ all_holds: weakref.WeakKeyDictionary[Any, Any]
+
+ def __init__(
+ self,
+ class_: Union[DeclarativeAttributeIntercept, DeclarativeMeta, type],
+ ) -> None:
+ self.class_ = class_
+
+ @classmethod
+ def _clear(cls) -> None:
+ cls.all_holds.clear()
+
+ class HoldEvents(Generic[_ET2]):
+ _dispatch_target: Optional[Type[_ET2]] = None
+
+ @classmethod
+ def _listen(
+ cls,
+ event_key: _EventKey[_ET2],
+ raw: bool = False,
+ propagate: bool = False,
+ retval: bool = False,
+ **kw: Any,
+ ) -> None:
+ target = event_key.dispatch_target
+
+ if target.class_ in target.all_holds:
+ collection = target.all_holds[target.class_]
+ else:
+ collection = target.all_holds[target.class_] = {}
+
+ event.registry._stored_in_collection(event_key, target)
+ collection[event_key._key] = (
+ event_key,
+ raw,
+ propagate,
+ retval,
+ kw,
+ )
+
+ if propagate:
+ stack = list(target.class_.__subclasses__())
+ while stack:
+ subclass = stack.pop(0)
+ stack.extend(subclass.__subclasses__())
+ subject = target.resolve(subclass)
+ if subject is not None:
+ # we are already going through __subclasses__()
+ # so leave generic propagate flag False
+ event_key.with_dispatch_target(subject).listen(
+ raw=raw, propagate=False, retval=retval, **kw
+ )
+
+ def remove(self, event_key: _EventKey[_ET]) -> None:
+ target = event_key.dispatch_target
+
+ if isinstance(target, _EventsHold):
+ collection = target.all_holds[target.class_]
+ del collection[event_key._key]
+
+ @classmethod
+ def populate(
+ cls,
+ class_: Union[DeclarativeAttributeIntercept, DeclarativeMeta, type],
+ subject: Union[ClassManager[_O], Mapper[_O]],
+ ) -> None:
+ for subclass in class_.__mro__:
+ if subclass in cls.all_holds:
+ collection = cls.all_holds[subclass]
+ for (
+ event_key,
+ raw,
+ propagate,
+ retval,
+ kw,
+ ) in collection.values():
+ if propagate or subclass is class_:
+ # since we can't be sure in what order different
+ # classes in a hierarchy are triggered with
+ # populate(), we rely upon _EventsHold for all event
+ # assignment, instead of using the generic propagate
+ # flag.
+ event_key.with_dispatch_target(subject).listen(
+ raw=raw, propagate=False, retval=retval, **kw
+ )
+
+
+class _InstanceEventsHold(_EventsHold[_ET]):
+ all_holds: weakref.WeakKeyDictionary[Any, Any] = (
+ weakref.WeakKeyDictionary()
+ )
+
+ def resolve(self, class_: Type[_O]) -> Optional[ClassManager[_O]]:
+ return instrumentation.opt_manager_of_class(class_)
+
+ class HoldInstanceEvents(_EventsHold.HoldEvents[_ET], InstanceEvents): # type: ignore [misc] # noqa: E501
+ pass
+
+ dispatch = event.dispatcher(HoldInstanceEvents)
+
+
+class MapperEvents(event.Events[mapperlib.Mapper[Any]]):
+ """Define events specific to mappings.
+
+ e.g.::
+
+ from sqlalchemy import event
+
+ def my_before_insert_listener(mapper, connection, target):
+ # execute a stored procedure upon INSERT,
+ # apply the value to the row to be inserted
+ target.calculated_value = connection.execute(
+ text("select my_special_function(%d)" % target.special_number)
+ ).scalar()
+
+ # associate the listener function with SomeClass,
+ # to execute during the "before_insert" hook
+ event.listen(
+ SomeClass, 'before_insert', my_before_insert_listener)
+
+ Available targets include:
+
+ * mapped classes
+ * unmapped superclasses of mapped or to-be-mapped classes
+ (using the ``propagate=True`` flag)
+ * :class:`_orm.Mapper` objects
+ * the :class:`_orm.Mapper` class itself indicates listening for all
+ mappers.
+
+ Mapper events provide hooks into critical sections of the
+ mapper, including those related to object instrumentation,
+ object loading, and object persistence. In particular, the
+ persistence methods :meth:`~.MapperEvents.before_insert`,
+ and :meth:`~.MapperEvents.before_update` are popular
+ places to augment the state being persisted - however, these
+ methods operate with several significant restrictions. The
+ user is encouraged to evaluate the
+ :meth:`.SessionEvents.before_flush` and
+ :meth:`.SessionEvents.after_flush` methods as more
+ flexible and user-friendly hooks in which to apply
+ additional database state during a flush.
+
+ When using :class:`.MapperEvents`, several modifiers are
+ available to the :func:`.event.listen` function.
+
+ :param propagate=False: When True, the event listener should
+ be applied to all inheriting mappers and/or the mappers of
+ inheriting classes, as well as any
+ mapper which is the target of this listener.
+ :param raw=False: When True, the "target" argument passed
+ to applicable event listener functions will be the
+ instance's :class:`.InstanceState` management
+ object, rather than the mapped instance itself.
+ :param retval=False: when True, the user-defined event function
+ must have a return value, the purpose of which is either to
+ control subsequent event propagation, or to otherwise alter
+ the operation in progress by the mapper. Possible return
+ values are:
+
+ * ``sqlalchemy.orm.interfaces.EXT_CONTINUE`` - continue event
+ processing normally.
+ * ``sqlalchemy.orm.interfaces.EXT_STOP`` - cancel all subsequent
+ event handlers in the chain.
+ * other values - the return value specified by specific listeners.
+
+ """
+
+ _target_class_doc = "SomeClass"
+ _dispatch_target = mapperlib.Mapper
+
+ @classmethod
+ def _new_mapper_instance(
+ cls,
+ class_: Union[DeclarativeAttributeIntercept, DeclarativeMeta, type],
+ mapper: Mapper[_O],
+ ) -> None:
+ _MapperEventsHold.populate(class_, mapper)
+
+ @classmethod
+ @util.preload_module("sqlalchemy.orm")
+ def _accept_with(
+ cls,
+ target: Union[mapperlib.Mapper[Any], Type[mapperlib.Mapper[Any]]],
+ identifier: str,
+ ) -> Optional[Union[mapperlib.Mapper[Any], Type[mapperlib.Mapper[Any]]]]:
+ orm = util.preloaded.orm
+
+ if target is orm.mapper: # type: ignore [attr-defined]
+ util.warn_deprecated(
+ "The `sqlalchemy.orm.mapper()` symbol is deprecated and "
+ "will be removed in a future release. For the mapper-wide "
+ "event target, use the 'sqlalchemy.orm.Mapper' class.",
+ "2.0",
+ )
+ return mapperlib.Mapper
+ elif isinstance(target, type):
+ if issubclass(target, mapperlib.Mapper):
+ return target
+ else:
+ mapper = _mapper_or_none(target)
+ if mapper is not None:
+ return mapper
+ else:
+ return _MapperEventsHold(target)
+ else:
+ return target
+
+ @classmethod
+ def _listen(
+ cls,
+ event_key: _EventKey[_ET],
+ raw: bool = False,
+ retval: bool = False,
+ propagate: bool = False,
+ **kw: Any,
+ ) -> None:
+ target, identifier, fn = (
+ event_key.dispatch_target,
+ event_key.identifier,
+ event_key._listen_fn,
+ )
+
+ if (
+ identifier in ("before_configured", "after_configured")
+ and target is not mapperlib.Mapper
+ ):
+ util.warn(
+ "'before_configured' and 'after_configured' ORM events "
+ "only invoke with the Mapper class "
+ "as the target."
+ )
+
+ if not raw or not retval:
+ if not raw:
+ meth = getattr(cls, identifier)
+ try:
+ target_index = (
+ inspect_getfullargspec(meth)[0].index("target") - 1
+ )
+ except ValueError:
+ target_index = None
+
+ def wrap(*arg: Any, **kw: Any) -> Any:
+ if not raw and target_index is not None:
+ arg = list(arg) # type: ignore [assignment]
+ arg[target_index] = arg[target_index].obj() # type: ignore [index] # noqa: E501
+ if not retval:
+ fn(*arg, **kw)
+ return interfaces.EXT_CONTINUE
+ else:
+ return fn(*arg, **kw)
+
+ event_key = event_key.with_wrapper(wrap)
+
+ if propagate:
+ for mapper in target.self_and_descendants:
+ event_key.with_dispatch_target(mapper).base_listen(
+ propagate=True, **kw
+ )
+ else:
+ event_key.base_listen(**kw)
+
+ @classmethod
+ def _clear(cls) -> None:
+ super()._clear()
+ _MapperEventsHold._clear()
+
+ def instrument_class(self, mapper: Mapper[_O], class_: Type[_O]) -> None:
+ r"""Receive a class when the mapper is first constructed,
+ before instrumentation is applied to the mapped class.
+
+ This event is the earliest phase of mapper construction.
+ Most attributes of the mapper are not yet initialized. To
+ receive an event within initial mapper construction where basic
+ state is available such as the :attr:`_orm.Mapper.attrs` collection,
+ the :meth:`_orm.MapperEvents.after_mapper_constructed` event may
+ be a better choice.
+
+ This listener can either be applied to the :class:`_orm.Mapper`
+ class overall, or to any un-mapped class which serves as a base
+ for classes that will be mapped (using the ``propagate=True`` flag)::
+
+ Base = declarative_base()
+
+ @event.listens_for(Base, "instrument_class", propagate=True)
+ def on_new_class(mapper, cls_):
+ " ... "
+
+ :param mapper: the :class:`_orm.Mapper` which is the target
+ of this event.
+ :param class\_: the mapped class.
+
+ .. seealso::
+
+ :meth:`_orm.MapperEvents.after_mapper_constructed`
+
+ """
+
+ def after_mapper_constructed(
+ self, mapper: Mapper[_O], class_: Type[_O]
+ ) -> None:
+ """Receive a class and mapper when the :class:`_orm.Mapper` has been
+ fully constructed.
+
+ This event is called after the initial constructor for
+ :class:`_orm.Mapper` completes. This occurs after the
+ :meth:`_orm.MapperEvents.instrument_class` event and after the
+ :class:`_orm.Mapper` has done an initial pass of its arguments
+ to generate its collection of :class:`_orm.MapperProperty` objects,
+ which are accessible via the :meth:`_orm.Mapper.get_property`
+ method and the :attr:`_orm.Mapper.iterate_properties` attribute.
+
+ This event differs from the
+ :meth:`_orm.MapperEvents.before_mapper_configured` event in that it
+ is invoked within the constructor for :class:`_orm.Mapper`, rather
+ than within the :meth:`_orm.registry.configure` process. Currently,
+ this event is the only one which is appropriate for handlers that
+ wish to create additional mapped classes in response to the
+ construction of this :class:`_orm.Mapper`, which will be part of the
+ same configure step when :meth:`_orm.registry.configure` next runs.
+
+ .. versionadded:: 2.0.2
+
+ .. seealso::
+
+ :ref:`examples_versioning` - an example which illustrates the use
+ of the :meth:`_orm.MapperEvents.before_mapper_configured`
+ event to create new mappers to record change-audit histories on
+ objects.
+
+ """
+
+ def before_mapper_configured(
+ self, mapper: Mapper[_O], class_: Type[_O]
+ ) -> None:
+ """Called right before a specific mapper is to be configured.
+
+ This event is intended to allow a specific mapper to be skipped during
+ the configure step, by returning the :attr:`.orm.interfaces.EXT_SKIP`
+ symbol which indicates to the :func:`.configure_mappers` call that this
+ particular mapper (or hierarchy of mappers, if ``propagate=True`` is
+ used) should be skipped in the current configuration run. When one or
+ more mappers are skipped, the he "new mappers" flag will remain set,
+ meaning the :func:`.configure_mappers` function will continue to be
+ called when mappers are used, to continue to try to configure all
+ available mappers.
+
+ In comparison to the other configure-level events,
+ :meth:`.MapperEvents.before_configured`,
+ :meth:`.MapperEvents.after_configured`, and
+ :meth:`.MapperEvents.mapper_configured`, the
+ :meth;`.MapperEvents.before_mapper_configured` event provides for a
+ meaningful return value when it is registered with the ``retval=True``
+ parameter.
+
+ .. versionadded:: 1.3
+
+ e.g.::
+
+ from sqlalchemy.orm import EXT_SKIP
+
+ Base = declarative_base()
+
+ DontConfigureBase = declarative_base()
+
+ @event.listens_for(
+ DontConfigureBase,
+ "before_mapper_configured", retval=True, propagate=True)
+ def dont_configure(mapper, cls):
+ return EXT_SKIP
+
+
+ .. seealso::
+
+ :meth:`.MapperEvents.before_configured`
+
+ :meth:`.MapperEvents.after_configured`
+
+ :meth:`.MapperEvents.mapper_configured`
+
+ """
+
+ def mapper_configured(self, mapper: Mapper[_O], class_: Type[_O]) -> None:
+ r"""Called when a specific mapper has completed its own configuration
+ within the scope of the :func:`.configure_mappers` call.
+
+ The :meth:`.MapperEvents.mapper_configured` event is invoked
+ for each mapper that is encountered when the
+ :func:`_orm.configure_mappers` function proceeds through the current
+ list of not-yet-configured mappers.
+ :func:`_orm.configure_mappers` is typically invoked
+ automatically as mappings are first used, as well as each time
+ new mappers have been made available and new mapper use is
+ detected.
+
+ When the event is called, the mapper should be in its final
+ state, but **not including backrefs** that may be invoked from
+ other mappers; they might still be pending within the
+ configuration operation. Bidirectional relationships that
+ are instead configured via the
+ :paramref:`.orm.relationship.back_populates` argument
+ *will* be fully available, since this style of relationship does not
+ rely upon other possibly-not-configured mappers to know that they
+ exist.
+
+ For an event that is guaranteed to have **all** mappers ready
+ to go including backrefs that are defined only on other
+ mappings, use the :meth:`.MapperEvents.after_configured`
+ event; this event invokes only after all known mappings have been
+ fully configured.
+
+ The :meth:`.MapperEvents.mapper_configured` event, unlike
+ :meth:`.MapperEvents.before_configured` or
+ :meth:`.MapperEvents.after_configured`,
+ is called for each mapper/class individually, and the mapper is
+ passed to the event itself. It also is called exactly once for
+ a particular mapper. The event is therefore useful for
+ configurational steps that benefit from being invoked just once
+ on a specific mapper basis, which don't require that "backref"
+ configurations are necessarily ready yet.
+
+ :param mapper: the :class:`_orm.Mapper` which is the target
+ of this event.
+ :param class\_: the mapped class.
+
+ .. seealso::
+
+ :meth:`.MapperEvents.before_configured`
+
+ :meth:`.MapperEvents.after_configured`
+
+ :meth:`.MapperEvents.before_mapper_configured`
+
+ """
+ # TODO: need coverage for this event
+
+ def before_configured(self) -> None:
+ """Called before a series of mappers have been configured.
+
+ The :meth:`.MapperEvents.before_configured` event is invoked
+ each time the :func:`_orm.configure_mappers` function is
+ invoked, before the function has done any of its work.
+ :func:`_orm.configure_mappers` is typically invoked
+ automatically as mappings are first used, as well as each time
+ new mappers have been made available and new mapper use is
+ detected.
+
+ This event can **only** be applied to the :class:`_orm.Mapper` class,
+ and not to individual mappings or mapped classes. It is only invoked
+ for all mappings as a whole::
+
+ from sqlalchemy.orm import Mapper
+
+ @event.listens_for(Mapper, "before_configured")
+ def go():
+ ...
+
+ Contrast this event to :meth:`.MapperEvents.after_configured`,
+ which is invoked after the series of mappers has been configured,
+ as well as :meth:`.MapperEvents.before_mapper_configured`
+ and :meth:`.MapperEvents.mapper_configured`, which are both invoked
+ on a per-mapper basis.
+
+ Theoretically this event is called once per
+ application, but is actually called any time new mappers
+ are to be affected by a :func:`_orm.configure_mappers`
+ call. If new mappings are constructed after existing ones have
+ already been used, this event will likely be called again. To ensure
+ that a particular event is only called once and no further, the
+ ``once=True`` argument (new in 0.9.4) can be applied::
+
+ from sqlalchemy.orm import mapper
+
+ @event.listens_for(mapper, "before_configured", once=True)
+ def go():
+ ...
+
+
+ .. seealso::
+
+ :meth:`.MapperEvents.before_mapper_configured`
+
+ :meth:`.MapperEvents.mapper_configured`
+
+ :meth:`.MapperEvents.after_configured`
+
+ """
+
+ def after_configured(self) -> None:
+ """Called after a series of mappers have been configured.
+
+ The :meth:`.MapperEvents.after_configured` event is invoked
+ each time the :func:`_orm.configure_mappers` function is
+ invoked, after the function has completed its work.
+ :func:`_orm.configure_mappers` is typically invoked
+ automatically as mappings are first used, as well as each time
+ new mappers have been made available and new mapper use is
+ detected.
+
+ Contrast this event to the :meth:`.MapperEvents.mapper_configured`
+ event, which is called on a per-mapper basis while the configuration
+ operation proceeds; unlike that event, when this event is invoked,
+ all cross-configurations (e.g. backrefs) will also have been made
+ available for any mappers that were pending.
+ Also contrast to :meth:`.MapperEvents.before_configured`,
+ which is invoked before the series of mappers has been configured.
+
+ This event can **only** be applied to the :class:`_orm.Mapper` class,
+ and not to individual mappings or
+ mapped classes. It is only invoked for all mappings as a whole::
+
+ from sqlalchemy.orm import Mapper
+
+ @event.listens_for(Mapper, "after_configured")
+ def go():
+ # ...
+
+ Theoretically this event is called once per
+ application, but is actually called any time new mappers
+ have been affected by a :func:`_orm.configure_mappers`
+ call. If new mappings are constructed after existing ones have
+ already been used, this event will likely be called again. To ensure
+ that a particular event is only called once and no further, the
+ ``once=True`` argument (new in 0.9.4) can be applied::
+
+ from sqlalchemy.orm import mapper
+
+ @event.listens_for(mapper, "after_configured", once=True)
+ def go():
+ # ...
+
+ .. seealso::
+
+ :meth:`.MapperEvents.before_mapper_configured`
+
+ :meth:`.MapperEvents.mapper_configured`
+
+ :meth:`.MapperEvents.before_configured`
+
+ """
+
+ def before_insert(
+ self, mapper: Mapper[_O], connection: Connection, target: _O
+ ) -> None:
+ """Receive an object instance before an INSERT statement
+ is emitted corresponding to that instance.
+
+ .. note:: this event **only** applies to the
+ :ref:`session flush operation <session_flushing>`
+ and does **not** apply to the ORM DML operations described at
+ :ref:`orm_expression_update_delete`. To intercept ORM
+ DML events, use :meth:`_orm.SessionEvents.do_orm_execute`.
+
+ This event is used to modify local, non-object related
+ attributes on the instance before an INSERT occurs, as well
+ as to emit additional SQL statements on the given
+ connection.
+
+ The event is often called for a batch of objects of the
+ same class before their INSERT statements are emitted at
+ once in a later step. In the extremely rare case that
+ this is not desirable, the :class:`_orm.Mapper` object can be
+ configured with ``batch=False``, which will cause
+ batches of instances to be broken up into individual
+ (and more poorly performing) event->persist->event
+ steps.
+
+ .. warning::
+
+ Mapper-level flush events only allow **very limited operations**,
+ on attributes local to the row being operated upon only,
+ as well as allowing any SQL to be emitted on the given
+ :class:`_engine.Connection`. **Please read fully** the notes
+ at :ref:`session_persistence_mapper` for guidelines on using
+ these methods; generally, the :meth:`.SessionEvents.before_flush`
+ method should be preferred for general on-flush changes.
+
+ :param mapper: the :class:`_orm.Mapper` which is the target
+ of this event.
+ :param connection: the :class:`_engine.Connection` being used to
+ emit INSERT statements for this instance. This
+ provides a handle into the current transaction on the
+ target database specific to this instance.
+ :param target: the mapped instance being persisted. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :return: No return value is supported by this event.
+
+ .. seealso::
+
+ :ref:`session_persistence_events`
+
+ """
+
+ def after_insert(
+ self, mapper: Mapper[_O], connection: Connection, target: _O
+ ) -> None:
+ """Receive an object instance after an INSERT statement
+ is emitted corresponding to that instance.
+
+ .. note:: this event **only** applies to the
+ :ref:`session flush operation <session_flushing>`
+ and does **not** apply to the ORM DML operations described at
+ :ref:`orm_expression_update_delete`. To intercept ORM
+ DML events, use :meth:`_orm.SessionEvents.do_orm_execute`.
+
+ This event is used to modify in-Python-only
+ state on the instance after an INSERT occurs, as well
+ as to emit additional SQL statements on the given
+ connection.
+
+ The event is often called for a batch of objects of the
+ same class after their INSERT statements have been
+ emitted at once in a previous step. In the extremely
+ rare case that this is not desirable, the
+ :class:`_orm.Mapper` object can be configured with ``batch=False``,
+ which will cause batches of instances to be broken up
+ into individual (and more poorly performing)
+ event->persist->event steps.
+
+ .. warning::
+
+ Mapper-level flush events only allow **very limited operations**,
+ on attributes local to the row being operated upon only,
+ as well as allowing any SQL to be emitted on the given
+ :class:`_engine.Connection`. **Please read fully** the notes
+ at :ref:`session_persistence_mapper` for guidelines on using
+ these methods; generally, the :meth:`.SessionEvents.before_flush`
+ method should be preferred for general on-flush changes.
+
+ :param mapper: the :class:`_orm.Mapper` which is the target
+ of this event.
+ :param connection: the :class:`_engine.Connection` being used to
+ emit INSERT statements for this instance. This
+ provides a handle into the current transaction on the
+ target database specific to this instance.
+ :param target: the mapped instance being persisted. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :return: No return value is supported by this event.
+
+ .. seealso::
+
+ :ref:`session_persistence_events`
+
+ """
+
+ def before_update(
+ self, mapper: Mapper[_O], connection: Connection, target: _O
+ ) -> None:
+ """Receive an object instance before an UPDATE statement
+ is emitted corresponding to that instance.
+
+ .. note:: this event **only** applies to the
+ :ref:`session flush operation <session_flushing>`
+ and does **not** apply to the ORM DML operations described at
+ :ref:`orm_expression_update_delete`. To intercept ORM
+ DML events, use :meth:`_orm.SessionEvents.do_orm_execute`.
+
+ This event is used to modify local, non-object related
+ attributes on the instance before an UPDATE occurs, as well
+ as to emit additional SQL statements on the given
+ connection.
+
+ This method is called for all instances that are
+ marked as "dirty", *even those which have no net changes
+ to their column-based attributes*. An object is marked
+ as dirty when any of its column-based attributes have a
+ "set attribute" operation called or when any of its
+ collections are modified. If, at update time, no
+ column-based attributes have any net changes, no UPDATE
+ statement will be issued. This means that an instance
+ being sent to :meth:`~.MapperEvents.before_update` is
+ *not* a guarantee that an UPDATE statement will be
+ issued, although you can affect the outcome here by
+ modifying attributes so that a net change in value does
+ exist.
+
+ To detect if the column-based attributes on the object have net
+ changes, and will therefore generate an UPDATE statement, use
+ ``object_session(instance).is_modified(instance,
+ include_collections=False)``.
+
+ The event is often called for a batch of objects of the
+ same class before their UPDATE statements are emitted at
+ once in a later step. In the extremely rare case that
+ this is not desirable, the :class:`_orm.Mapper` can be
+ configured with ``batch=False``, which will cause
+ batches of instances to be broken up into individual
+ (and more poorly performing) event->persist->event
+ steps.
+
+ .. warning::
+
+ Mapper-level flush events only allow **very limited operations**,
+ on attributes local to the row being operated upon only,
+ as well as allowing any SQL to be emitted on the given
+ :class:`_engine.Connection`. **Please read fully** the notes
+ at :ref:`session_persistence_mapper` for guidelines on using
+ these methods; generally, the :meth:`.SessionEvents.before_flush`
+ method should be preferred for general on-flush changes.
+
+ :param mapper: the :class:`_orm.Mapper` which is the target
+ of this event.
+ :param connection: the :class:`_engine.Connection` being used to
+ emit UPDATE statements for this instance. This
+ provides a handle into the current transaction on the
+ target database specific to this instance.
+ :param target: the mapped instance being persisted. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :return: No return value is supported by this event.
+
+ .. seealso::
+
+ :ref:`session_persistence_events`
+
+ """
+
+ def after_update(
+ self, mapper: Mapper[_O], connection: Connection, target: _O
+ ) -> None:
+ """Receive an object instance after an UPDATE statement
+ is emitted corresponding to that instance.
+
+ .. note:: this event **only** applies to the
+ :ref:`session flush operation <session_flushing>`
+ and does **not** apply to the ORM DML operations described at
+ :ref:`orm_expression_update_delete`. To intercept ORM
+ DML events, use :meth:`_orm.SessionEvents.do_orm_execute`.
+
+ This event is used to modify in-Python-only
+ state on the instance after an UPDATE occurs, as well
+ as to emit additional SQL statements on the given
+ connection.
+
+ This method is called for all instances that are
+ marked as "dirty", *even those which have no net changes
+ to their column-based attributes*, and for which
+ no UPDATE statement has proceeded. An object is marked
+ as dirty when any of its column-based attributes have a
+ "set attribute" operation called or when any of its
+ collections are modified. If, at update time, no
+ column-based attributes have any net changes, no UPDATE
+ statement will be issued. This means that an instance
+ being sent to :meth:`~.MapperEvents.after_update` is
+ *not* a guarantee that an UPDATE statement has been
+ issued.
+
+ To detect if the column-based attributes on the object have net
+ changes, and therefore resulted in an UPDATE statement, use
+ ``object_session(instance).is_modified(instance,
+ include_collections=False)``.
+
+ The event is often called for a batch of objects of the
+ same class after their UPDATE statements have been emitted at
+ once in a previous step. In the extremely rare case that
+ this is not desirable, the :class:`_orm.Mapper` can be
+ configured with ``batch=False``, which will cause
+ batches of instances to be broken up into individual
+ (and more poorly performing) event->persist->event
+ steps.
+
+ .. warning::
+
+ Mapper-level flush events only allow **very limited operations**,
+ on attributes local to the row being operated upon only,
+ as well as allowing any SQL to be emitted on the given
+ :class:`_engine.Connection`. **Please read fully** the notes
+ at :ref:`session_persistence_mapper` for guidelines on using
+ these methods; generally, the :meth:`.SessionEvents.before_flush`
+ method should be preferred for general on-flush changes.
+
+ :param mapper: the :class:`_orm.Mapper` which is the target
+ of this event.
+ :param connection: the :class:`_engine.Connection` being used to
+ emit UPDATE statements for this instance. This
+ provides a handle into the current transaction on the
+ target database specific to this instance.
+ :param target: the mapped instance being persisted. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :return: No return value is supported by this event.
+
+ .. seealso::
+
+ :ref:`session_persistence_events`
+
+ """
+
+ def before_delete(
+ self, mapper: Mapper[_O], connection: Connection, target: _O
+ ) -> None:
+ """Receive an object instance before a DELETE statement
+ is emitted corresponding to that instance.
+
+ .. note:: this event **only** applies to the
+ :ref:`session flush operation <session_flushing>`
+ and does **not** apply to the ORM DML operations described at
+ :ref:`orm_expression_update_delete`. To intercept ORM
+ DML events, use :meth:`_orm.SessionEvents.do_orm_execute`.
+
+ This event is used to emit additional SQL statements on
+ the given connection as well as to perform application
+ specific bookkeeping related to a deletion event.
+
+ The event is often called for a batch of objects of the
+ same class before their DELETE statements are emitted at
+ once in a later step.
+
+ .. warning::
+
+ Mapper-level flush events only allow **very limited operations**,
+ on attributes local to the row being operated upon only,
+ as well as allowing any SQL to be emitted on the given
+ :class:`_engine.Connection`. **Please read fully** the notes
+ at :ref:`session_persistence_mapper` for guidelines on using
+ these methods; generally, the :meth:`.SessionEvents.before_flush`
+ method should be preferred for general on-flush changes.
+
+ :param mapper: the :class:`_orm.Mapper` which is the target
+ of this event.
+ :param connection: the :class:`_engine.Connection` being used to
+ emit DELETE statements for this instance. This
+ provides a handle into the current transaction on the
+ target database specific to this instance.
+ :param target: the mapped instance being deleted. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :return: No return value is supported by this event.
+
+ .. seealso::
+
+ :ref:`session_persistence_events`
+
+ """
+
+ def after_delete(
+ self, mapper: Mapper[_O], connection: Connection, target: _O
+ ) -> None:
+ """Receive an object instance after a DELETE statement
+ has been emitted corresponding to that instance.
+
+ .. note:: this event **only** applies to the
+ :ref:`session flush operation <session_flushing>`
+ and does **not** apply to the ORM DML operations described at
+ :ref:`orm_expression_update_delete`. To intercept ORM
+ DML events, use :meth:`_orm.SessionEvents.do_orm_execute`.
+
+ This event is used to emit additional SQL statements on
+ the given connection as well as to perform application
+ specific bookkeeping related to a deletion event.
+
+ The event is often called for a batch of objects of the
+ same class after their DELETE statements have been emitted at
+ once in a previous step.
+
+ .. warning::
+
+ Mapper-level flush events only allow **very limited operations**,
+ on attributes local to the row being operated upon only,
+ as well as allowing any SQL to be emitted on the given
+ :class:`_engine.Connection`. **Please read fully** the notes
+ at :ref:`session_persistence_mapper` for guidelines on using
+ these methods; generally, the :meth:`.SessionEvents.before_flush`
+ method should be preferred for general on-flush changes.
+
+ :param mapper: the :class:`_orm.Mapper` which is the target
+ of this event.
+ :param connection: the :class:`_engine.Connection` being used to
+ emit DELETE statements for this instance. This
+ provides a handle into the current transaction on the
+ target database specific to this instance.
+ :param target: the mapped instance being deleted. If
+ the event is configured with ``raw=True``, this will
+ instead be the :class:`.InstanceState` state-management
+ object associated with the instance.
+ :return: No return value is supported by this event.
+
+ .. seealso::
+
+ :ref:`session_persistence_events`
+
+ """
+
+
+class _MapperEventsHold(_EventsHold[_ET]):
+ all_holds = weakref.WeakKeyDictionary()
+
+ def resolve(
+ self, class_: Union[Type[_T], _InternalEntityType[_T]]
+ ) -> Optional[Mapper[_T]]:
+ return _mapper_or_none(class_)
+
+ class HoldMapperEvents(_EventsHold.HoldEvents[_ET], MapperEvents): # type: ignore [misc] # noqa: E501
+ pass
+
+ dispatch = event.dispatcher(HoldMapperEvents)
+
+
+_sessionevents_lifecycle_event_names: Set[str] = set()
+
+
+class SessionEvents(event.Events[Session]):
+ """Define events specific to :class:`.Session` lifecycle.
+
+ e.g.::
+
+ from sqlalchemy import event
+ from sqlalchemy.orm import sessionmaker
+
+ def my_before_commit(session):
+ print("before commit!")
+
+ Session = sessionmaker()
+
+ event.listen(Session, "before_commit", my_before_commit)
+
+ The :func:`~.event.listen` function will accept
+ :class:`.Session` objects as well as the return result
+ of :class:`~.sessionmaker()` and :class:`~.scoped_session()`.
+
+ Additionally, it accepts the :class:`.Session` class which
+ will apply listeners to all :class:`.Session` instances
+ globally.
+
+ :param raw=False: When True, the "target" argument passed
+ to applicable event listener functions that work on individual
+ objects will be the instance's :class:`.InstanceState` management
+ object, rather than the mapped instance itself.
+
+ .. versionadded:: 1.3.14
+
+ :param restore_load_context=False: Applies to the
+ :meth:`.SessionEvents.loaded_as_persistent` event. Restores the loader
+ context of the object when the event hook is complete, so that ongoing
+ eager load operations continue to target the object appropriately. A
+ warning is emitted if the object is moved to a new loader context from
+ within this event if this flag is not set.
+
+ .. versionadded:: 1.3.14
+
+ """
+
+ _target_class_doc = "SomeSessionClassOrObject"
+
+ _dispatch_target = Session
+
+ def _lifecycle_event( # type: ignore [misc]
+ fn: Callable[[SessionEvents, Session, Any], None]
+ ) -> Callable[[SessionEvents, Session, Any], None]:
+ _sessionevents_lifecycle_event_names.add(fn.__name__)
+ return fn
+
+ @classmethod
+ def _accept_with( # type: ignore [return]
+ cls, target: Any, identifier: str
+ ) -> Union[Session, type]:
+ if isinstance(target, scoped_session):
+ target = target.session_factory
+ if not isinstance(target, sessionmaker) and (
+ not isinstance(target, type) or not issubclass(target, Session)
+ ):
+ raise exc.ArgumentError(
+ "Session event listen on a scoped_session "
+ "requires that its creation callable "
+ "is associated with the Session class."
+ )
+
+ if isinstance(target, sessionmaker):
+ return target.class_
+ elif isinstance(target, type):
+ if issubclass(target, scoped_session):
+ return Session
+ elif issubclass(target, Session):
+ return target
+ elif isinstance(target, Session):
+ return target
+ elif hasattr(target, "_no_async_engine_events"):
+ target._no_async_engine_events()
+ else:
+ # allows alternate SessionEvents-like-classes to be consulted
+ return event.Events._accept_with(target, identifier) # type: ignore [return-value] # noqa: E501
+
+ @classmethod
+ def _listen(
+ cls,
+ event_key: Any,
+ *,
+ raw: bool = False,
+ restore_load_context: bool = False,
+ **kw: Any,
+ ) -> None:
+ is_instance_event = (
+ event_key.identifier in _sessionevents_lifecycle_event_names
+ )
+
+ if is_instance_event:
+ if not raw or restore_load_context:
+ fn = event_key._listen_fn
+
+ def wrap(
+ session: Session,
+ state: InstanceState[_O],
+ *arg: Any,
+ **kw: Any,
+ ) -> Optional[Any]:
+ if not raw:
+ target = state.obj()
+ if target is None:
+ # existing behavior is that if the object is
+ # garbage collected, no event is emitted
+ return None
+ else:
+ target = state # type: ignore [assignment]
+ if restore_load_context:
+ runid = state.runid
+ try:
+ return fn(session, target, *arg, **kw)
+ finally:
+ if restore_load_context:
+ state.runid = runid
+
+ event_key = event_key.with_wrapper(wrap)
+
+ event_key.base_listen(**kw)
+
+ def do_orm_execute(self, orm_execute_state: ORMExecuteState) -> None:
+ """Intercept statement executions that occur on behalf of an
+ ORM :class:`.Session` object.
+
+ This event is invoked for all top-level SQL statements invoked from the
+ :meth:`_orm.Session.execute` method, as well as related methods such as
+ :meth:`_orm.Session.scalars` and :meth:`_orm.Session.scalar`. As of
+ SQLAlchemy 1.4, all ORM queries that run through the
+ :meth:`_orm.Session.execute` method as well as related methods
+ :meth:`_orm.Session.scalars`, :meth:`_orm.Session.scalar` etc.
+ will participate in this event.
+ This event hook does **not** apply to the queries that are
+ emitted internally within the ORM flush process, i.e. the
+ process described at :ref:`session_flushing`.
+
+ .. note:: The :meth:`_orm.SessionEvents.do_orm_execute` event hook
+ is triggered **for ORM statement executions only**, meaning those
+ invoked via the :meth:`_orm.Session.execute` and similar methods on
+ the :class:`_orm.Session` object. It does **not** trigger for
+ statements that are invoked by SQLAlchemy Core only, i.e. statements
+ invoked directly using :meth:`_engine.Connection.execute` or
+ otherwise originating from an :class:`_engine.Engine` object without
+ any :class:`_orm.Session` involved. To intercept **all** SQL
+ executions regardless of whether the Core or ORM APIs are in use,
+ see the event hooks at :class:`.ConnectionEvents`, such as
+ :meth:`.ConnectionEvents.before_execute` and
+ :meth:`.ConnectionEvents.before_cursor_execute`.
+
+ Also, this event hook does **not** apply to queries that are
+ emitted internally within the ORM flush process,
+ i.e. the process described at :ref:`session_flushing`; to
+ intercept steps within the flush process, see the event
+ hooks described at :ref:`session_persistence_events` as
+ well as :ref:`session_persistence_mapper`.
+
+ This event is a ``do_`` event, meaning it has the capability to replace
+ the operation that the :meth:`_orm.Session.execute` method normally
+ performs. The intended use for this includes sharding and
+ result-caching schemes which may seek to invoke the same statement
+ across multiple database connections, returning a result that is
+ merged from each of them, or which don't invoke the statement at all,
+ instead returning data from a cache.
+
+ The hook intends to replace the use of the
+ ``Query._execute_and_instances`` method that could be subclassed prior
+ to SQLAlchemy 1.4.
+
+ :param orm_execute_state: an instance of :class:`.ORMExecuteState`
+ which contains all information about the current execution, as well
+ as helper functions used to derive other commonly required
+ information. See that object for details.
+
+ .. seealso::
+
+ :ref:`session_execute_events` - top level documentation on how
+ to use :meth:`_orm.SessionEvents.do_orm_execute`
+
+ :class:`.ORMExecuteState` - the object passed to the
+ :meth:`_orm.SessionEvents.do_orm_execute` event which contains
+ all information about the statement to be invoked. It also
+ provides an interface to extend the current statement, options,
+ and parameters as well as an option that allows programmatic
+ invocation of the statement at any point.
+
+ :ref:`examples_session_orm_events` - includes examples of using
+ :meth:`_orm.SessionEvents.do_orm_execute`
+
+ :ref:`examples_caching` - an example of how to integrate
+ Dogpile caching with the ORM :class:`_orm.Session` making use
+ of the :meth:`_orm.SessionEvents.do_orm_execute` event hook.
+
+ :ref:`examples_sharding` - the Horizontal Sharding example /
+ extension relies upon the
+ :meth:`_orm.SessionEvents.do_orm_execute` event hook to invoke a
+ SQL statement on multiple backends and return a merged result.
+
+
+ .. versionadded:: 1.4
+
+ """
+
+ def after_transaction_create(
+ self, session: Session, transaction: SessionTransaction
+ ) -> None:
+ """Execute when a new :class:`.SessionTransaction` is created.
+
+ This event differs from :meth:`~.SessionEvents.after_begin`
+ in that it occurs for each :class:`.SessionTransaction`
+ overall, as opposed to when transactions are begun
+ on individual database connections. It is also invoked
+ for nested transactions and subtransactions, and is always
+ matched by a corresponding
+ :meth:`~.SessionEvents.after_transaction_end` event
+ (assuming normal operation of the :class:`.Session`).
+
+ :param session: the target :class:`.Session`.
+ :param transaction: the target :class:`.SessionTransaction`.
+
+ To detect if this is the outermost
+ :class:`.SessionTransaction`, as opposed to a "subtransaction" or a
+ SAVEPOINT, test that the :attr:`.SessionTransaction.parent` attribute
+ is ``None``::
+
+ @event.listens_for(session, "after_transaction_create")
+ def after_transaction_create(session, transaction):
+ if transaction.parent is None:
+ # work with top-level transaction
+
+ To detect if the :class:`.SessionTransaction` is a SAVEPOINT, use the
+ :attr:`.SessionTransaction.nested` attribute::
+
+ @event.listens_for(session, "after_transaction_create")
+ def after_transaction_create(session, transaction):
+ if transaction.nested:
+ # work with SAVEPOINT transaction
+
+
+ .. seealso::
+
+ :class:`.SessionTransaction`
+
+ :meth:`~.SessionEvents.after_transaction_end`
+
+ """
+
+ def after_transaction_end(
+ self, session: Session, transaction: SessionTransaction
+ ) -> None:
+ """Execute when the span of a :class:`.SessionTransaction` ends.
+
+ This event differs from :meth:`~.SessionEvents.after_commit`
+ in that it corresponds to all :class:`.SessionTransaction`
+ objects in use, including those for nested transactions
+ and subtransactions, and is always matched by a corresponding
+ :meth:`~.SessionEvents.after_transaction_create` event.
+
+ :param session: the target :class:`.Session`.
+ :param transaction: the target :class:`.SessionTransaction`.
+
+ To detect if this is the outermost
+ :class:`.SessionTransaction`, as opposed to a "subtransaction" or a
+ SAVEPOINT, test that the :attr:`.SessionTransaction.parent` attribute
+ is ``None``::
+
+ @event.listens_for(session, "after_transaction_create")
+ def after_transaction_end(session, transaction):
+ if transaction.parent is None:
+ # work with top-level transaction
+
+ To detect if the :class:`.SessionTransaction` is a SAVEPOINT, use the
+ :attr:`.SessionTransaction.nested` attribute::
+
+ @event.listens_for(session, "after_transaction_create")
+ def after_transaction_end(session, transaction):
+ if transaction.nested:
+ # work with SAVEPOINT transaction
+
+
+ .. seealso::
+
+ :class:`.SessionTransaction`
+
+ :meth:`~.SessionEvents.after_transaction_create`
+
+ """
+
+ def before_commit(self, session: Session) -> None:
+ """Execute before commit is called.
+
+ .. note::
+
+ The :meth:`~.SessionEvents.before_commit` hook is *not* per-flush,
+ that is, the :class:`.Session` can emit SQL to the database
+ many times within the scope of a transaction.
+ For interception of these events, use the
+ :meth:`~.SessionEvents.before_flush`,
+ :meth:`~.SessionEvents.after_flush`, or
+ :meth:`~.SessionEvents.after_flush_postexec`
+ events.
+
+ :param session: The target :class:`.Session`.
+
+ .. seealso::
+
+ :meth:`~.SessionEvents.after_commit`
+
+ :meth:`~.SessionEvents.after_begin`
+
+ :meth:`~.SessionEvents.after_transaction_create`
+
+ :meth:`~.SessionEvents.after_transaction_end`
+
+ """
+
+ def after_commit(self, session: Session) -> None:
+ """Execute after a commit has occurred.
+
+ .. note::
+
+ The :meth:`~.SessionEvents.after_commit` hook is *not* per-flush,
+ that is, the :class:`.Session` can emit SQL to the database
+ many times within the scope of a transaction.
+ For interception of these events, use the
+ :meth:`~.SessionEvents.before_flush`,
+ :meth:`~.SessionEvents.after_flush`, or
+ :meth:`~.SessionEvents.after_flush_postexec`
+ events.
+
+ .. note::
+
+ The :class:`.Session` is not in an active transaction
+ when the :meth:`~.SessionEvents.after_commit` event is invoked,
+ and therefore can not emit SQL. To emit SQL corresponding to
+ every transaction, use the :meth:`~.SessionEvents.before_commit`
+ event.
+
+ :param session: The target :class:`.Session`.
+
+ .. seealso::
+
+ :meth:`~.SessionEvents.before_commit`
+
+ :meth:`~.SessionEvents.after_begin`
+
+ :meth:`~.SessionEvents.after_transaction_create`
+
+ :meth:`~.SessionEvents.after_transaction_end`
+
+ """
+
+ def after_rollback(self, session: Session) -> None:
+ """Execute after a real DBAPI rollback has occurred.
+
+ Note that this event only fires when the *actual* rollback against
+ the database occurs - it does *not* fire each time the
+ :meth:`.Session.rollback` method is called, if the underlying
+ DBAPI transaction has already been rolled back. In many
+ cases, the :class:`.Session` will not be in
+ an "active" state during this event, as the current
+ transaction is not valid. To acquire a :class:`.Session`
+ which is active after the outermost rollback has proceeded,
+ use the :meth:`.SessionEvents.after_soft_rollback` event, checking the
+ :attr:`.Session.is_active` flag.
+
+ :param session: The target :class:`.Session`.
+
+ """
+
+ def after_soft_rollback(
+ self, session: Session, previous_transaction: SessionTransaction
+ ) -> None:
+ """Execute after any rollback has occurred, including "soft"
+ rollbacks that don't actually emit at the DBAPI level.
+
+ This corresponds to both nested and outer rollbacks, i.e.
+ the innermost rollback that calls the DBAPI's
+ rollback() method, as well as the enclosing rollback
+ calls that only pop themselves from the transaction stack.
+
+ The given :class:`.Session` can be used to invoke SQL and
+ :meth:`.Session.query` operations after an outermost rollback
+ by first checking the :attr:`.Session.is_active` flag::
+
+ @event.listens_for(Session, "after_soft_rollback")
+ def do_something(session, previous_transaction):
+ if session.is_active:
+ session.execute(text("select * from some_table"))
+
+ :param session: The target :class:`.Session`.
+ :param previous_transaction: The :class:`.SessionTransaction`
+ transactional marker object which was just closed. The current
+ :class:`.SessionTransaction` for the given :class:`.Session` is
+ available via the :attr:`.Session.transaction` attribute.
+
+ """
+
+ def before_flush(
+ self,
+ session: Session,
+ flush_context: UOWTransaction,
+ instances: Optional[Sequence[_O]],
+ ) -> None:
+ """Execute before flush process has started.
+
+ :param session: The target :class:`.Session`.
+ :param flush_context: Internal :class:`.UOWTransaction` object
+ which handles the details of the flush.
+ :param instances: Usually ``None``, this is the collection of
+ objects which can be passed to the :meth:`.Session.flush` method
+ (note this usage is deprecated).
+
+ .. seealso::
+
+ :meth:`~.SessionEvents.after_flush`
+
+ :meth:`~.SessionEvents.after_flush_postexec`
+
+ :ref:`session_persistence_events`
+
+ """
+
+ def after_flush(
+ self, session: Session, flush_context: UOWTransaction
+ ) -> None:
+ """Execute after flush has completed, but before commit has been
+ called.
+
+ Note that the session's state is still in pre-flush, i.e. 'new',
+ 'dirty', and 'deleted' lists still show pre-flush state as well
+ as the history settings on instance attributes.
+
+ .. warning:: This event runs after the :class:`.Session` has emitted
+ SQL to modify the database, but **before** it has altered its
+ internal state to reflect those changes, including that newly
+ inserted objects are placed into the identity map. ORM operations
+ emitted within this event such as loads of related items
+ may produce new identity map entries that will immediately
+ be replaced, sometimes causing confusing results. SQLAlchemy will
+ emit a warning for this condition as of version 1.3.9.
+
+ :param session: The target :class:`.Session`.
+ :param flush_context: Internal :class:`.UOWTransaction` object
+ which handles the details of the flush.
+
+ .. seealso::
+
+ :meth:`~.SessionEvents.before_flush`
+
+ :meth:`~.SessionEvents.after_flush_postexec`
+
+ :ref:`session_persistence_events`
+
+ """
+
+ def after_flush_postexec(
+ self, session: Session, flush_context: UOWTransaction
+ ) -> None:
+ """Execute after flush has completed, and after the post-exec
+ state occurs.
+
+ This will be when the 'new', 'dirty', and 'deleted' lists are in
+ their final state. An actual commit() may or may not have
+ occurred, depending on whether or not the flush started its own
+ transaction or participated in a larger transaction.
+
+ :param session: The target :class:`.Session`.
+ :param flush_context: Internal :class:`.UOWTransaction` object
+ which handles the details of the flush.
+
+
+ .. seealso::
+
+ :meth:`~.SessionEvents.before_flush`
+
+ :meth:`~.SessionEvents.after_flush`
+
+ :ref:`session_persistence_events`
+
+ """
+
+ def after_begin(
+ self,
+ session: Session,
+ transaction: SessionTransaction,
+ connection: Connection,
+ ) -> None:
+ """Execute after a transaction is begun on a connection.
+
+ .. note:: This event is called within the process of the
+ :class:`_orm.Session` modifying its own internal state.
+ To invoke SQL operations within this hook, use the
+ :class:`_engine.Connection` provided to the event;
+ do not run SQL operations using the :class:`_orm.Session`
+ directly.
+
+ :param session: The target :class:`.Session`.
+ :param transaction: The :class:`.SessionTransaction`.
+ :param connection: The :class:`_engine.Connection` object
+ which will be used for SQL statements.
+
+ .. seealso::
+
+ :meth:`~.SessionEvents.before_commit`
+
+ :meth:`~.SessionEvents.after_commit`
+
+ :meth:`~.SessionEvents.after_transaction_create`
+
+ :meth:`~.SessionEvents.after_transaction_end`
+
+ """
+
+ @_lifecycle_event
+ def before_attach(self, session: Session, instance: _O) -> None:
+ """Execute before an instance is attached to a session.
+
+ This is called before an add, delete or merge causes
+ the object to be part of the session.
+
+ .. seealso::
+
+ :meth:`~.SessionEvents.after_attach`
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def after_attach(self, session: Session, instance: _O) -> None:
+ """Execute after an instance is attached to a session.
+
+ This is called after an add, delete or merge.
+
+ .. note::
+
+ As of 0.8, this event fires off *after* the item
+ has been fully associated with the session, which is
+ different than previous releases. For event
+ handlers that require the object not yet
+ be part of session state (such as handlers which
+ may autoflush while the target object is not
+ yet complete) consider the
+ new :meth:`.before_attach` event.
+
+ .. seealso::
+
+ :meth:`~.SessionEvents.before_attach`
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @event._legacy_signature(
+ "0.9",
+ ["session", "query", "query_context", "result"],
+ lambda update_context: (
+ update_context.session,
+ update_context.query,
+ None,
+ update_context.result,
+ ),
+ )
+ def after_bulk_update(self, update_context: _O) -> None:
+ """Event for after the legacy :meth:`_orm.Query.update` method
+ has been called.
+
+ .. legacy:: The :meth:`_orm.SessionEvents.after_bulk_update` method
+ is a legacy event hook as of SQLAlchemy 2.0. The event
+ **does not participate** in :term:`2.0 style` invocations
+ using :func:`_dml.update` documented at
+ :ref:`orm_queryguide_update_delete_where`. For 2.0 style use,
+ the :meth:`_orm.SessionEvents.do_orm_execute` hook will intercept
+ these calls.
+
+ :param update_context: an "update context" object which contains
+ details about the update, including these attributes:
+
+ * ``session`` - the :class:`.Session` involved
+ * ``query`` -the :class:`_query.Query`
+ object that this update operation
+ was called upon.
+ * ``values`` The "values" dictionary that was passed to
+ :meth:`_query.Query.update`.
+ * ``result`` the :class:`_engine.CursorResult`
+ returned as a result of the
+ bulk UPDATE operation.
+
+ .. versionchanged:: 1.4 the update_context no longer has a
+ ``QueryContext`` object associated with it.
+
+ .. seealso::
+
+ :meth:`.QueryEvents.before_compile_update`
+
+ :meth:`.SessionEvents.after_bulk_delete`
+
+ """
+
+ @event._legacy_signature(
+ "0.9",
+ ["session", "query", "query_context", "result"],
+ lambda delete_context: (
+ delete_context.session,
+ delete_context.query,
+ None,
+ delete_context.result,
+ ),
+ )
+ def after_bulk_delete(self, delete_context: _O) -> None:
+ """Event for after the legacy :meth:`_orm.Query.delete` method
+ has been called.
+
+ .. legacy:: The :meth:`_orm.SessionEvents.after_bulk_delete` method
+ is a legacy event hook as of SQLAlchemy 2.0. The event
+ **does not participate** in :term:`2.0 style` invocations
+ using :func:`_dml.delete` documented at
+ :ref:`orm_queryguide_update_delete_where`. For 2.0 style use,
+ the :meth:`_orm.SessionEvents.do_orm_execute` hook will intercept
+ these calls.
+
+ :param delete_context: a "delete context" object which contains
+ details about the update, including these attributes:
+
+ * ``session`` - the :class:`.Session` involved
+ * ``query`` -the :class:`_query.Query`
+ object that this update operation
+ was called upon.
+ * ``result`` the :class:`_engine.CursorResult`
+ returned as a result of the
+ bulk DELETE operation.
+
+ .. versionchanged:: 1.4 the update_context no longer has a
+ ``QueryContext`` object associated with it.
+
+ .. seealso::
+
+ :meth:`.QueryEvents.before_compile_delete`
+
+ :meth:`.SessionEvents.after_bulk_update`
+
+ """
+
+ @_lifecycle_event
+ def transient_to_pending(self, session: Session, instance: _O) -> None:
+ """Intercept the "transient to pending" transition for a specific
+ object.
+
+ This event is a specialization of the
+ :meth:`.SessionEvents.after_attach` event which is only invoked
+ for this specific transition. It is invoked typically during the
+ :meth:`.Session.add` call.
+
+ :param session: target :class:`.Session`
+
+ :param instance: the ORM-mapped instance being operated upon.
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def pending_to_transient(self, session: Session, instance: _O) -> None:
+ """Intercept the "pending to transient" transition for a specific
+ object.
+
+ This less common transition occurs when an pending object that has
+ not been flushed is evicted from the session; this can occur
+ when the :meth:`.Session.rollback` method rolls back the transaction,
+ or when the :meth:`.Session.expunge` method is used.
+
+ :param session: target :class:`.Session`
+
+ :param instance: the ORM-mapped instance being operated upon.
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def persistent_to_transient(self, session: Session, instance: _O) -> None:
+ """Intercept the "persistent to transient" transition for a specific
+ object.
+
+ This less common transition occurs when an pending object that has
+ has been flushed is evicted from the session; this can occur
+ when the :meth:`.Session.rollback` method rolls back the transaction.
+
+ :param session: target :class:`.Session`
+
+ :param instance: the ORM-mapped instance being operated upon.
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def pending_to_persistent(self, session: Session, instance: _O) -> None:
+ """Intercept the "pending to persistent"" transition for a specific
+ object.
+
+ This event is invoked within the flush process, and is
+ similar to scanning the :attr:`.Session.new` collection within
+ the :meth:`.SessionEvents.after_flush` event. However, in this
+ case the object has already been moved to the persistent state
+ when the event is called.
+
+ :param session: target :class:`.Session`
+
+ :param instance: the ORM-mapped instance being operated upon.
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def detached_to_persistent(self, session: Session, instance: _O) -> None:
+ """Intercept the "detached to persistent" transition for a specific
+ object.
+
+ This event is a specialization of the
+ :meth:`.SessionEvents.after_attach` event which is only invoked
+ for this specific transition. It is invoked typically during the
+ :meth:`.Session.add` call, as well as during the
+ :meth:`.Session.delete` call if the object was not previously
+ associated with the
+ :class:`.Session` (note that an object marked as "deleted" remains
+ in the "persistent" state until the flush proceeds).
+
+ .. note::
+
+ If the object becomes persistent as part of a call to
+ :meth:`.Session.delete`, the object is **not** yet marked as
+ deleted when this event is called. To detect deleted objects,
+ check the ``deleted`` flag sent to the
+ :meth:`.SessionEvents.persistent_to_detached` to event after the
+ flush proceeds, or check the :attr:`.Session.deleted` collection
+ within the :meth:`.SessionEvents.before_flush` event if deleted
+ objects need to be intercepted before the flush.
+
+ :param session: target :class:`.Session`
+
+ :param instance: the ORM-mapped instance being operated upon.
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def loaded_as_persistent(self, session: Session, instance: _O) -> None:
+ """Intercept the "loaded as persistent" transition for a specific
+ object.
+
+ This event is invoked within the ORM loading process, and is invoked
+ very similarly to the :meth:`.InstanceEvents.load` event. However,
+ the event here is linkable to a :class:`.Session` class or instance,
+ rather than to a mapper or class hierarchy, and integrates
+ with the other session lifecycle events smoothly. The object
+ is guaranteed to be present in the session's identity map when
+ this event is called.
+
+ .. note:: This event is invoked within the loader process before
+ eager loaders may have been completed, and the object's state may
+ not be complete. Additionally, invoking row-level refresh
+ operations on the object will place the object into a new loader
+ context, interfering with the existing load context. See the note
+ on :meth:`.InstanceEvents.load` for background on making use of the
+ :paramref:`.SessionEvents.restore_load_context` parameter, which
+ works in the same manner as that of
+ :paramref:`.InstanceEvents.restore_load_context`, in order to
+ resolve this scenario.
+
+ :param session: target :class:`.Session`
+
+ :param instance: the ORM-mapped instance being operated upon.
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def persistent_to_deleted(self, session: Session, instance: _O) -> None:
+ """Intercept the "persistent to deleted" transition for a specific
+ object.
+
+ This event is invoked when a persistent object's identity
+ is deleted from the database within a flush, however the object
+ still remains associated with the :class:`.Session` until the
+ transaction completes.
+
+ If the transaction is rolled back, the object moves again
+ to the persistent state, and the
+ :meth:`.SessionEvents.deleted_to_persistent` event is called.
+ If the transaction is committed, the object becomes detached,
+ which will emit the :meth:`.SessionEvents.deleted_to_detached`
+ event.
+
+ Note that while the :meth:`.Session.delete` method is the primary
+ public interface to mark an object as deleted, many objects
+ get deleted due to cascade rules, which are not always determined
+ until flush time. Therefore, there's no way to catch
+ every object that will be deleted until the flush has proceeded.
+ the :meth:`.SessionEvents.persistent_to_deleted` event is therefore
+ invoked at the end of a flush.
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def deleted_to_persistent(self, session: Session, instance: _O) -> None:
+ """Intercept the "deleted to persistent" transition for a specific
+ object.
+
+ This transition occurs only when an object that's been deleted
+ successfully in a flush is restored due to a call to
+ :meth:`.Session.rollback`. The event is not called under
+ any other circumstances.
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def deleted_to_detached(self, session: Session, instance: _O) -> None:
+ """Intercept the "deleted to detached" transition for a specific
+ object.
+
+ This event is invoked when a deleted object is evicted
+ from the session. The typical case when this occurs is when
+ the transaction for a :class:`.Session` in which the object
+ was deleted is committed; the object moves from the deleted
+ state to the detached state.
+
+ It is also invoked for objects that were deleted in a flush
+ when the :meth:`.Session.expunge_all` or :meth:`.Session.close`
+ events are called, as well as if the object is individually
+ expunged from its deleted state via :meth:`.Session.expunge`.
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+ @_lifecycle_event
+ def persistent_to_detached(self, session: Session, instance: _O) -> None:
+ """Intercept the "persistent to detached" transition for a specific
+ object.
+
+ This event is invoked when a persistent object is evicted
+ from the session. There are many conditions that cause this
+ to happen, including:
+
+ * using a method such as :meth:`.Session.expunge`
+ or :meth:`.Session.close`
+
+ * Calling the :meth:`.Session.rollback` method, when the object
+ was part of an INSERT statement for that session's transaction
+
+
+ :param session: target :class:`.Session`
+
+ :param instance: the ORM-mapped instance being operated upon.
+
+ :param deleted: boolean. If True, indicates this object moved
+ to the detached state because it was marked as deleted and flushed.
+
+
+ .. seealso::
+
+ :ref:`session_lifecycle_events`
+
+ """
+
+
+class AttributeEvents(event.Events[QueryableAttribute[Any]]):
+ r"""Define events for object attributes.
+
+ These are typically defined on the class-bound descriptor for the
+ target class.
+
+ For example, to register a listener that will receive the
+ :meth:`_orm.AttributeEvents.append` event::
+
+ from sqlalchemy import event
+
+ @event.listens_for(MyClass.collection, 'append', propagate=True)
+ def my_append_listener(target, value, initiator):
+ print("received append event for target: %s" % target)
+
+
+ Listeners have the option to return a possibly modified version of the
+ value, when the :paramref:`.AttributeEvents.retval` flag is passed to
+ :func:`.event.listen` or :func:`.event.listens_for`, such as below,
+ illustrated using the :meth:`_orm.AttributeEvents.set` event::
+
+ def validate_phone(target, value, oldvalue, initiator):
+ "Strip non-numeric characters from a phone number"
+
+ return re.sub(r'\D', '', value)
+
+ # setup listener on UserContact.phone attribute, instructing
+ # it to use the return value
+ listen(UserContact.phone, 'set', validate_phone, retval=True)
+
+ A validation function like the above can also raise an exception
+ such as :exc:`ValueError` to halt the operation.
+
+ The :paramref:`.AttributeEvents.propagate` flag is also important when
+ applying listeners to mapped classes that also have mapped subclasses,
+ as when using mapper inheritance patterns::
+
+
+ @event.listens_for(MySuperClass.attr, 'set', propagate=True)
+ def receive_set(target, value, initiator):
+ print("value set: %s" % target)
+
+ The full list of modifiers available to the :func:`.event.listen`
+ and :func:`.event.listens_for` functions are below.
+
+ :param active_history=False: When True, indicates that the
+ "set" event would like to receive the "old" value being
+ replaced unconditionally, even if this requires firing off
+ database loads. Note that ``active_history`` can also be
+ set directly via :func:`.column_property` and
+ :func:`_orm.relationship`.
+
+ :param propagate=False: When True, the listener function will
+ be established not just for the class attribute given, but
+ for attributes of the same name on all current subclasses
+ of that class, as well as all future subclasses of that
+ class, using an additional listener that listens for
+ instrumentation events.
+ :param raw=False: When True, the "target" argument to the
+ event will be the :class:`.InstanceState` management
+ object, rather than the mapped instance itself.
+ :param retval=False: when True, the user-defined event
+ listening must return the "value" argument from the
+ function. This gives the listening function the opportunity
+ to change the value that is ultimately used for a "set"
+ or "append" event.
+
+ """
+
+ _target_class_doc = "SomeClass.some_attribute"
+ _dispatch_target = QueryableAttribute
+
+ @staticmethod
+ def _set_dispatch(
+ cls: Type[_HasEventsDispatch[Any]], dispatch_cls: Type[_Dispatch[Any]]
+ ) -> _Dispatch[Any]:
+ dispatch = event.Events._set_dispatch(cls, dispatch_cls)
+ dispatch_cls._active_history = False
+ return dispatch
+
+ @classmethod
+ def _accept_with(
+ cls,
+ target: Union[QueryableAttribute[Any], Type[QueryableAttribute[Any]]],
+ identifier: str,
+ ) -> Union[QueryableAttribute[Any], Type[QueryableAttribute[Any]]]:
+ # TODO: coverage
+ if isinstance(target, interfaces.MapperProperty):
+ return getattr(target.parent.class_, target.key)
+ else:
+ return target
+
+ @classmethod
+ def _listen( # type: ignore [override]
+ cls,
+ event_key: _EventKey[QueryableAttribute[Any]],
+ active_history: bool = False,
+ raw: bool = False,
+ retval: bool = False,
+ propagate: bool = False,
+ include_key: bool = False,
+ ) -> None:
+ target, fn = event_key.dispatch_target, event_key._listen_fn
+
+ if active_history:
+ target.dispatch._active_history = True
+
+ if not raw or not retval or not include_key:
+
+ def wrap(target: InstanceState[_O], *arg: Any, **kw: Any) -> Any:
+ if not raw:
+ target = target.obj() # type: ignore [assignment]
+ if not retval:
+ if arg:
+ value = arg[0]
+ else:
+ value = None
+ if include_key:
+ fn(target, *arg, **kw)
+ else:
+ fn(target, *arg)
+ return value
+ else:
+ if include_key:
+ return fn(target, *arg, **kw)
+ else:
+ return fn(target, *arg)
+
+ event_key = event_key.with_wrapper(wrap)
+
+ event_key.base_listen(propagate=propagate)
+
+ if propagate:
+ manager = instrumentation.manager_of_class(target.class_)
+
+ for mgr in manager.subclass_managers(True): # type: ignore [no-untyped-call] # noqa: E501
+ event_key.with_dispatch_target(mgr[target.key]).base_listen(
+ propagate=True
+ )
+ if active_history:
+ mgr[target.key].dispatch._active_history = True
+
+ def append(
+ self,
+ target: _O,
+ value: _T,
+ initiator: Event,
+ *,
+ key: EventConstants = NO_KEY,
+ ) -> Optional[_T]:
+ """Receive a collection append event.
+
+ The append event is invoked for each element as it is appended
+ to the collection. This occurs for single-item appends as well
+ as for a "bulk replace" operation.
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param value: the value being appended. If this listener
+ is registered with ``retval=True``, the listener
+ function must return this value, or a new value which
+ replaces it.
+ :param initiator: An instance of :class:`.attributes.Event`
+ representing the initiation of the event. May be modified
+ from its original value by backref handlers in order to control
+ chained event propagation, as well as be inspected for information
+ about the source of the event.
+ :param key: When the event is established using the
+ :paramref:`.AttributeEvents.include_key` parameter set to
+ True, this will be the key used in the operation, such as
+ ``collection[some_key_or_index] = value``.
+ The parameter is not passed
+ to the event at all if the the
+ :paramref:`.AttributeEvents.include_key`
+ was not used to set up the event; this is to allow backwards
+ compatibility with existing event handlers that don't include the
+ ``key`` parameter.
+
+ .. versionadded:: 2.0
+
+ :return: if the event was registered with ``retval=True``,
+ the given value, or a new effective value, should be returned.
+
+ .. seealso::
+
+ :class:`.AttributeEvents` - background on listener options such
+ as propagation to subclasses.
+
+ :meth:`.AttributeEvents.bulk_replace`
+
+ """
+
+ def append_wo_mutation(
+ self,
+ target: _O,
+ value: _T,
+ initiator: Event,
+ *,
+ key: EventConstants = NO_KEY,
+ ) -> None:
+ """Receive a collection append event where the collection was not
+ actually mutated.
+
+ This event differs from :meth:`_orm.AttributeEvents.append` in that
+ it is fired off for de-duplicating collections such as sets and
+ dictionaries, when the object already exists in the target collection.
+ The event does not have a return value and the identity of the
+ given object cannot be changed.
+
+ The event is used for cascading objects into a :class:`_orm.Session`
+ when the collection has already been mutated via a backref event.
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param value: the value that would be appended if the object did not
+ already exist in the collection.
+ :param initiator: An instance of :class:`.attributes.Event`
+ representing the initiation of the event. May be modified
+ from its original value by backref handlers in order to control
+ chained event propagation, as well as be inspected for information
+ about the source of the event.
+ :param key: When the event is established using the
+ :paramref:`.AttributeEvents.include_key` parameter set to
+ True, this will be the key used in the operation, such as
+ ``collection[some_key_or_index] = value``.
+ The parameter is not passed
+ to the event at all if the the
+ :paramref:`.AttributeEvents.include_key`
+ was not used to set up the event; this is to allow backwards
+ compatibility with existing event handlers that don't include the
+ ``key`` parameter.
+
+ .. versionadded:: 2.0
+
+ :return: No return value is defined for this event.
+
+ .. versionadded:: 1.4.15
+
+ """
+
+ def bulk_replace(
+ self,
+ target: _O,
+ values: Iterable[_T],
+ initiator: Event,
+ *,
+ keys: Optional[Iterable[EventConstants]] = None,
+ ) -> None:
+ """Receive a collection 'bulk replace' event.
+
+ This event is invoked for a sequence of values as they are incoming
+ to a bulk collection set operation, which can be
+ modified in place before the values are treated as ORM objects.
+ This is an "early hook" that runs before the bulk replace routine
+ attempts to reconcile which objects are already present in the
+ collection and which are being removed by the net replace operation.
+
+ It is typical that this method be combined with use of the
+ :meth:`.AttributeEvents.append` event. When using both of these
+ events, note that a bulk replace operation will invoke
+ the :meth:`.AttributeEvents.append` event for all new items,
+ even after :meth:`.AttributeEvents.bulk_replace` has been invoked
+ for the collection as a whole. In order to determine if an
+ :meth:`.AttributeEvents.append` event is part of a bulk replace,
+ use the symbol :attr:`~.attributes.OP_BULK_REPLACE` to test the
+ incoming initiator::
+
+ from sqlalchemy.orm.attributes import OP_BULK_REPLACE
+
+ @event.listens_for(SomeObject.collection, "bulk_replace")
+ def process_collection(target, values, initiator):
+ values[:] = [_make_value(value) for value in values]
+
+ @event.listens_for(SomeObject.collection, "append", retval=True)
+ def process_collection(target, value, initiator):
+ # make sure bulk_replace didn't already do it
+ if initiator is None or initiator.op is not OP_BULK_REPLACE:
+ return _make_value(value)
+ else:
+ return value
+
+ .. versionadded:: 1.2
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param value: a sequence (e.g. a list) of the values being set. The
+ handler can modify this list in place.
+ :param initiator: An instance of :class:`.attributes.Event`
+ representing the initiation of the event.
+ :param keys: When the event is established using the
+ :paramref:`.AttributeEvents.include_key` parameter set to
+ True, this will be the sequence of keys used in the operation,
+ typically only for a dictionary update. The parameter is not passed
+ to the event at all if the the
+ :paramref:`.AttributeEvents.include_key`
+ was not used to set up the event; this is to allow backwards
+ compatibility with existing event handlers that don't include the
+ ``key`` parameter.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :class:`.AttributeEvents` - background on listener options such
+ as propagation to subclasses.
+
+
+ """
+
+ def remove(
+ self,
+ target: _O,
+ value: _T,
+ initiator: Event,
+ *,
+ key: EventConstants = NO_KEY,
+ ) -> None:
+ """Receive a collection remove event.
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param value: the value being removed.
+ :param initiator: An instance of :class:`.attributes.Event`
+ representing the initiation of the event. May be modified
+ from its original value by backref handlers in order to control
+ chained event propagation.
+
+ :param key: When the event is established using the
+ :paramref:`.AttributeEvents.include_key` parameter set to
+ True, this will be the key used in the operation, such as
+ ``del collection[some_key_or_index]``. The parameter is not passed
+ to the event at all if the the
+ :paramref:`.AttributeEvents.include_key`
+ was not used to set up the event; this is to allow backwards
+ compatibility with existing event handlers that don't include the
+ ``key`` parameter.
+
+ .. versionadded:: 2.0
+
+ :return: No return value is defined for this event.
+
+
+ .. seealso::
+
+ :class:`.AttributeEvents` - background on listener options such
+ as propagation to subclasses.
+
+ """
+
+ def set(
+ self, target: _O, value: _T, oldvalue: _T, initiator: Event
+ ) -> None:
+ """Receive a scalar set event.
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param value: the value being set. If this listener
+ is registered with ``retval=True``, the listener
+ function must return this value, or a new value which
+ replaces it.
+ :param oldvalue: the previous value being replaced. This
+ may also be the symbol ``NEVER_SET`` or ``NO_VALUE``.
+ If the listener is registered with ``active_history=True``,
+ the previous value of the attribute will be loaded from
+ the database if the existing value is currently unloaded
+ or expired.
+ :param initiator: An instance of :class:`.attributes.Event`
+ representing the initiation of the event. May be modified
+ from its original value by backref handlers in order to control
+ chained event propagation.
+
+ :return: if the event was registered with ``retval=True``,
+ the given value, or a new effective value, should be returned.
+
+ .. seealso::
+
+ :class:`.AttributeEvents` - background on listener options such
+ as propagation to subclasses.
+
+ """
+
+ def init_scalar(
+ self, target: _O, value: _T, dict_: Dict[Any, Any]
+ ) -> None:
+ r"""Receive a scalar "init" event.
+
+ This event is invoked when an uninitialized, unpersisted scalar
+ attribute is accessed, e.g. read::
+
+
+ x = my_object.some_attribute
+
+ The ORM's default behavior when this occurs for an un-initialized
+ attribute is to return the value ``None``; note this differs from
+ Python's usual behavior of raising ``AttributeError``. The
+ event here can be used to customize what value is actually returned,
+ with the assumption that the event listener would be mirroring
+ a default generator that is configured on the Core
+ :class:`_schema.Column`
+ object as well.
+
+ Since a default generator on a :class:`_schema.Column`
+ might also produce
+ a changing value such as a timestamp, the
+ :meth:`.AttributeEvents.init_scalar`
+ event handler can also be used to **set** the newly returned value, so
+ that a Core-level default generation function effectively fires off
+ only once, but at the moment the attribute is accessed on the
+ non-persisted object. Normally, no change to the object's state
+ is made when an uninitialized attribute is accessed (much older
+ SQLAlchemy versions did in fact change the object's state).
+
+ If a default generator on a column returned a particular constant,
+ a handler might be used as follows::
+
+ SOME_CONSTANT = 3.1415926
+
+ class MyClass(Base):
+ # ...
+
+ some_attribute = Column(Numeric, default=SOME_CONSTANT)
+
+ @event.listens_for(
+ MyClass.some_attribute, "init_scalar",
+ retval=True, propagate=True)
+ def _init_some_attribute(target, dict_, value):
+ dict_['some_attribute'] = SOME_CONSTANT
+ return SOME_CONSTANT
+
+ Above, we initialize the attribute ``MyClass.some_attribute`` to the
+ value of ``SOME_CONSTANT``. The above code includes the following
+ features:
+
+ * By setting the value ``SOME_CONSTANT`` in the given ``dict_``,
+ we indicate that this value is to be persisted to the database.
+ This supersedes the use of ``SOME_CONSTANT`` in the default generator
+ for the :class:`_schema.Column`. The ``active_column_defaults.py``
+ example given at :ref:`examples_instrumentation` illustrates using
+ the same approach for a changing default, e.g. a timestamp
+ generator. In this particular example, it is not strictly
+ necessary to do this since ``SOME_CONSTANT`` would be part of the
+ INSERT statement in either case.
+
+ * By establishing the ``retval=True`` flag, the value we return
+ from the function will be returned by the attribute getter.
+ Without this flag, the event is assumed to be a passive observer
+ and the return value of our function is ignored.
+
+ * The ``propagate=True`` flag is significant if the mapped class
+ includes inheriting subclasses, which would also make use of this
+ event listener. Without this flag, an inheriting subclass will
+ not use our event handler.
+
+ In the above example, the attribute set event
+ :meth:`.AttributeEvents.set` as well as the related validation feature
+ provided by :obj:`_orm.validates` is **not** invoked when we apply our
+ value to the given ``dict_``. To have these events to invoke in
+ response to our newly generated value, apply the value to the given
+ object as a normal attribute set operation::
+
+ SOME_CONSTANT = 3.1415926
+
+ @event.listens_for(
+ MyClass.some_attribute, "init_scalar",
+ retval=True, propagate=True)
+ def _init_some_attribute(target, dict_, value):
+ # will also fire off attribute set events
+ target.some_attribute = SOME_CONSTANT
+ return SOME_CONSTANT
+
+ When multiple listeners are set up, the generation of the value
+ is "chained" from one listener to the next by passing the value
+ returned by the previous listener that specifies ``retval=True``
+ as the ``value`` argument of the next listener.
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param value: the value that is to be returned before this event
+ listener were invoked. This value begins as the value ``None``,
+ however will be the return value of the previous event handler
+ function if multiple listeners are present.
+ :param dict\_: the attribute dictionary of this mapped object.
+ This is normally the ``__dict__`` of the object, but in all cases
+ represents the destination that the attribute system uses to get
+ at the actual value of this attribute. Placing the value in this
+ dictionary has the effect that the value will be used in the
+ INSERT statement generated by the unit of work.
+
+
+ .. seealso::
+
+ :meth:`.AttributeEvents.init_collection` - collection version
+ of this event
+
+ :class:`.AttributeEvents` - background on listener options such
+ as propagation to subclasses.
+
+ :ref:`examples_instrumentation` - see the
+ ``active_column_defaults.py`` example.
+
+ """
+
+ def init_collection(
+ self,
+ target: _O,
+ collection: Type[Collection[Any]],
+ collection_adapter: CollectionAdapter,
+ ) -> None:
+ """Receive a 'collection init' event.
+
+ This event is triggered for a collection-based attribute, when
+ the initial "empty collection" is first generated for a blank
+ attribute, as well as for when the collection is replaced with
+ a new one, such as via a set event.
+
+ E.g., given that ``User.addresses`` is a relationship-based
+ collection, the event is triggered here::
+
+ u1 = User()
+ u1.addresses.append(a1) # <- new collection
+
+ and also during replace operations::
+
+ u1.addresses = [a2, a3] # <- new collection
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param collection: the new collection. This will always be generated
+ from what was specified as
+ :paramref:`_orm.relationship.collection_class`, and will always
+ be empty.
+ :param collection_adapter: the :class:`.CollectionAdapter` that will
+ mediate internal access to the collection.
+
+ .. seealso::
+
+ :class:`.AttributeEvents` - background on listener options such
+ as propagation to subclasses.
+
+ :meth:`.AttributeEvents.init_scalar` - "scalar" version of this
+ event.
+
+ """
+
+ def dispose_collection(
+ self,
+ target: _O,
+ collection: Collection[Any],
+ collection_adapter: CollectionAdapter,
+ ) -> None:
+ """Receive a 'collection dispose' event.
+
+ This event is triggered for a collection-based attribute when
+ a collection is replaced, that is::
+
+ u1.addresses.append(a1)
+
+ u1.addresses = [a2, a3] # <- old collection is disposed
+
+ The old collection received will contain its previous contents.
+
+ .. versionchanged:: 1.2 The collection passed to
+ :meth:`.AttributeEvents.dispose_collection` will now have its
+ contents before the dispose intact; previously, the collection
+ would be empty.
+
+ .. seealso::
+
+ :class:`.AttributeEvents` - background on listener options such
+ as propagation to subclasses.
+
+ """
+
+ def modified(self, target: _O, initiator: Event) -> None:
+ """Receive a 'modified' event.
+
+ This event is triggered when the :func:`.attributes.flag_modified`
+ function is used to trigger a modify event on an attribute without
+ any specific value being set.
+
+ .. versionadded:: 1.2
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+
+ :param initiator: An instance of :class:`.attributes.Event`
+ representing the initiation of the event.
+
+ .. seealso::
+
+ :class:`.AttributeEvents` - background on listener options such
+ as propagation to subclasses.
+
+ """
+
+
+class QueryEvents(event.Events[Query[Any]]):
+ """Represent events within the construction of a :class:`_query.Query`
+ object.
+
+ .. legacy:: The :class:`_orm.QueryEvents` event methods are legacy
+ as of SQLAlchemy 2.0, and only apply to direct use of the
+ :class:`_orm.Query` object. They are not used for :term:`2.0 style`
+ statements. For events to intercept and modify 2.0 style ORM use,
+ use the :meth:`_orm.SessionEvents.do_orm_execute` hook.
+
+
+ The :class:`_orm.QueryEvents` hooks are now superseded by the
+ :meth:`_orm.SessionEvents.do_orm_execute` event hook.
+
+ """
+
+ _target_class_doc = "SomeQuery"
+ _dispatch_target = Query
+
+ def before_compile(self, query: Query[Any]) -> None:
+ """Receive the :class:`_query.Query`
+ object before it is composed into a
+ core :class:`_expression.Select` object.
+
+ .. deprecated:: 1.4 The :meth:`_orm.QueryEvents.before_compile` event
+ is superseded by the much more capable
+ :meth:`_orm.SessionEvents.do_orm_execute` hook. In version 1.4,
+ the :meth:`_orm.QueryEvents.before_compile` event is **no longer
+ used** for ORM-level attribute loads, such as loads of deferred
+ or expired attributes as well as relationship loaders. See the
+ new examples in :ref:`examples_session_orm_events` which
+ illustrate new ways of intercepting and modifying ORM queries
+ for the most common purpose of adding arbitrary filter criteria.
+
+
+ This event is intended to allow changes to the query given::
+
+ @event.listens_for(Query, "before_compile", retval=True)
+ def no_deleted(query):
+ for desc in query.column_descriptions:
+ if desc['type'] is User:
+ entity = desc['entity']
+ query = query.filter(entity.deleted == False)
+ return query
+
+ The event should normally be listened with the ``retval=True``
+ parameter set, so that the modified query may be returned.
+
+ The :meth:`.QueryEvents.before_compile` event by default
+ will disallow "baked" queries from caching a query, if the event
+ hook returns a new :class:`_query.Query` object.
+ This affects both direct
+ use of the baked query extension as well as its operation within
+ lazy loaders and eager loaders for relationships. In order to
+ re-establish the query being cached, apply the event adding the
+ ``bake_ok`` flag::
+
+ @event.listens_for(
+ Query, "before_compile", retval=True, bake_ok=True)
+ def my_event(query):
+ for desc in query.column_descriptions:
+ if desc['type'] is User:
+ entity = desc['entity']
+ query = query.filter(entity.deleted == False)
+ return query
+
+ When ``bake_ok`` is set to True, the event hook will only be invoked
+ once, and not called for subsequent invocations of a particular query
+ that is being cached.
+
+ .. versionadded:: 1.3.11 - added the "bake_ok" flag to the
+ :meth:`.QueryEvents.before_compile` event and disallowed caching via
+ the "baked" extension from occurring for event handlers that
+ return a new :class:`_query.Query` object if this flag is not set.
+
+ .. seealso::
+
+ :meth:`.QueryEvents.before_compile_update`
+
+ :meth:`.QueryEvents.before_compile_delete`
+
+ :ref:`baked_with_before_compile`
+
+ """
+
+ def before_compile_update(
+ self, query: Query[Any], update_context: BulkUpdate
+ ) -> None:
+ """Allow modifications to the :class:`_query.Query` object within
+ :meth:`_query.Query.update`.
+
+ .. deprecated:: 1.4 The :meth:`_orm.QueryEvents.before_compile_update`
+ event is superseded by the much more capable
+ :meth:`_orm.SessionEvents.do_orm_execute` hook.
+
+ Like the :meth:`.QueryEvents.before_compile` event, if the event
+ is to be used to alter the :class:`_query.Query` object, it should
+ be configured with ``retval=True``, and the modified
+ :class:`_query.Query` object returned, as in ::
+
+ @event.listens_for(Query, "before_compile_update", retval=True)
+ def no_deleted(query, update_context):
+ for desc in query.column_descriptions:
+ if desc['type'] is User:
+ entity = desc['entity']
+ query = query.filter(entity.deleted == False)
+
+ update_context.values['timestamp'] = datetime.utcnow()
+ return query
+
+ The ``.values`` dictionary of the "update context" object can also
+ be modified in place as illustrated above.
+
+ :param query: a :class:`_query.Query` instance; this is also
+ the ``.query`` attribute of the given "update context"
+ object.
+
+ :param update_context: an "update context" object which is
+ the same kind of object as described in
+ :paramref:`.QueryEvents.after_bulk_update.update_context`.
+ The object has a ``.values`` attribute in an UPDATE context which is
+ the dictionary of parameters passed to :meth:`_query.Query.update`.
+ This
+ dictionary can be modified to alter the VALUES clause of the
+ resulting UPDATE statement.
+
+ .. versionadded:: 1.2.17
+
+ .. seealso::
+
+ :meth:`.QueryEvents.before_compile`
+
+ :meth:`.QueryEvents.before_compile_delete`
+
+
+ """
+
+ def before_compile_delete(
+ self, query: Query[Any], delete_context: BulkDelete
+ ) -> None:
+ """Allow modifications to the :class:`_query.Query` object within
+ :meth:`_query.Query.delete`.
+
+ .. deprecated:: 1.4 The :meth:`_orm.QueryEvents.before_compile_delete`
+ event is superseded by the much more capable
+ :meth:`_orm.SessionEvents.do_orm_execute` hook.
+
+ Like the :meth:`.QueryEvents.before_compile` event, this event
+ should be configured with ``retval=True``, and the modified
+ :class:`_query.Query` object returned, as in ::
+
+ @event.listens_for(Query, "before_compile_delete", retval=True)
+ def no_deleted(query, delete_context):
+ for desc in query.column_descriptions:
+ if desc['type'] is User:
+ entity = desc['entity']
+ query = query.filter(entity.deleted == False)
+ return query
+
+ :param query: a :class:`_query.Query` instance; this is also
+ the ``.query`` attribute of the given "delete context"
+ object.
+
+ :param delete_context: a "delete context" object which is
+ the same kind of object as described in
+ :paramref:`.QueryEvents.after_bulk_delete.delete_context`.
+
+ .. versionadded:: 1.2.17
+
+ .. seealso::
+
+ :meth:`.QueryEvents.before_compile`
+
+ :meth:`.QueryEvents.before_compile_update`
+
+
+ """
+
+ @classmethod
+ def _listen(
+ cls,
+ event_key: _EventKey[_ET],
+ retval: bool = False,
+ bake_ok: bool = False,
+ **kw: Any,
+ ) -> None:
+ fn = event_key._listen_fn
+
+ if not retval:
+
+ def wrap(*arg: Any, **kw: Any) -> Any:
+ if not retval:
+ query = arg[0]
+ fn(*arg, **kw)
+ return query
+ else:
+ return fn(*arg, **kw)
+
+ event_key = event_key.with_wrapper(wrap)
+ else:
+ # don't assume we can apply an attribute to the callable
+ def wrap(*arg: Any, **kw: Any) -> Any:
+ return fn(*arg, **kw)
+
+ event_key = event_key.with_wrapper(wrap)
+
+ wrap._bake_ok = bake_ok # type: ignore [attr-defined]
+
+ event_key.base_listen(**kw)
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/exc.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/exc.py
new file mode 100644
index 0000000..39dd540
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/exc.py
@@ -0,0 +1,228 @@
+# orm/exc.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""SQLAlchemy ORM exceptions."""
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Optional
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+
+from .util import _mapper_property_as_plain_name
+from .. import exc as sa_exc
+from .. import util
+from ..exc import MultipleResultsFound # noqa
+from ..exc import NoResultFound # noqa
+
+if TYPE_CHECKING:
+ from .interfaces import LoaderStrategy
+ from .interfaces import MapperProperty
+ from .state import InstanceState
+
+_T = TypeVar("_T", bound=Any)
+
+NO_STATE = (AttributeError, KeyError)
+"""Exception types that may be raised by instrumentation implementations."""
+
+
+class StaleDataError(sa_exc.SQLAlchemyError):
+ """An operation encountered database state that is unaccounted for.
+
+ Conditions which cause this to happen include:
+
+ * A flush may have attempted to update or delete rows
+ and an unexpected number of rows were matched during
+ the UPDATE or DELETE statement. Note that when
+ version_id_col is used, rows in UPDATE or DELETE statements
+ are also matched against the current known version
+ identifier.
+
+ * A mapped object with version_id_col was refreshed,
+ and the version number coming back from the database does
+ not match that of the object itself.
+
+ * A object is detached from its parent object, however
+ the object was previously attached to a different parent
+ identity which was garbage collected, and a decision
+ cannot be made if the new parent was really the most
+ recent "parent".
+
+ """
+
+
+ConcurrentModificationError = StaleDataError
+
+
+class FlushError(sa_exc.SQLAlchemyError):
+ """A invalid condition was detected during flush()."""
+
+
+class UnmappedError(sa_exc.InvalidRequestError):
+ """Base for exceptions that involve expected mappings not present."""
+
+
+class ObjectDereferencedError(sa_exc.SQLAlchemyError):
+ """An operation cannot complete due to an object being garbage
+ collected.
+
+ """
+
+
+class DetachedInstanceError(sa_exc.SQLAlchemyError):
+ """An attempt to access unloaded attributes on a
+ mapped instance that is detached."""
+
+ code = "bhk3"
+
+
+class UnmappedInstanceError(UnmappedError):
+ """An mapping operation was requested for an unknown instance."""
+
+ @util.preload_module("sqlalchemy.orm.base")
+ def __init__(self, obj: object, msg: Optional[str] = None):
+ base = util.preloaded.orm_base
+
+ if not msg:
+ try:
+ base.class_mapper(type(obj))
+ name = _safe_cls_name(type(obj))
+ msg = (
+ "Class %r is mapped, but this instance lacks "
+ "instrumentation. This occurs when the instance "
+ "is created before sqlalchemy.orm.mapper(%s) "
+ "was called." % (name, name)
+ )
+ except UnmappedClassError:
+ msg = f"Class '{_safe_cls_name(type(obj))}' is not mapped"
+ if isinstance(obj, type):
+ msg += (
+ "; was a class (%s) supplied where an instance was "
+ "required?" % _safe_cls_name(obj)
+ )
+ UnmappedError.__init__(self, msg)
+
+ def __reduce__(self) -> Any:
+ return self.__class__, (None, self.args[0])
+
+
+class UnmappedClassError(UnmappedError):
+ """An mapping operation was requested for an unknown class."""
+
+ def __init__(self, cls: Type[_T], msg: Optional[str] = None):
+ if not msg:
+ msg = _default_unmapped(cls)
+ UnmappedError.__init__(self, msg)
+
+ def __reduce__(self) -> Any:
+ return self.__class__, (None, self.args[0])
+
+
+class ObjectDeletedError(sa_exc.InvalidRequestError):
+ """A refresh operation failed to retrieve the database
+ row corresponding to an object's known primary key identity.
+
+ A refresh operation proceeds when an expired attribute is
+ accessed on an object, or when :meth:`_query.Query.get` is
+ used to retrieve an object which is, upon retrieval, detected
+ as expired. A SELECT is emitted for the target row
+ based on primary key; if no row is returned, this
+ exception is raised.
+
+ The true meaning of this exception is simply that
+ no row exists for the primary key identifier associated
+ with a persistent object. The row may have been
+ deleted, or in some cases the primary key updated
+ to a new value, outside of the ORM's management of the target
+ object.
+
+ """
+
+ @util.preload_module("sqlalchemy.orm.base")
+ def __init__(self, state: InstanceState[Any], msg: Optional[str] = None):
+ base = util.preloaded.orm_base
+
+ if not msg:
+ msg = (
+ "Instance '%s' has been deleted, or its "
+ "row is otherwise not present." % base.state_str(state)
+ )
+
+ sa_exc.InvalidRequestError.__init__(self, msg)
+
+ def __reduce__(self) -> Any:
+ return self.__class__, (None, self.args[0])
+
+
+class UnmappedColumnError(sa_exc.InvalidRequestError):
+ """Mapping operation was requested on an unknown column."""
+
+
+class LoaderStrategyException(sa_exc.InvalidRequestError):
+ """A loader strategy for an attribute does not exist."""
+
+ def __init__(
+ self,
+ applied_to_property_type: Type[Any],
+ requesting_property: MapperProperty[Any],
+ applies_to: Optional[Type[MapperProperty[Any]]],
+ actual_strategy_type: Optional[Type[LoaderStrategy]],
+ strategy_key: Tuple[Any, ...],
+ ):
+ if actual_strategy_type is None:
+ sa_exc.InvalidRequestError.__init__(
+ self,
+ "Can't find strategy %s for %s"
+ % (strategy_key, requesting_property),
+ )
+ else:
+ assert applies_to is not None
+ sa_exc.InvalidRequestError.__init__(
+ self,
+ 'Can\'t apply "%s" strategy to property "%s", '
+ 'which is a "%s"; this loader strategy is intended '
+ 'to be used with a "%s".'
+ % (
+ util.clsname_as_plain_name(actual_strategy_type),
+ requesting_property,
+ _mapper_property_as_plain_name(applied_to_property_type),
+ _mapper_property_as_plain_name(applies_to),
+ ),
+ )
+
+
+def _safe_cls_name(cls: Type[Any]) -> str:
+ cls_name: Optional[str]
+ try:
+ cls_name = ".".join((cls.__module__, cls.__name__))
+ except AttributeError:
+ cls_name = getattr(cls, "__name__", None)
+ if cls_name is None:
+ cls_name = repr(cls)
+ return cls_name
+
+
+@util.preload_module("sqlalchemy.orm.base")
+def _default_unmapped(cls: Type[Any]) -> Optional[str]:
+ base = util.preloaded.orm_base
+
+ try:
+ mappers = base.manager_of_class(cls).mappers # type: ignore
+ except (
+ UnmappedClassError,
+ TypeError,
+ ) + NO_STATE:
+ mappers = {}
+ name = _safe_cls_name(cls)
+
+ if not mappers:
+ return f"Class '{name}' is not mapped"
+ else:
+ return None
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/identity.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/identity.py
new file mode 100644
index 0000000..23682f7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/identity.py
@@ -0,0 +1,302 @@
+# orm/identity.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+from __future__ import annotations
+
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import NoReturn
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import TypeVar
+import weakref
+
+from . import util as orm_util
+from .. import exc as sa_exc
+
+if TYPE_CHECKING:
+ from ._typing import _IdentityKeyType
+ from .state import InstanceState
+
+
+_T = TypeVar("_T", bound=Any)
+
+_O = TypeVar("_O", bound=object)
+
+
+class IdentityMap:
+ _wr: weakref.ref[IdentityMap]
+
+ _dict: Dict[_IdentityKeyType[Any], Any]
+ _modified: Set[InstanceState[Any]]
+
+ def __init__(self) -> None:
+ self._dict = {}
+ self._modified = set()
+ self._wr = weakref.ref(self)
+
+ def _kill(self) -> None:
+ self._add_unpresent = _killed # type: ignore
+
+ def all_states(self) -> List[InstanceState[Any]]:
+ raise NotImplementedError()
+
+ def contains_state(self, state: InstanceState[Any]) -> bool:
+ raise NotImplementedError()
+
+ def __contains__(self, key: _IdentityKeyType[Any]) -> bool:
+ raise NotImplementedError()
+
+ def safe_discard(self, state: InstanceState[Any]) -> None:
+ raise NotImplementedError()
+
+ def __getitem__(self, key: _IdentityKeyType[_O]) -> _O:
+ raise NotImplementedError()
+
+ def get(
+ self, key: _IdentityKeyType[_O], default: Optional[_O] = None
+ ) -> Optional[_O]:
+ raise NotImplementedError()
+
+ def fast_get_state(
+ self, key: _IdentityKeyType[_O]
+ ) -> Optional[InstanceState[_O]]:
+ raise NotImplementedError()
+
+ def keys(self) -> Iterable[_IdentityKeyType[Any]]:
+ return self._dict.keys()
+
+ def values(self) -> Iterable[object]:
+ raise NotImplementedError()
+
+ def replace(self, state: InstanceState[_O]) -> Optional[InstanceState[_O]]:
+ raise NotImplementedError()
+
+ def add(self, state: InstanceState[Any]) -> bool:
+ raise NotImplementedError()
+
+ def _fast_discard(self, state: InstanceState[Any]) -> None:
+ raise NotImplementedError()
+
+ def _add_unpresent(
+ self, state: InstanceState[Any], key: _IdentityKeyType[Any]
+ ) -> None:
+ """optional inlined form of add() which can assume item isn't present
+ in the map"""
+ self.add(state)
+
+ def _manage_incoming_state(self, state: InstanceState[Any]) -> None:
+ state._instance_dict = self._wr
+
+ if state.modified:
+ self._modified.add(state)
+
+ def _manage_removed_state(self, state: InstanceState[Any]) -> None:
+ del state._instance_dict
+ if state.modified:
+ self._modified.discard(state)
+
+ def _dirty_states(self) -> Set[InstanceState[Any]]:
+ return self._modified
+
+ def check_modified(self) -> bool:
+ """return True if any InstanceStates present have been marked
+ as 'modified'.
+
+ """
+ return bool(self._modified)
+
+ def has_key(self, key: _IdentityKeyType[Any]) -> bool:
+ return key in self
+
+ def __len__(self) -> int:
+ return len(self._dict)
+
+
+class WeakInstanceDict(IdentityMap):
+ _dict: Dict[_IdentityKeyType[Any], InstanceState[Any]]
+
+ def __getitem__(self, key: _IdentityKeyType[_O]) -> _O:
+ state = cast("InstanceState[_O]", self._dict[key])
+ o = state.obj()
+ if o is None:
+ raise KeyError(key)
+ return o
+
+ def __contains__(self, key: _IdentityKeyType[Any]) -> bool:
+ try:
+ if key in self._dict:
+ state = self._dict[key]
+ o = state.obj()
+ else:
+ return False
+ except KeyError:
+ return False
+ else:
+ return o is not None
+
+ def contains_state(self, state: InstanceState[Any]) -> bool:
+ if state.key in self._dict:
+ if TYPE_CHECKING:
+ assert state.key is not None
+ try:
+ return self._dict[state.key] is state
+ except KeyError:
+ return False
+ else:
+ return False
+
+ def replace(
+ self, state: InstanceState[Any]
+ ) -> Optional[InstanceState[Any]]:
+ assert state.key is not None
+ if state.key in self._dict:
+ try:
+ existing = existing_non_none = self._dict[state.key]
+ except KeyError:
+ # catch gc removed the key after we just checked for it
+ existing = None
+ else:
+ if existing_non_none is not state:
+ self._manage_removed_state(existing_non_none)
+ else:
+ return None
+ else:
+ existing = None
+
+ self._dict[state.key] = state
+ self._manage_incoming_state(state)
+ return existing
+
+ def add(self, state: InstanceState[Any]) -> bool:
+ key = state.key
+ assert key is not None
+ # inline of self.__contains__
+ if key in self._dict:
+ try:
+ existing_state = self._dict[key]
+ except KeyError:
+ # catch gc removed the key after we just checked for it
+ pass
+ else:
+ if existing_state is not state:
+ o = existing_state.obj()
+ if o is not None:
+ raise sa_exc.InvalidRequestError(
+ "Can't attach instance "
+ "%s; another instance with key %s is already "
+ "present in this session."
+ % (orm_util.state_str(state), state.key)
+ )
+ else:
+ return False
+ self._dict[key] = state
+ self._manage_incoming_state(state)
+ return True
+
+ def _add_unpresent(
+ self, state: InstanceState[Any], key: _IdentityKeyType[Any]
+ ) -> None:
+ # inlined form of add() called by loading.py
+ self._dict[key] = state
+ state._instance_dict = self._wr
+
+ def fast_get_state(
+ self, key: _IdentityKeyType[_O]
+ ) -> Optional[InstanceState[_O]]:
+ return self._dict.get(key)
+
+ def get(
+ self, key: _IdentityKeyType[_O], default: Optional[_O] = None
+ ) -> Optional[_O]:
+ if key not in self._dict:
+ return default
+ try:
+ state = cast("InstanceState[_O]", self._dict[key])
+ except KeyError:
+ # catch gc removed the key after we just checked for it
+ return default
+ else:
+ o = state.obj()
+ if o is None:
+ return default
+ return o
+
+ def items(self) -> List[Tuple[_IdentityKeyType[Any], InstanceState[Any]]]:
+ values = self.all_states()
+ result = []
+ for state in values:
+ value = state.obj()
+ key = state.key
+ assert key is not None
+ if value is not None:
+ result.append((key, value))
+ return result
+
+ def values(self) -> List[object]:
+ values = self.all_states()
+ result = []
+ for state in values:
+ value = state.obj()
+ if value is not None:
+ result.append(value)
+
+ return result
+
+ def __iter__(self) -> Iterator[_IdentityKeyType[Any]]:
+ return iter(self.keys())
+
+ def all_states(self) -> List[InstanceState[Any]]:
+ return list(self._dict.values())
+
+ def _fast_discard(self, state: InstanceState[Any]) -> None:
+ # used by InstanceState for state being
+ # GC'ed, inlines _managed_removed_state
+ key = state.key
+ assert key is not None
+ try:
+ st = self._dict[key]
+ except KeyError:
+ # catch gc removed the key after we just checked for it
+ pass
+ else:
+ if st is state:
+ self._dict.pop(key, None)
+
+ def discard(self, state: InstanceState[Any]) -> None:
+ self.safe_discard(state)
+
+ def safe_discard(self, state: InstanceState[Any]) -> None:
+ key = state.key
+ if key in self._dict:
+ assert key is not None
+ try:
+ st = self._dict[key]
+ except KeyError:
+ # catch gc removed the key after we just checked for it
+ pass
+ else:
+ if st is state:
+ self._dict.pop(key, None)
+ self._manage_removed_state(state)
+
+
+def _killed(state: InstanceState[Any], key: _IdentityKeyType[Any]) -> NoReturn:
+ # external function to avoid creating cycles when assigned to
+ # the IdentityMap
+ raise sa_exc.InvalidRequestError(
+ "Object %s cannot be converted to 'persistent' state, as this "
+ "identity map is no longer valid. Has the owning Session "
+ "been closed?" % orm_util.state_str(state),
+ code="lkrp",
+ )
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/instrumentation.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/instrumentation.py
new file mode 100644
index 0000000..e9fe843
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/instrumentation.py
@@ -0,0 +1,754 @@
+# orm/instrumentation.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: allow-untyped-defs, allow-untyped-calls
+
+"""Defines SQLAlchemy's system of class instrumentation.
+
+This module is usually not directly visible to user applications, but
+defines a large part of the ORM's interactivity.
+
+instrumentation.py deals with registration of end-user classes
+for state tracking. It interacts closely with state.py
+and attributes.py which establish per-instance and per-class-attribute
+instrumentation, respectively.
+
+The class instrumentation system can be customized on a per-class
+or global basis using the :mod:`sqlalchemy.ext.instrumentation`
+module, which provides the means to build and specify
+alternate instrumentation forms.
+
+.. versionchanged: 0.8
+ The instrumentation extension system was moved out of the
+ ORM and into the external :mod:`sqlalchemy.ext.instrumentation`
+ package. When that package is imported, it installs
+ itself within sqlalchemy.orm so that its more comprehensive
+ resolution mechanics take effect.
+
+"""
+
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Collection
+from typing import Dict
+from typing import Generic
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import base
+from . import collections
+from . import exc
+from . import interfaces
+from . import state
+from ._typing import _O
+from .attributes import _is_collection_attribute_impl
+from .. import util
+from ..event import EventTarget
+from ..util import HasMemoized
+from ..util.typing import Literal
+from ..util.typing import Protocol
+
+if TYPE_CHECKING:
+ from ._typing import _RegistryType
+ from .attributes import AttributeImpl
+ from .attributes import QueryableAttribute
+ from .collections import _AdaptedCollectionProtocol
+ from .collections import _CollectionFactoryType
+ from .decl_base import _MapperConfig
+ from .events import InstanceEvents
+ from .mapper import Mapper
+ from .state import InstanceState
+ from ..event import dispatcher
+
+_T = TypeVar("_T", bound=Any)
+DEL_ATTR = util.symbol("DEL_ATTR")
+
+
+class _ExpiredAttributeLoaderProto(Protocol):
+ def __call__(
+ self,
+ state: state.InstanceState[Any],
+ toload: Set[str],
+ passive: base.PassiveFlag,
+ ) -> None: ...
+
+
+class _ManagerFactory(Protocol):
+ def __call__(self, class_: Type[_O]) -> ClassManager[_O]: ...
+
+
+class ClassManager(
+ HasMemoized,
+ Dict[str, "QueryableAttribute[Any]"],
+ Generic[_O],
+ EventTarget,
+):
+ """Tracks state information at the class level."""
+
+ dispatch: dispatcher[ClassManager[_O]]
+
+ MANAGER_ATTR = base.DEFAULT_MANAGER_ATTR
+ STATE_ATTR = base.DEFAULT_STATE_ATTR
+
+ _state_setter = staticmethod(util.attrsetter(STATE_ATTR))
+
+ expired_attribute_loader: _ExpiredAttributeLoaderProto
+ "previously known as deferred_scalar_loader"
+
+ init_method: Optional[Callable[..., None]]
+ original_init: Optional[Callable[..., None]] = None
+
+ factory: Optional[_ManagerFactory]
+
+ declarative_scan: Optional[weakref.ref[_MapperConfig]] = None
+
+ registry: _RegistryType
+
+ if not TYPE_CHECKING:
+ # starts as None during setup
+ registry = None
+
+ class_: Type[_O]
+
+ _bases: List[ClassManager[Any]]
+
+ @property
+ @util.deprecated(
+ "1.4",
+ message="The ClassManager.deferred_scalar_loader attribute is now "
+ "named expired_attribute_loader",
+ )
+ def deferred_scalar_loader(self):
+ return self.expired_attribute_loader
+
+ @deferred_scalar_loader.setter
+ @util.deprecated(
+ "1.4",
+ message="The ClassManager.deferred_scalar_loader attribute is now "
+ "named expired_attribute_loader",
+ )
+ def deferred_scalar_loader(self, obj):
+ self.expired_attribute_loader = obj
+
+ def __init__(self, class_):
+ self.class_ = class_
+ self.info = {}
+ self.new_init = None
+ self.local_attrs = {}
+ self.originals = {}
+ self._finalized = False
+ self.factory = None
+ self.init_method = None
+
+ self._bases = [
+ mgr
+ for mgr in cast(
+ "List[Optional[ClassManager[Any]]]",
+ [
+ opt_manager_of_class(base)
+ for base in self.class_.__bases__
+ if isinstance(base, type)
+ ],
+ )
+ if mgr is not None
+ ]
+
+ for base_ in self._bases:
+ self.update(base_)
+
+ cast(
+ "InstanceEvents", self.dispatch._events
+ )._new_classmanager_instance(class_, self)
+
+ for basecls in class_.__mro__:
+ mgr = opt_manager_of_class(basecls)
+ if mgr is not None:
+ self.dispatch._update(mgr.dispatch)
+
+ self.manage()
+
+ if "__del__" in class_.__dict__:
+ util.warn(
+ "__del__() method on class %s will "
+ "cause unreachable cycles and memory leaks, "
+ "as SQLAlchemy instrumentation often creates "
+ "reference cycles. Please remove this method." % class_
+ )
+
+ def _update_state(
+ self,
+ finalize: bool = False,
+ mapper: Optional[Mapper[_O]] = None,
+ registry: Optional[_RegistryType] = None,
+ declarative_scan: Optional[_MapperConfig] = None,
+ expired_attribute_loader: Optional[
+ _ExpiredAttributeLoaderProto
+ ] = None,
+ init_method: Optional[Callable[..., None]] = None,
+ ) -> None:
+ if mapper:
+ self.mapper = mapper #
+ if registry:
+ registry._add_manager(self)
+ if declarative_scan:
+ self.declarative_scan = weakref.ref(declarative_scan)
+ if expired_attribute_loader:
+ self.expired_attribute_loader = expired_attribute_loader
+
+ if init_method:
+ assert not self._finalized, (
+ "class is already instrumented, "
+ "init_method %s can't be applied" % init_method
+ )
+ self.init_method = init_method
+
+ if not self._finalized:
+ self.original_init = (
+ self.init_method
+ if self.init_method is not None
+ and self.class_.__init__ is object.__init__
+ else self.class_.__init__
+ )
+
+ if finalize and not self._finalized:
+ self._finalize()
+
+ def _finalize(self) -> None:
+ if self._finalized:
+ return
+ self._finalized = True
+
+ self._instrument_init()
+
+ _instrumentation_factory.dispatch.class_instrument(self.class_)
+
+ def __hash__(self) -> int: # type: ignore[override]
+ return id(self)
+
+ def __eq__(self, other: Any) -> bool:
+ return other is self
+
+ @property
+ def is_mapped(self) -> bool:
+ return "mapper" in self.__dict__
+
+ @HasMemoized.memoized_attribute
+ def _all_key_set(self):
+ return frozenset(self)
+
+ @HasMemoized.memoized_attribute
+ def _collection_impl_keys(self):
+ return frozenset(
+ [attr.key for attr in self.values() if attr.impl.collection]
+ )
+
+ @HasMemoized.memoized_attribute
+ def _scalar_loader_impls(self):
+ return frozenset(
+ [
+ attr.impl
+ for attr in self.values()
+ if attr.impl.accepts_scalar_loader
+ ]
+ )
+
+ @HasMemoized.memoized_attribute
+ def _loader_impls(self):
+ return frozenset([attr.impl for attr in self.values()])
+
+ @util.memoized_property
+ def mapper(self) -> Mapper[_O]:
+ # raises unless self.mapper has been assigned
+ raise exc.UnmappedClassError(self.class_)
+
+ def _all_sqla_attributes(self, exclude=None):
+ """return an iterator of all classbound attributes that are
+ implement :class:`.InspectionAttr`.
+
+ This includes :class:`.QueryableAttribute` as well as extension
+ types such as :class:`.hybrid_property` and
+ :class:`.AssociationProxy`.
+
+ """
+
+ found: Dict[str, Any] = {}
+
+ # constraints:
+ # 1. yield keys in cls.__dict__ order
+ # 2. if a subclass has the same key as a superclass, include that
+ # key as part of the ordering of the superclass, because an
+ # overridden key is usually installed by the mapper which is going
+ # on a different ordering
+ # 3. don't use getattr() as this fires off descriptors
+
+ for supercls in self.class_.__mro__[0:-1]:
+ inherits = supercls.__mro__[1]
+ for key in supercls.__dict__:
+ found.setdefault(key, supercls)
+ if key in inherits.__dict__:
+ continue
+ val = found[key].__dict__[key]
+ if (
+ isinstance(val, interfaces.InspectionAttr)
+ and val.is_attribute
+ ):
+ yield key, val
+
+ def _get_class_attr_mro(self, key, default=None):
+ """return an attribute on the class without tripping it."""
+
+ for supercls in self.class_.__mro__:
+ if key in supercls.__dict__:
+ return supercls.__dict__[key]
+ else:
+ return default
+
+ def _attr_has_impl(self, key: str) -> bool:
+ """Return True if the given attribute is fully initialized.
+
+ i.e. has an impl.
+ """
+
+ return key in self and self[key].impl is not None
+
+ def _subclass_manager(self, cls: Type[_T]) -> ClassManager[_T]:
+ """Create a new ClassManager for a subclass of this ClassManager's
+ class.
+
+ This is called automatically when attributes are instrumented so that
+ the attributes can be propagated to subclasses against their own
+ class-local manager, without the need for mappers etc. to have already
+ pre-configured managers for the full class hierarchy. Mappers
+ can post-configure the auto-generated ClassManager when needed.
+
+ """
+ return register_class(cls, finalize=False)
+
+ def _instrument_init(self):
+ self.new_init = _generate_init(self.class_, self, self.original_init)
+ self.install_member("__init__", self.new_init)
+
+ @util.memoized_property
+ def _state_constructor(self) -> Type[state.InstanceState[_O]]:
+ self.dispatch.first_init(self, self.class_)
+ return state.InstanceState
+
+ def manage(self):
+ """Mark this instance as the manager for its class."""
+
+ setattr(self.class_, self.MANAGER_ATTR, self)
+
+ @util.hybridmethod
+ def manager_getter(self):
+ return _default_manager_getter
+
+ @util.hybridmethod
+ def state_getter(self):
+ """Return a (instance) -> InstanceState callable.
+
+ "state getter" callables should raise either KeyError or
+ AttributeError if no InstanceState could be found for the
+ instance.
+ """
+
+ return _default_state_getter
+
+ @util.hybridmethod
+ def dict_getter(self):
+ return _default_dict_getter
+
+ def instrument_attribute(
+ self,
+ key: str,
+ inst: QueryableAttribute[Any],
+ propagated: bool = False,
+ ) -> None:
+ if propagated:
+ if key in self.local_attrs:
+ return # don't override local attr with inherited attr
+ else:
+ self.local_attrs[key] = inst
+ self.install_descriptor(key, inst)
+ self._reset_memoizations()
+ self[key] = inst
+
+ for cls in self.class_.__subclasses__():
+ manager = self._subclass_manager(cls)
+ manager.instrument_attribute(key, inst, True)
+
+ def subclass_managers(self, recursive):
+ for cls in self.class_.__subclasses__():
+ mgr = opt_manager_of_class(cls)
+ if mgr is not None and mgr is not self:
+ yield mgr
+ if recursive:
+ yield from mgr.subclass_managers(True)
+
+ def post_configure_attribute(self, key):
+ _instrumentation_factory.dispatch.attribute_instrument(
+ self.class_, key, self[key]
+ )
+
+ def uninstrument_attribute(self, key, propagated=False):
+ if key not in self:
+ return
+ if propagated:
+ if key in self.local_attrs:
+ return # don't get rid of local attr
+ else:
+ del self.local_attrs[key]
+ self.uninstall_descriptor(key)
+ self._reset_memoizations()
+ del self[key]
+ for cls in self.class_.__subclasses__():
+ manager = opt_manager_of_class(cls)
+ if manager:
+ manager.uninstrument_attribute(key, True)
+
+ def unregister(self) -> None:
+ """remove all instrumentation established by this ClassManager."""
+
+ for key in list(self.originals):
+ self.uninstall_member(key)
+
+ self.mapper = None
+ self.dispatch = None # type: ignore
+ self.new_init = None
+ self.info.clear()
+
+ for key in list(self):
+ if key in self.local_attrs:
+ self.uninstrument_attribute(key)
+
+ if self.MANAGER_ATTR in self.class_.__dict__:
+ delattr(self.class_, self.MANAGER_ATTR)
+
+ def install_descriptor(
+ self, key: str, inst: QueryableAttribute[Any]
+ ) -> None:
+ if key in (self.STATE_ATTR, self.MANAGER_ATTR):
+ raise KeyError(
+ "%r: requested attribute name conflicts with "
+ "instrumentation attribute of the same name." % key
+ )
+ setattr(self.class_, key, inst)
+
+ def uninstall_descriptor(self, key: str) -> None:
+ delattr(self.class_, key)
+
+ def install_member(self, key: str, implementation: Any) -> None:
+ if key in (self.STATE_ATTR, self.MANAGER_ATTR):
+ raise KeyError(
+ "%r: requested attribute name conflicts with "
+ "instrumentation attribute of the same name." % key
+ )
+ self.originals.setdefault(key, self.class_.__dict__.get(key, DEL_ATTR))
+ setattr(self.class_, key, implementation)
+
+ def uninstall_member(self, key: str) -> None:
+ original = self.originals.pop(key, None)
+ if original is not DEL_ATTR:
+ setattr(self.class_, key, original)
+ else:
+ delattr(self.class_, key)
+
+ def instrument_collection_class(
+ self, key: str, collection_class: Type[Collection[Any]]
+ ) -> _CollectionFactoryType:
+ return collections.prepare_instrumentation(collection_class)
+
+ def initialize_collection(
+ self,
+ key: str,
+ state: InstanceState[_O],
+ factory: _CollectionFactoryType,
+ ) -> Tuple[collections.CollectionAdapter, _AdaptedCollectionProtocol]:
+ user_data = factory()
+ impl = self.get_impl(key)
+ assert _is_collection_attribute_impl(impl)
+ adapter = collections.CollectionAdapter(impl, state, user_data)
+ return adapter, user_data
+
+ def is_instrumented(self, key: str, search: bool = False) -> bool:
+ if search:
+ return key in self
+ else:
+ return key in self.local_attrs
+
+ def get_impl(self, key: str) -> AttributeImpl:
+ return self[key].impl
+
+ @property
+ def attributes(self) -> Iterable[Any]:
+ return iter(self.values())
+
+ # InstanceState management
+
+ def new_instance(self, state: Optional[InstanceState[_O]] = None) -> _O:
+ # here, we would prefer _O to be bound to "object"
+ # so that mypy sees that __new__ is present. currently
+ # it's bound to Any as there were other problems not having
+ # it that way but these can be revisited
+ instance = self.class_.__new__(self.class_)
+ if state is None:
+ state = self._state_constructor(instance, self)
+ self._state_setter(instance, state)
+ return instance
+
+ def setup_instance(
+ self, instance: _O, state: Optional[InstanceState[_O]] = None
+ ) -> None:
+ if state is None:
+ state = self._state_constructor(instance, self)
+ self._state_setter(instance, state)
+
+ def teardown_instance(self, instance: _O) -> None:
+ delattr(instance, self.STATE_ATTR)
+
+ def _serialize(
+ self, state: InstanceState[_O], state_dict: Dict[str, Any]
+ ) -> _SerializeManager:
+ return _SerializeManager(state, state_dict)
+
+ def _new_state_if_none(
+ self, instance: _O
+ ) -> Union[Literal[False], InstanceState[_O]]:
+ """Install a default InstanceState if none is present.
+
+ A private convenience method used by the __init__ decorator.
+
+ """
+ if hasattr(instance, self.STATE_ATTR):
+ return False
+ elif self.class_ is not instance.__class__ and self.is_mapped:
+ # this will create a new ClassManager for the
+ # subclass, without a mapper. This is likely a
+ # user error situation but allow the object
+ # to be constructed, so that it is usable
+ # in a non-ORM context at least.
+ return self._subclass_manager(
+ instance.__class__
+ )._new_state_if_none(instance)
+ else:
+ state = self._state_constructor(instance, self)
+ self._state_setter(instance, state)
+ return state
+
+ def has_state(self, instance: _O) -> bool:
+ return hasattr(instance, self.STATE_ATTR)
+
+ def has_parent(
+ self, state: InstanceState[_O], key: str, optimistic: bool = False
+ ) -> bool:
+ """TODO"""
+ return self.get_impl(key).hasparent(state, optimistic=optimistic)
+
+ def __bool__(self) -> bool:
+ """All ClassManagers are non-zero regardless of attribute state."""
+ return True
+
+ def __repr__(self) -> str:
+ return "<%s of %r at %x>" % (
+ self.__class__.__name__,
+ self.class_,
+ id(self),
+ )
+
+
+class _SerializeManager:
+ """Provide serialization of a :class:`.ClassManager`.
+
+ The :class:`.InstanceState` uses ``__init__()`` on serialize
+ and ``__call__()`` on deserialize.
+
+ """
+
+ def __init__(self, state: state.InstanceState[Any], d: Dict[str, Any]):
+ self.class_ = state.class_
+ manager = state.manager
+ manager.dispatch.pickle(state, d)
+
+ def __call__(self, state, inst, state_dict):
+ state.manager = manager = opt_manager_of_class(self.class_)
+ if manager is None:
+ raise exc.UnmappedInstanceError(
+ inst,
+ "Cannot deserialize object of type %r - "
+ "no mapper() has "
+ "been configured for this class within the current "
+ "Python process!" % self.class_,
+ )
+ elif manager.is_mapped and not manager.mapper.configured:
+ manager.mapper._check_configure()
+
+ # setup _sa_instance_state ahead of time so that
+ # unpickle events can access the object normally.
+ # see [ticket:2362]
+ if inst is not None:
+ manager.setup_instance(inst, state)
+ manager.dispatch.unpickle(state, state_dict)
+
+
+class InstrumentationFactory(EventTarget):
+ """Factory for new ClassManager instances."""
+
+ dispatch: dispatcher[InstrumentationFactory]
+
+ def create_manager_for_cls(self, class_: Type[_O]) -> ClassManager[_O]:
+ assert class_ is not None
+ assert opt_manager_of_class(class_) is None
+
+ # give a more complicated subclass
+ # a chance to do what it wants here
+ manager, factory = self._locate_extended_factory(class_)
+
+ if factory is None:
+ factory = ClassManager
+ manager = ClassManager(class_)
+ else:
+ assert manager is not None
+
+ self._check_conflicts(class_, factory)
+
+ manager.factory = factory
+
+ return manager
+
+ def _locate_extended_factory(
+ self, class_: Type[_O]
+ ) -> Tuple[Optional[ClassManager[_O]], Optional[_ManagerFactory]]:
+ """Overridden by a subclass to do an extended lookup."""
+ return None, None
+
+ def _check_conflicts(
+ self, class_: Type[_O], factory: Callable[[Type[_O]], ClassManager[_O]]
+ ) -> None:
+ """Overridden by a subclass to test for conflicting factories."""
+
+ def unregister(self, class_: Type[_O]) -> None:
+ manager = manager_of_class(class_)
+ manager.unregister()
+ self.dispatch.class_uninstrument(class_)
+
+
+# this attribute is replaced by sqlalchemy.ext.instrumentation
+# when imported.
+_instrumentation_factory = InstrumentationFactory()
+
+# these attributes are replaced by sqlalchemy.ext.instrumentation
+# when a non-standard InstrumentationManager class is first
+# used to instrument a class.
+instance_state = _default_state_getter = base.instance_state
+
+instance_dict = _default_dict_getter = base.instance_dict
+
+manager_of_class = _default_manager_getter = base.manager_of_class
+opt_manager_of_class = _default_opt_manager_getter = base.opt_manager_of_class
+
+
+def register_class(
+ class_: Type[_O],
+ finalize: bool = True,
+ mapper: Optional[Mapper[_O]] = None,
+ registry: Optional[_RegistryType] = None,
+ declarative_scan: Optional[_MapperConfig] = None,
+ expired_attribute_loader: Optional[_ExpiredAttributeLoaderProto] = None,
+ init_method: Optional[Callable[..., None]] = None,
+) -> ClassManager[_O]:
+ """Register class instrumentation.
+
+ Returns the existing or newly created class manager.
+
+ """
+
+ manager = opt_manager_of_class(class_)
+ if manager is None:
+ manager = _instrumentation_factory.create_manager_for_cls(class_)
+ manager._update_state(
+ mapper=mapper,
+ registry=registry,
+ declarative_scan=declarative_scan,
+ expired_attribute_loader=expired_attribute_loader,
+ init_method=init_method,
+ finalize=finalize,
+ )
+
+ return manager
+
+
+def unregister_class(class_):
+ """Unregister class instrumentation."""
+
+ _instrumentation_factory.unregister(class_)
+
+
+def is_instrumented(instance, key):
+ """Return True if the given attribute on the given instance is
+ instrumented by the attributes package.
+
+ This function may be used regardless of instrumentation
+ applied directly to the class, i.e. no descriptors are required.
+
+ """
+ return manager_of_class(instance.__class__).is_instrumented(
+ key, search=True
+ )
+
+
+def _generate_init(class_, class_manager, original_init):
+ """Build an __init__ decorator that triggers ClassManager events."""
+
+ # TODO: we should use the ClassManager's notion of the
+ # original '__init__' method, once ClassManager is fixed
+ # to always reference that.
+
+ if original_init is None:
+ original_init = class_.__init__
+
+ # Go through some effort here and don't change the user's __init__
+ # calling signature, including the unlikely case that it has
+ # a return value.
+ # FIXME: need to juggle local names to avoid constructor argument
+ # clashes.
+ func_body = """\
+def __init__(%(apply_pos)s):
+ new_state = class_manager._new_state_if_none(%(self_arg)s)
+ if new_state:
+ return new_state._initialize_instance(%(apply_kw)s)
+ else:
+ return original_init(%(apply_kw)s)
+"""
+ func_vars = util.format_argspec_init(original_init, grouped=False)
+ func_text = func_body % func_vars
+
+ func_defaults = getattr(original_init, "__defaults__", None)
+ func_kw_defaults = getattr(original_init, "__kwdefaults__", None)
+
+ env = locals().copy()
+ env["__name__"] = __name__
+ exec(func_text, env)
+ __init__ = env["__init__"]
+ __init__.__doc__ = original_init.__doc__
+ __init__._sa_original_init = original_init
+
+ if func_defaults:
+ __init__.__defaults__ = func_defaults
+ if func_kw_defaults:
+ __init__.__kwdefaults__ = func_kw_defaults
+
+ return __init__
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py
new file mode 100644
index 0000000..36336e7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py
@@ -0,0 +1,1469 @@
+# orm/interfaces.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""
+
+Contains various base classes used throughout the ORM.
+
+Defines some key base classes prominent within the internals.
+
+This module and the classes within are mostly private, though some attributes
+are exposed when inspecting mappings.
+
+"""
+
+from __future__ import annotations
+
+import collections
+import dataclasses
+import typing
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import ClassVar
+from typing import Dict
+from typing import Generic
+from typing import Iterator
+from typing import List
+from typing import NamedTuple
+from typing import NoReturn
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import exc as orm_exc
+from . import path_registry
+from .base import _MappedAttribute as _MappedAttribute
+from .base import EXT_CONTINUE as EXT_CONTINUE # noqa: F401
+from .base import EXT_SKIP as EXT_SKIP # noqa: F401
+from .base import EXT_STOP as EXT_STOP # noqa: F401
+from .base import InspectionAttr as InspectionAttr # noqa: F401
+from .base import InspectionAttrInfo as InspectionAttrInfo
+from .base import MANYTOMANY as MANYTOMANY # noqa: F401
+from .base import MANYTOONE as MANYTOONE # noqa: F401
+from .base import NO_KEY as NO_KEY # noqa: F401
+from .base import NO_VALUE as NO_VALUE # noqa: F401
+from .base import NotExtension as NotExtension # noqa: F401
+from .base import ONETOMANY as ONETOMANY # noqa: F401
+from .base import RelationshipDirection as RelationshipDirection # noqa: F401
+from .base import SQLORMOperations
+from .. import ColumnElement
+from .. import exc as sa_exc
+from .. import inspection
+from .. import util
+from ..sql import operators
+from ..sql import roles
+from ..sql import visitors
+from ..sql.base import _NoArg
+from ..sql.base import ExecutableOption
+from ..sql.cache_key import HasCacheKey
+from ..sql.operators import ColumnOperators
+from ..sql.schema import Column
+from ..sql.type_api import TypeEngine
+from ..util import warn_deprecated
+from ..util.typing import RODescriptorReference
+from ..util.typing import TypedDict
+
+if typing.TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _IdentityKeyType
+ from ._typing import _InstanceDict
+ from ._typing import _InternalEntityType
+ from ._typing import _ORMAdapterProto
+ from .attributes import InstrumentedAttribute
+ from .base import Mapped
+ from .context import _MapperEntity
+ from .context import ORMCompileState
+ from .context import QueryContext
+ from .decl_api import RegistryType
+ from .decl_base import _ClassScanMapperConfig
+ from .loading import _PopulatorDict
+ from .mapper import Mapper
+ from .path_registry import AbstractEntityRegistry
+ from .query import Query
+ from .session import Session
+ from .state import InstanceState
+ from .strategy_options import _LoadElement
+ from .util import AliasedInsp
+ from .util import ORMAdapter
+ from ..engine.result import Result
+ from ..sql._typing import _ColumnExpressionArgument
+ from ..sql._typing import _ColumnsClauseArgument
+ from ..sql._typing import _DMLColumnArgument
+ from ..sql._typing import _InfoType
+ from ..sql.operators import OperatorType
+ from ..sql.visitors import _TraverseInternalsType
+ from ..util.typing import _AnnotationScanType
+
+_StrategyKey = Tuple[Any, ...]
+
+_T = TypeVar("_T", bound=Any)
+_T_co = TypeVar("_T_co", bound=Any, covariant=True)
+
+_TLS = TypeVar("_TLS", bound="Type[LoaderStrategy]")
+
+
+class ORMStatementRole(roles.StatementRole):
+ __slots__ = ()
+ _role_name = (
+ "Executable SQL or text() construct, including ORM aware objects"
+ )
+
+
+class ORMColumnsClauseRole(
+ roles.ColumnsClauseRole, roles.TypedColumnsClauseRole[_T]
+):
+ __slots__ = ()
+ _role_name = "ORM mapped entity, aliased entity, or Column expression"
+
+
+class ORMEntityColumnsClauseRole(ORMColumnsClauseRole[_T]):
+ __slots__ = ()
+ _role_name = "ORM mapped or aliased entity"
+
+
+class ORMFromClauseRole(roles.StrictFromClauseRole):
+ __slots__ = ()
+ _role_name = "ORM mapped entity, aliased entity, or FROM expression"
+
+
+class ORMColumnDescription(TypedDict):
+ name: str
+ # TODO: add python_type and sql_type here; combining them
+ # into "type" is a bad idea
+ type: Union[Type[Any], TypeEngine[Any]]
+ aliased: bool
+ expr: _ColumnsClauseArgument[Any]
+ entity: Optional[_ColumnsClauseArgument[Any]]
+
+
+class _IntrospectsAnnotations:
+ __slots__ = ()
+
+ @classmethod
+ def _mapper_property_name(cls) -> str:
+ return cls.__name__
+
+ def found_in_pep593_annotated(self) -> Any:
+ """return a copy of this object to use in declarative when the
+ object is found inside of an Annotated object."""
+
+ raise NotImplementedError(
+ f"Use of the {self._mapper_property_name()!r} "
+ "construct inside of an Annotated object is not yet supported."
+ )
+
+ def declarative_scan(
+ self,
+ decl_scan: _ClassScanMapperConfig,
+ registry: RegistryType,
+ cls: Type[Any],
+ originating_module: Optional[str],
+ key: str,
+ mapped_container: Optional[Type[Mapped[Any]]],
+ annotation: Optional[_AnnotationScanType],
+ extracted_mapped_annotation: Optional[_AnnotationScanType],
+ is_dataclass_field: bool,
+ ) -> None:
+ """Perform class-specific initializaton at early declarative scanning
+ time.
+
+ .. versionadded:: 2.0
+
+ """
+
+ def _raise_for_required(self, key: str, cls: Type[Any]) -> NoReturn:
+ raise sa_exc.ArgumentError(
+ f"Python typing annotation is required for attribute "
+ f'"{cls.__name__}.{key}" when primary argument(s) for '
+ f'"{self._mapper_property_name()}" '
+ "construct are None or not present"
+ )
+
+
+class _AttributeOptions(NamedTuple):
+ """define Python-local attribute behavior options common to all
+ :class:`.MapperProperty` objects.
+
+ Currently this includes dataclass-generation arguments.
+
+ .. versionadded:: 2.0
+
+ """
+
+ dataclasses_init: Union[_NoArg, bool]
+ dataclasses_repr: Union[_NoArg, bool]
+ dataclasses_default: Union[_NoArg, Any]
+ dataclasses_default_factory: Union[_NoArg, Callable[[], Any]]
+ dataclasses_compare: Union[_NoArg, bool]
+ dataclasses_kw_only: Union[_NoArg, bool]
+
+ def _as_dataclass_field(self, key: str) -> Any:
+ """Return a ``dataclasses.Field`` object given these arguments."""
+
+ kw: Dict[str, Any] = {}
+ if self.dataclasses_default_factory is not _NoArg.NO_ARG:
+ kw["default_factory"] = self.dataclasses_default_factory
+ if self.dataclasses_default is not _NoArg.NO_ARG:
+ kw["default"] = self.dataclasses_default
+ if self.dataclasses_init is not _NoArg.NO_ARG:
+ kw["init"] = self.dataclasses_init
+ if self.dataclasses_repr is not _NoArg.NO_ARG:
+ kw["repr"] = self.dataclasses_repr
+ if self.dataclasses_compare is not _NoArg.NO_ARG:
+ kw["compare"] = self.dataclasses_compare
+ if self.dataclasses_kw_only is not _NoArg.NO_ARG:
+ kw["kw_only"] = self.dataclasses_kw_only
+
+ if "default" in kw and callable(kw["default"]):
+ # callable defaults are ambiguous. deprecate them in favour of
+ # insert_default or default_factory. #9936
+ warn_deprecated(
+ f"Callable object passed to the ``default`` parameter for "
+ f"attribute {key!r} in a ORM-mapped Dataclasses context is "
+ "ambiguous, "
+ "and this use will raise an error in a future release. "
+ "If this callable is intended to produce Core level INSERT "
+ "default values for an underlying ``Column``, use "
+ "the ``mapped_column.insert_default`` parameter instead. "
+ "To establish this callable as providing a default value "
+ "for instances of the dataclass itself, use the "
+ "``default_factory`` dataclasses parameter.",
+ "2.0",
+ )
+
+ if (
+ "init" in kw
+ and not kw["init"]
+ and "default" in kw
+ and not callable(kw["default"]) # ignore callable defaults. #9936
+ and "default_factory" not in kw # illegal but let dc.field raise
+ ):
+ # fix for #9879
+ default = kw.pop("default")
+ kw["default_factory"] = lambda: default
+
+ return dataclasses.field(**kw)
+
+ @classmethod
+ def _get_arguments_for_make_dataclass(
+ cls,
+ key: str,
+ annotation: _AnnotationScanType,
+ mapped_container: Optional[Any],
+ elem: _T,
+ ) -> Union[
+ Tuple[str, _AnnotationScanType],
+ Tuple[str, _AnnotationScanType, dataclasses.Field[Any]],
+ ]:
+ """given attribute key, annotation, and value from a class, return
+ the argument tuple we would pass to dataclasses.make_dataclass()
+ for this attribute.
+
+ """
+ if isinstance(elem, _DCAttributeOptions):
+ dc_field = elem._attribute_options._as_dataclass_field(key)
+
+ return (key, annotation, dc_field)
+ elif elem is not _NoArg.NO_ARG:
+ # why is typing not erroring on this?
+ return (key, annotation, elem)
+ elif mapped_container is not None:
+ # it's Mapped[], but there's no "element", which means declarative
+ # did not actually do anything for this field. this shouldn't
+ # happen.
+ # previously, this would occur because _scan_attributes would
+ # skip a field that's on an already mapped superclass, but it
+ # would still include it in the annotations, leading
+ # to issue #8718
+
+ assert False, "Mapped[] received without a mapping declaration"
+
+ else:
+ # plain dataclass field, not mapped. Is only possible
+ # if __allow_unmapped__ is set up. I can see this mode causing
+ # problems...
+ return (key, annotation)
+
+
+_DEFAULT_ATTRIBUTE_OPTIONS = _AttributeOptions(
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+)
+
+_DEFAULT_READONLY_ATTRIBUTE_OPTIONS = _AttributeOptions(
+ False,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+)
+
+
+class _DCAttributeOptions:
+ """mixin for descriptors or configurational objects that include dataclass
+ field options.
+
+ This includes :class:`.MapperProperty`, :class:`._MapsColumn` within
+ the ORM, but also includes :class:`.AssociationProxy` within ext.
+ Can in theory be used for other descriptors that serve a similar role
+ as association proxy. (*maybe* hybrids, not sure yet.)
+
+ """
+
+ __slots__ = ()
+
+ _attribute_options: _AttributeOptions
+ """behavioral options for ORM-enabled Python attributes
+
+ .. versionadded:: 2.0
+
+ """
+
+ _has_dataclass_arguments: bool
+
+
+class _MapsColumns(_DCAttributeOptions, _MappedAttribute[_T]):
+ """interface for declarative-capable construct that delivers one or more
+ Column objects to the declarative process to be part of a Table.
+ """
+
+ __slots__ = ()
+
+ @property
+ def mapper_property_to_assign(self) -> Optional[MapperProperty[_T]]:
+ """return a MapperProperty to be assigned to the declarative mapping"""
+ raise NotImplementedError()
+
+ @property
+ def columns_to_assign(self) -> List[Tuple[Column[_T], int]]:
+ """A list of Column objects that should be declaratively added to the
+ new Table object.
+
+ """
+ raise NotImplementedError()
+
+
+# NOTE: MapperProperty needs to extend _MappedAttribute so that declarative
+# typing works, i.e. "Mapped[A] = relationship()". This introduces an
+# inconvenience which is that all the MapperProperty objects are treated
+# as descriptors by typing tools, which are misled by this as assignment /
+# access to a descriptor attribute wants to move through __get__.
+# Therefore, references to MapperProperty as an instance variable, such
+# as in PropComparator, may have some special typing workarounds such as the
+# use of sqlalchemy.util.typing.DescriptorReference to avoid mis-interpretation
+# by typing tools
+@inspection._self_inspects
+class MapperProperty(
+ HasCacheKey,
+ _DCAttributeOptions,
+ _MappedAttribute[_T],
+ InspectionAttrInfo,
+ util.MemoizedSlots,
+):
+ """Represent a particular class attribute mapped by :class:`_orm.Mapper`.
+
+ The most common occurrences of :class:`.MapperProperty` are the
+ mapped :class:`_schema.Column`, which is represented in a mapping as
+ an instance of :class:`.ColumnProperty`,
+ and a reference to another class produced by :func:`_orm.relationship`,
+ represented in the mapping as an instance of
+ :class:`.Relationship`.
+
+ """
+
+ __slots__ = (
+ "_configure_started",
+ "_configure_finished",
+ "_attribute_options",
+ "_has_dataclass_arguments",
+ "parent",
+ "key",
+ "info",
+ "doc",
+ )
+
+ _cache_key_traversal: _TraverseInternalsType = [
+ ("parent", visitors.ExtendedInternalTraversal.dp_has_cache_key),
+ ("key", visitors.ExtendedInternalTraversal.dp_string),
+ ]
+
+ if not TYPE_CHECKING:
+ cascade = None
+
+ is_property = True
+ """Part of the InspectionAttr interface; states this object is a
+ mapper property.
+
+ """
+
+ comparator: PropComparator[_T]
+ """The :class:`_orm.PropComparator` instance that implements SQL
+ expression construction on behalf of this mapped attribute."""
+
+ key: str
+ """name of class attribute"""
+
+ parent: Mapper[Any]
+ """the :class:`.Mapper` managing this property."""
+
+ _is_relationship = False
+
+ _links_to_entity: bool
+ """True if this MapperProperty refers to a mapped entity.
+
+ Should only be True for Relationship, False for all others.
+
+ """
+
+ doc: Optional[str]
+ """optional documentation string"""
+
+ info: _InfoType
+ """Info dictionary associated with the object, allowing user-defined
+ data to be associated with this :class:`.InspectionAttr`.
+
+ The dictionary is generated when first accessed. Alternatively,
+ it can be specified as a constructor argument to the
+ :func:`.column_property`, :func:`_orm.relationship`, or :func:`.composite`
+ functions.
+
+ .. seealso::
+
+ :attr:`.QueryableAttribute.info`
+
+ :attr:`.SchemaItem.info`
+
+ """
+
+ def _memoized_attr_info(self) -> _InfoType:
+ """Info dictionary associated with the object, allowing user-defined
+ data to be associated with this :class:`.InspectionAttr`.
+
+ The dictionary is generated when first accessed. Alternatively,
+ it can be specified as a constructor argument to the
+ :func:`.column_property`, :func:`_orm.relationship`, or
+ :func:`.composite`
+ functions.
+
+ .. seealso::
+
+ :attr:`.QueryableAttribute.info`
+
+ :attr:`.SchemaItem.info`
+
+ """
+ return {}
+
+ def setup(
+ self,
+ context: ORMCompileState,
+ query_entity: _MapperEntity,
+ path: AbstractEntityRegistry,
+ adapter: Optional[ORMAdapter],
+ **kwargs: Any,
+ ) -> None:
+ """Called by Query for the purposes of constructing a SQL statement.
+
+ Each MapperProperty associated with the target mapper processes the
+ statement referenced by the query context, adding columns and/or
+ criterion as appropriate.
+
+ """
+
+ def create_row_processor(
+ self,
+ context: ORMCompileState,
+ query_entity: _MapperEntity,
+ path: AbstractEntityRegistry,
+ mapper: Mapper[Any],
+ result: Result[Any],
+ adapter: Optional[ORMAdapter],
+ populators: _PopulatorDict,
+ ) -> None:
+ """Produce row processing functions and append to the given
+ set of populators lists.
+
+ """
+
+ def cascade_iterator(
+ self,
+ type_: str,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ visited_states: Set[InstanceState[Any]],
+ halt_on: Optional[Callable[[InstanceState[Any]], bool]] = None,
+ ) -> Iterator[
+ Tuple[object, Mapper[Any], InstanceState[Any], _InstanceDict]
+ ]:
+ """Iterate through instances related to the given instance for
+ a particular 'cascade', starting with this MapperProperty.
+
+ Return an iterator3-tuples (instance, mapper, state).
+
+ Note that the 'cascade' collection on this MapperProperty is
+ checked first for the given type before cascade_iterator is called.
+
+ This method typically only applies to Relationship.
+
+ """
+
+ return iter(())
+
+ def set_parent(self, parent: Mapper[Any], init: bool) -> None:
+ """Set the parent mapper that references this MapperProperty.
+
+ This method is overridden by some subclasses to perform extra
+ setup when the mapper is first known.
+
+ """
+ self.parent = parent
+
+ def instrument_class(self, mapper: Mapper[Any]) -> None:
+ """Hook called by the Mapper to the property to initiate
+ instrumentation of the class attribute managed by this
+ MapperProperty.
+
+ The MapperProperty here will typically call out to the
+ attributes module to set up an InstrumentedAttribute.
+
+ This step is the first of two steps to set up an InstrumentedAttribute,
+ and is called early in the mapper setup process.
+
+ The second step is typically the init_class_attribute step,
+ called from StrategizedProperty via the post_instrument_class()
+ hook. This step assigns additional state to the InstrumentedAttribute
+ (specifically the "impl") which has been determined after the
+ MapperProperty has determined what kind of persistence
+ management it needs to do (e.g. scalar, object, collection, etc).
+
+ """
+
+ def __init__(
+ self,
+ attribute_options: Optional[_AttributeOptions] = None,
+ _assume_readonly_dc_attributes: bool = False,
+ ) -> None:
+ self._configure_started = False
+ self._configure_finished = False
+
+ if _assume_readonly_dc_attributes:
+ default_attrs = _DEFAULT_READONLY_ATTRIBUTE_OPTIONS
+ else:
+ default_attrs = _DEFAULT_ATTRIBUTE_OPTIONS
+
+ if attribute_options and attribute_options != default_attrs:
+ self._has_dataclass_arguments = True
+ self._attribute_options = attribute_options
+ else:
+ self._has_dataclass_arguments = False
+ self._attribute_options = default_attrs
+
+ def init(self) -> None:
+ """Called after all mappers are created to assemble
+ relationships between mappers and perform other post-mapper-creation
+ initialization steps.
+
+
+ """
+ self._configure_started = True
+ self.do_init()
+ self._configure_finished = True
+
+ @property
+ def class_attribute(self) -> InstrumentedAttribute[_T]:
+ """Return the class-bound descriptor corresponding to this
+ :class:`.MapperProperty`.
+
+ This is basically a ``getattr()`` call::
+
+ return getattr(self.parent.class_, self.key)
+
+ I.e. if this :class:`.MapperProperty` were named ``addresses``,
+ and the class to which it is mapped is ``User``, this sequence
+ is possible::
+
+ >>> from sqlalchemy import inspect
+ >>> mapper = inspect(User)
+ >>> addresses_property = mapper.attrs.addresses
+ >>> addresses_property.class_attribute is User.addresses
+ True
+ >>> User.addresses.property is addresses_property
+ True
+
+
+ """
+
+ return getattr(self.parent.class_, self.key) # type: ignore
+
+ def do_init(self) -> None:
+ """Perform subclass-specific initialization post-mapper-creation
+ steps.
+
+ This is a template method called by the ``MapperProperty``
+ object's init() method.
+
+ """
+
+ def post_instrument_class(self, mapper: Mapper[Any]) -> None:
+ """Perform instrumentation adjustments that need to occur
+ after init() has completed.
+
+ The given Mapper is the Mapper invoking the operation, which
+ may not be the same Mapper as self.parent in an inheritance
+ scenario; however, Mapper will always at least be a sub-mapper of
+ self.parent.
+
+ This method is typically used by StrategizedProperty, which delegates
+ it to LoaderStrategy.init_class_attribute() to perform final setup
+ on the class-bound InstrumentedAttribute.
+
+ """
+
+ def merge(
+ self,
+ session: Session,
+ source_state: InstanceState[Any],
+ source_dict: _InstanceDict,
+ dest_state: InstanceState[Any],
+ dest_dict: _InstanceDict,
+ load: bool,
+ _recursive: Dict[Any, object],
+ _resolve_conflict_map: Dict[_IdentityKeyType[Any], object],
+ ) -> None:
+ """Merge the attribute represented by this ``MapperProperty``
+ from source to destination object.
+
+ """
+
+ def __repr__(self) -> str:
+ return "<%s at 0x%x; %s>" % (
+ self.__class__.__name__,
+ id(self),
+ getattr(self, "key", "no key"),
+ )
+
+
+@inspection._self_inspects
+class PropComparator(SQLORMOperations[_T_co], Generic[_T_co], ColumnOperators):
+ r"""Defines SQL operations for ORM mapped attributes.
+
+ SQLAlchemy allows for operators to
+ be redefined at both the Core and ORM level. :class:`.PropComparator`
+ is the base class of operator redefinition for ORM-level operations,
+ including those of :class:`.ColumnProperty`,
+ :class:`.Relationship`, and :class:`.Composite`.
+
+ User-defined subclasses of :class:`.PropComparator` may be created. The
+ built-in Python comparison and math operator methods, such as
+ :meth:`.operators.ColumnOperators.__eq__`,
+ :meth:`.operators.ColumnOperators.__lt__`, and
+ :meth:`.operators.ColumnOperators.__add__`, can be overridden to provide
+ new operator behavior. The custom :class:`.PropComparator` is passed to
+ the :class:`.MapperProperty` instance via the ``comparator_factory``
+ argument. In each case,
+ the appropriate subclass of :class:`.PropComparator` should be used::
+
+ # definition of custom PropComparator subclasses
+
+ from sqlalchemy.orm.properties import \
+ ColumnProperty,\
+ Composite,\
+ Relationship
+
+ class MyColumnComparator(ColumnProperty.Comparator):
+ def __eq__(self, other):
+ return self.__clause_element__() == other
+
+ class MyRelationshipComparator(Relationship.Comparator):
+ def any(self, expression):
+ "define the 'any' operation"
+ # ...
+
+ class MyCompositeComparator(Composite.Comparator):
+ def __gt__(self, other):
+ "redefine the 'greater than' operation"
+
+ return sql.and_(*[a>b for a, b in
+ zip(self.__clause_element__().clauses,
+ other.__composite_values__())])
+
+
+ # application of custom PropComparator subclasses
+
+ from sqlalchemy.orm import column_property, relationship, composite
+ from sqlalchemy import Column, String
+
+ class SomeMappedClass(Base):
+ some_column = column_property(Column("some_column", String),
+ comparator_factory=MyColumnComparator)
+
+ some_relationship = relationship(SomeOtherClass,
+ comparator_factory=MyRelationshipComparator)
+
+ some_composite = composite(
+ Column("a", String), Column("b", String),
+ comparator_factory=MyCompositeComparator
+ )
+
+ Note that for column-level operator redefinition, it's usually
+ simpler to define the operators at the Core level, using the
+ :attr:`.TypeEngine.comparator_factory` attribute. See
+ :ref:`types_operators` for more detail.
+
+ .. seealso::
+
+ :class:`.ColumnProperty.Comparator`
+
+ :class:`.Relationship.Comparator`
+
+ :class:`.Composite.Comparator`
+
+ :class:`.ColumnOperators`
+
+ :ref:`types_operators`
+
+ :attr:`.TypeEngine.comparator_factory`
+
+ """
+
+ __slots__ = "prop", "_parententity", "_adapt_to_entity"
+
+ __visit_name__ = "orm_prop_comparator"
+
+ _parententity: _InternalEntityType[Any]
+ _adapt_to_entity: Optional[AliasedInsp[Any]]
+ prop: RODescriptorReference[MapperProperty[_T_co]]
+
+ def __init__(
+ self,
+ prop: MapperProperty[_T],
+ parentmapper: _InternalEntityType[Any],
+ adapt_to_entity: Optional[AliasedInsp[Any]] = None,
+ ):
+ self.prop = prop
+ self._parententity = adapt_to_entity or parentmapper
+ self._adapt_to_entity = adapt_to_entity
+
+ @util.non_memoized_property
+ def property(self) -> MapperProperty[_T_co]:
+ """Return the :class:`.MapperProperty` associated with this
+ :class:`.PropComparator`.
+
+
+ Return values here will commonly be instances of
+ :class:`.ColumnProperty` or :class:`.Relationship`.
+
+
+ """
+ return self.prop
+
+ def __clause_element__(self) -> roles.ColumnsClauseRole:
+ raise NotImplementedError("%r" % self)
+
+ def _bulk_update_tuples(
+ self, value: Any
+ ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
+ """Receive a SQL expression that represents a value in the SET
+ clause of an UPDATE statement.
+
+ Return a tuple that can be passed to a :class:`_expression.Update`
+ construct.
+
+ """
+
+ return [(cast("_DMLColumnArgument", self.__clause_element__()), value)]
+
+ def adapt_to_entity(
+ self, adapt_to_entity: AliasedInsp[Any]
+ ) -> PropComparator[_T_co]:
+ """Return a copy of this PropComparator which will use the given
+ :class:`.AliasedInsp` to produce corresponding expressions.
+ """
+ return self.__class__(self.prop, self._parententity, adapt_to_entity)
+
+ @util.ro_non_memoized_property
+ def _parentmapper(self) -> Mapper[Any]:
+ """legacy; this is renamed to _parententity to be
+ compatible with QueryableAttribute."""
+ return self._parententity.mapper
+
+ def _criterion_exists(
+ self,
+ criterion: Optional[_ColumnExpressionArgument[bool]] = None,
+ **kwargs: Any,
+ ) -> ColumnElement[Any]:
+ return self.prop.comparator._criterion_exists(criterion, **kwargs)
+
+ @util.ro_non_memoized_property
+ def adapter(self) -> Optional[_ORMAdapterProto]:
+ """Produce a callable that adapts column expressions
+ to suit an aliased version of this comparator.
+
+ """
+ if self._adapt_to_entity is None:
+ return None
+ else:
+ return self._adapt_to_entity._orm_adapt_element
+
+ @util.ro_non_memoized_property
+ def info(self) -> _InfoType:
+ return self.prop.info
+
+ @staticmethod
+ def _any_op(a: Any, b: Any, **kwargs: Any) -> Any:
+ return a.any(b, **kwargs)
+
+ @staticmethod
+ def _has_op(left: Any, other: Any, **kwargs: Any) -> Any:
+ return left.has(other, **kwargs)
+
+ @staticmethod
+ def _of_type_op(a: Any, class_: Any) -> Any:
+ return a.of_type(class_)
+
+ any_op = cast(operators.OperatorType, _any_op)
+ has_op = cast(operators.OperatorType, _has_op)
+ of_type_op = cast(operators.OperatorType, _of_type_op)
+
+ if typing.TYPE_CHECKING:
+
+ def operate(
+ self, op: OperatorType, *other: Any, **kwargs: Any
+ ) -> ColumnElement[Any]: ...
+
+ def reverse_operate(
+ self, op: OperatorType, other: Any, **kwargs: Any
+ ) -> ColumnElement[Any]: ...
+
+ def of_type(self, class_: _EntityType[Any]) -> PropComparator[_T_co]:
+ r"""Redefine this object in terms of a polymorphic subclass,
+ :func:`_orm.with_polymorphic` construct, or :func:`_orm.aliased`
+ construct.
+
+ Returns a new PropComparator from which further criterion can be
+ evaluated.
+
+ e.g.::
+
+ query.join(Company.employees.of_type(Engineer)).\
+ filter(Engineer.name=='foo')
+
+ :param \class_: a class or mapper indicating that criterion will be
+ against this specific subclass.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_joining_relationships_aliased` - in the
+ :ref:`queryguide_toplevel`
+
+ :ref:`inheritance_of_type`
+
+ """
+
+ return self.operate(PropComparator.of_type_op, class_) # type: ignore
+
+ def and_(
+ self, *criteria: _ColumnExpressionArgument[bool]
+ ) -> PropComparator[bool]:
+ """Add additional criteria to the ON clause that's represented by this
+ relationship attribute.
+
+ E.g.::
+
+
+ stmt = select(User).join(
+ User.addresses.and_(Address.email_address != 'foo')
+ )
+
+ stmt = select(User).options(
+ joinedload(User.addresses.and_(Address.email_address != 'foo'))
+ )
+
+ .. versionadded:: 1.4
+
+ .. seealso::
+
+ :ref:`orm_queryguide_join_on_augmented`
+
+ :ref:`loader_option_criteria`
+
+ :func:`.with_loader_criteria`
+
+ """
+ return self.operate(operators.and_, *criteria) # type: ignore
+
+ def any(
+ self,
+ criterion: Optional[_ColumnExpressionArgument[bool]] = None,
+ **kwargs: Any,
+ ) -> ColumnElement[bool]:
+ r"""Return a SQL expression representing true if this element
+ references a member which meets the given criterion.
+
+ The usual implementation of ``any()`` is
+ :meth:`.Relationship.Comparator.any`.
+
+ :param criterion: an optional ClauseElement formulated against the
+ member class' table or attributes.
+
+ :param \**kwargs: key/value pairs corresponding to member class
+ attribute names which will be compared via equality to the
+ corresponding values.
+
+ """
+
+ return self.operate(PropComparator.any_op, criterion, **kwargs)
+
+ def has(
+ self,
+ criterion: Optional[_ColumnExpressionArgument[bool]] = None,
+ **kwargs: Any,
+ ) -> ColumnElement[bool]:
+ r"""Return a SQL expression representing true if this element
+ references a member which meets the given criterion.
+
+ The usual implementation of ``has()`` is
+ :meth:`.Relationship.Comparator.has`.
+
+ :param criterion: an optional ClauseElement formulated against the
+ member class' table or attributes.
+
+ :param \**kwargs: key/value pairs corresponding to member class
+ attribute names which will be compared via equality to the
+ corresponding values.
+
+ """
+
+ return self.operate(PropComparator.has_op, criterion, **kwargs)
+
+
+class StrategizedProperty(MapperProperty[_T]):
+ """A MapperProperty which uses selectable strategies to affect
+ loading behavior.
+
+ There is a single strategy selected by default. Alternate
+ strategies can be selected at Query time through the usage of
+ ``StrategizedOption`` objects via the Query.options() method.
+
+ The mechanics of StrategizedProperty are used for every Query
+ invocation for every mapped attribute participating in that Query,
+ to determine first how the attribute will be rendered in SQL
+ and secondly how the attribute will retrieve a value from a result
+ row and apply it to a mapped object. The routines here are very
+ performance-critical.
+
+ """
+
+ __slots__ = (
+ "_strategies",
+ "strategy",
+ "_wildcard_token",
+ "_default_path_loader_key",
+ "strategy_key",
+ )
+ inherit_cache = True
+ strategy_wildcard_key: ClassVar[str]
+
+ strategy_key: _StrategyKey
+
+ _strategies: Dict[_StrategyKey, LoaderStrategy]
+
+ def _memoized_attr__wildcard_token(self) -> Tuple[str]:
+ return (
+ f"{self.strategy_wildcard_key}:{path_registry._WILDCARD_TOKEN}",
+ )
+
+ def _memoized_attr__default_path_loader_key(
+ self,
+ ) -> Tuple[str, Tuple[str]]:
+ return (
+ "loader",
+ (f"{self.strategy_wildcard_key}:{path_registry._DEFAULT_TOKEN}",),
+ )
+
+ def _get_context_loader(
+ self, context: ORMCompileState, path: AbstractEntityRegistry
+ ) -> Optional[_LoadElement]:
+ load: Optional[_LoadElement] = None
+
+ search_path = path[self]
+
+ # search among: exact match, "attr.*", "default" strategy
+ # if any.
+ for path_key in (
+ search_path._loader_key,
+ search_path._wildcard_path_loader_key,
+ search_path._default_path_loader_key,
+ ):
+ if path_key in context.attributes:
+ load = context.attributes[path_key]
+ break
+
+ # note that if strategy_options.Load is placing non-actionable
+ # objects in the context like defaultload(), we would
+ # need to continue the loop here if we got such an
+ # option as below.
+ # if load.strategy or load.local_opts:
+ # break
+
+ return load
+
+ def _get_strategy(self, key: _StrategyKey) -> LoaderStrategy:
+ try:
+ return self._strategies[key]
+ except KeyError:
+ pass
+
+ # run outside to prevent transfer of exception context
+ cls = self._strategy_lookup(self, *key)
+ # this previously was setting self._strategies[cls], that's
+ # a bad idea; should use strategy key at all times because every
+ # strategy has multiple keys at this point
+ self._strategies[key] = strategy = cls(self, key)
+ return strategy
+
+ def setup(
+ self,
+ context: ORMCompileState,
+ query_entity: _MapperEntity,
+ path: AbstractEntityRegistry,
+ adapter: Optional[ORMAdapter],
+ **kwargs: Any,
+ ) -> None:
+ loader = self._get_context_loader(context, path)
+ if loader and loader.strategy:
+ strat = self._get_strategy(loader.strategy)
+ else:
+ strat = self.strategy
+ strat.setup_query(
+ context, query_entity, path, loader, adapter, **kwargs
+ )
+
+ def create_row_processor(
+ self,
+ context: ORMCompileState,
+ query_entity: _MapperEntity,
+ path: AbstractEntityRegistry,
+ mapper: Mapper[Any],
+ result: Result[Any],
+ adapter: Optional[ORMAdapter],
+ populators: _PopulatorDict,
+ ) -> None:
+ loader = self._get_context_loader(context, path)
+ if loader and loader.strategy:
+ strat = self._get_strategy(loader.strategy)
+ else:
+ strat = self.strategy
+ strat.create_row_processor(
+ context,
+ query_entity,
+ path,
+ loader,
+ mapper,
+ result,
+ adapter,
+ populators,
+ )
+
+ def do_init(self) -> None:
+ self._strategies = {}
+ self.strategy = self._get_strategy(self.strategy_key)
+
+ def post_instrument_class(self, mapper: Mapper[Any]) -> None:
+ if (
+ not self.parent.non_primary
+ and not mapper.class_manager._attr_has_impl(self.key)
+ ):
+ self.strategy.init_class_attribute(mapper)
+
+ _all_strategies: collections.defaultdict[
+ Type[MapperProperty[Any]], Dict[_StrategyKey, Type[LoaderStrategy]]
+ ] = collections.defaultdict(dict)
+
+ @classmethod
+ def strategy_for(cls, **kw: Any) -> Callable[[_TLS], _TLS]:
+ def decorate(dec_cls: _TLS) -> _TLS:
+ # ensure each subclass of the strategy has its
+ # own _strategy_keys collection
+ if "_strategy_keys" not in dec_cls.__dict__:
+ dec_cls._strategy_keys = []
+ key = tuple(sorted(kw.items()))
+ cls._all_strategies[cls][key] = dec_cls
+ dec_cls._strategy_keys.append(key)
+ return dec_cls
+
+ return decorate
+
+ @classmethod
+ def _strategy_lookup(
+ cls, requesting_property: MapperProperty[Any], *key: Any
+ ) -> Type[LoaderStrategy]:
+ requesting_property.parent._with_polymorphic_mappers
+
+ for prop_cls in cls.__mro__:
+ if prop_cls in cls._all_strategies:
+ if TYPE_CHECKING:
+ assert issubclass(prop_cls, MapperProperty)
+ strategies = cls._all_strategies[prop_cls]
+ try:
+ return strategies[key]
+ except KeyError:
+ pass
+
+ for property_type, strats in cls._all_strategies.items():
+ if key in strats:
+ intended_property_type = property_type
+ actual_strategy = strats[key]
+ break
+ else:
+ intended_property_type = None
+ actual_strategy = None
+
+ raise orm_exc.LoaderStrategyException(
+ cls,
+ requesting_property,
+ intended_property_type,
+ actual_strategy,
+ key,
+ )
+
+
+class ORMOption(ExecutableOption):
+ """Base class for option objects that are passed to ORM queries.
+
+ These options may be consumed by :meth:`.Query.options`,
+ :meth:`.Select.options`, or in a more general sense by any
+ :meth:`.Executable.options` method. They are interpreted at
+ statement compile time or execution time in modern use. The
+ deprecated :class:`.MapperOption` is consumed at ORM query construction
+ time.
+
+ .. versionadded:: 1.4
+
+ """
+
+ __slots__ = ()
+
+ _is_legacy_option = False
+
+ propagate_to_loaders = False
+ """if True, indicate this option should be carried along
+ to "secondary" SELECT statements that occur for relationship
+ lazy loaders as well as attribute load / refresh operations.
+
+ """
+
+ _is_core = False
+
+ _is_user_defined = False
+
+ _is_compile_state = False
+
+ _is_criteria_option = False
+
+ _is_strategy_option = False
+
+ def _adapt_cached_option_to_uncached_option(
+ self, context: QueryContext, uncached_opt: ORMOption
+ ) -> ORMOption:
+ """adapt this option to the "uncached" version of itself in a
+ loader strategy context.
+
+ given "self" which is an option from a cached query, as well as the
+ corresponding option from the uncached version of the same query,
+ return the option we should use in a new query, in the context of a
+ loader strategy being asked to load related rows on behalf of that
+ cached query, which is assumed to be building a new query based on
+ entities passed to us from the cached query.
+
+ Currently this routine chooses between "self" and "uncached" without
+ manufacturing anything new. If the option is itself a loader strategy
+ option which has a path, that path needs to match to the entities being
+ passed to us by the cached query, so the :class:`_orm.Load` subclass
+ overrides this to return "self". For all other options, we return the
+ uncached form which may have changing state, such as a
+ with_loader_criteria() option which will very often have new state.
+
+ This routine could in the future involve
+ generating a new option based on both inputs if use cases arise,
+ such as if with_loader_criteria() needed to match up to
+ ``AliasedClass`` instances given in the parent query.
+
+ However, longer term it might be better to restructure things such that
+ ``AliasedClass`` entities are always matched up on their cache key,
+ instead of identity, in things like paths and such, so that this whole
+ issue of "the uncached option does not match the entities" goes away.
+ However this would make ``PathRegistry`` more complicated and difficult
+ to debug as well as potentially less performant in that it would be
+ hashing enormous cache keys rather than a simple AliasedInsp. UNLESS,
+ we could get cache keys overall to be reliably hashed into something
+ like an md5 key.
+
+ .. versionadded:: 1.4.41
+
+ """
+ if uncached_opt is not None:
+ return uncached_opt
+ else:
+ return self
+
+
+class CompileStateOption(HasCacheKey, ORMOption):
+ """base for :class:`.ORMOption` classes that affect the compilation of
+ a SQL query and therefore need to be part of the cache key.
+
+ .. note:: :class:`.CompileStateOption` is generally non-public and
+ should not be used as a base class for user-defined options; instead,
+ use :class:`.UserDefinedOption`, which is easier to use as it does not
+ interact with ORM compilation internals or caching.
+
+ :class:`.CompileStateOption` defines an internal attribute
+ ``_is_compile_state=True`` which has the effect of the ORM compilation
+ routines for SELECT and other statements will call upon these options when
+ a SQL string is being compiled. As such, these classes implement
+ :class:`.HasCacheKey` and need to provide robust ``_cache_key_traversal``
+ structures.
+
+ The :class:`.CompileStateOption` class is used to implement the ORM
+ :class:`.LoaderOption` and :class:`.CriteriaOption` classes.
+
+ .. versionadded:: 1.4.28
+
+
+ """
+
+ __slots__ = ()
+
+ _is_compile_state = True
+
+ def process_compile_state(self, compile_state: ORMCompileState) -> None:
+ """Apply a modification to a given :class:`.ORMCompileState`.
+
+ This method is part of the implementation of a particular
+ :class:`.CompileStateOption` and is only invoked internally
+ when an ORM query is compiled.
+
+ """
+
+ def process_compile_state_replaced_entities(
+ self,
+ compile_state: ORMCompileState,
+ mapper_entities: Sequence[_MapperEntity],
+ ) -> None:
+ """Apply a modification to a given :class:`.ORMCompileState`,
+ given entities that were replaced by with_only_columns() or
+ with_entities().
+
+ This method is part of the implementation of a particular
+ :class:`.CompileStateOption` and is only invoked internally
+ when an ORM query is compiled.
+
+ .. versionadded:: 1.4.19
+
+ """
+
+
+class LoaderOption(CompileStateOption):
+ """Describe a loader modification to an ORM statement at compilation time.
+
+ .. versionadded:: 1.4
+
+ """
+
+ __slots__ = ()
+
+ def process_compile_state_replaced_entities(
+ self,
+ compile_state: ORMCompileState,
+ mapper_entities: Sequence[_MapperEntity],
+ ) -> None:
+ self.process_compile_state(compile_state)
+
+
+class CriteriaOption(CompileStateOption):
+ """Describe a WHERE criteria modification to an ORM statement at
+ compilation time.
+
+ .. versionadded:: 1.4
+
+ """
+
+ __slots__ = ()
+
+ _is_criteria_option = True
+
+ def get_global_criteria(self, attributes: Dict[str, Any]) -> None:
+ """update additional entity criteria options in the given
+ attributes dictionary.
+
+ """
+
+
+class UserDefinedOption(ORMOption):
+ """Base class for a user-defined option that can be consumed from the
+ :meth:`.SessionEvents.do_orm_execute` event hook.
+
+ """
+
+ __slots__ = ("payload",)
+
+ _is_legacy_option = False
+
+ _is_user_defined = True
+
+ propagate_to_loaders = False
+ """if True, indicate this option should be carried along
+ to "secondary" Query objects produced during lazy loads
+ or refresh operations.
+
+ """
+
+ def __init__(self, payload: Optional[Any] = None):
+ self.payload = payload
+
+
+@util.deprecated_cls(
+ "1.4",
+ "The :class:`.MapperOption class is deprecated and will be removed "
+ "in a future release. For "
+ "modifications to queries on a per-execution basis, use the "
+ ":class:`.UserDefinedOption` class to establish state within a "
+ ":class:`.Query` or other Core statement, then use the "
+ ":meth:`.SessionEvents.before_orm_execute` hook to consume them.",
+ constructor=None,
+)
+class MapperOption(ORMOption):
+ """Describe a modification to a Query"""
+
+ __slots__ = ()
+
+ _is_legacy_option = True
+
+ propagate_to_loaders = False
+ """if True, indicate this option should be carried along
+ to "secondary" Query objects produced during lazy loads
+ or refresh operations.
+
+ """
+
+ def process_query(self, query: Query[Any]) -> None:
+ """Apply a modification to the given :class:`_query.Query`."""
+
+ def process_query_conditionally(self, query: Query[Any]) -> None:
+ """same as process_query(), except that this option may not
+ apply to the given query.
+
+ This is typically applied during a lazy load or scalar refresh
+ operation to propagate options stated in the original Query to the
+ new Query being used for the load. It occurs for those options that
+ specify propagate_to_loaders=True.
+
+ """
+
+ self.process_query(query)
+
+
+class LoaderStrategy:
+ """Describe the loading behavior of a StrategizedProperty object.
+
+ The ``LoaderStrategy`` interacts with the querying process in three
+ ways:
+
+ * it controls the configuration of the ``InstrumentedAttribute``
+ placed on a class to handle the behavior of the attribute. this
+ may involve setting up class-level callable functions to fire
+ off a select operation when the attribute is first accessed
+ (i.e. a lazy load)
+
+ * it processes the ``QueryContext`` at statement construction time,
+ where it can modify the SQL statement that is being produced.
+ For example, simple column attributes will add their represented
+ column to the list of selected columns, a joined eager loader
+ may establish join clauses to add to the statement.
+
+ * It produces "row processor" functions at result fetching time.
+ These "row processor" functions populate a particular attribute
+ on a particular mapped instance.
+
+ """
+
+ __slots__ = (
+ "parent_property",
+ "is_class_level",
+ "parent",
+ "key",
+ "strategy_key",
+ "strategy_opts",
+ )
+
+ _strategy_keys: ClassVar[List[_StrategyKey]]
+
+ def __init__(
+ self, parent: MapperProperty[Any], strategy_key: _StrategyKey
+ ):
+ self.parent_property = parent
+ self.is_class_level = False
+ self.parent = self.parent_property.parent
+ self.key = self.parent_property.key
+ self.strategy_key = strategy_key
+ self.strategy_opts = dict(strategy_key)
+
+ def init_class_attribute(self, mapper: Mapper[Any]) -> None:
+ pass
+
+ def setup_query(
+ self,
+ compile_state: ORMCompileState,
+ query_entity: _MapperEntity,
+ path: AbstractEntityRegistry,
+ loadopt: Optional[_LoadElement],
+ adapter: Optional[ORMAdapter],
+ **kwargs: Any,
+ ) -> None:
+ """Establish column and other state for a given QueryContext.
+
+ This method fulfills the contract specified by MapperProperty.setup().
+
+ StrategizedProperty delegates its setup() method
+ directly to this method.
+
+ """
+
+ def create_row_processor(
+ self,
+ context: ORMCompileState,
+ query_entity: _MapperEntity,
+ path: AbstractEntityRegistry,
+ loadopt: Optional[_LoadElement],
+ mapper: Mapper[Any],
+ result: Result[Any],
+ adapter: Optional[ORMAdapter],
+ populators: _PopulatorDict,
+ ) -> None:
+ """Establish row processing functions for a given QueryContext.
+
+ This method fulfills the contract specified by
+ MapperProperty.create_row_processor().
+
+ StrategizedProperty delegates its create_row_processor() method
+ directly to this method.
+
+ """
+
+ def __str__(self) -> str:
+ return str(self.parent_property)
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/loading.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/loading.py
new file mode 100644
index 0000000..4e2cb82
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/loading.py
@@ -0,0 +1,1665 @@
+# orm/loading.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: ignore-errors
+
+
+"""private module containing functions used to convert database
+rows into object instances and associated state.
+
+the functions here are called primarily by Query, Mapper,
+as well as some of the attribute loading strategies.
+
+"""
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import attributes
+from . import exc as orm_exc
+from . import path_registry
+from .base import _DEFER_FOR_STATE
+from .base import _RAISE_FOR_STATE
+from .base import _SET_DEFERRED_EXPIRED
+from .base import PassiveFlag
+from .context import FromStatement
+from .context import ORMCompileState
+from .context import QueryContext
+from .util import _none_set
+from .util import state_str
+from .. import exc as sa_exc
+from .. import util
+from ..engine import result_tuple
+from ..engine.result import ChunkedIteratorResult
+from ..engine.result import FrozenResult
+from ..engine.result import SimpleResultMetaData
+from ..sql import select
+from ..sql import util as sql_util
+from ..sql.selectable import ForUpdateArg
+from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
+from ..sql.selectable import SelectState
+from ..util import EMPTY_DICT
+
+if TYPE_CHECKING:
+ from ._typing import _IdentityKeyType
+ from .base import LoaderCallableStatus
+ from .interfaces import ORMOption
+ from .mapper import Mapper
+ from .query import Query
+ from .session import Session
+ from .state import InstanceState
+ from ..engine.cursor import CursorResult
+ from ..engine.interfaces import _ExecuteOptions
+ from ..engine.result import Result
+ from ..sql import Select
+
+_T = TypeVar("_T", bound=Any)
+_O = TypeVar("_O", bound=object)
+_new_runid = util.counter()
+
+
+_PopulatorDict = Dict[str, List[Tuple[str, Any]]]
+
+
+def instances(cursor: CursorResult[Any], context: QueryContext) -> Result[Any]:
+ """Return a :class:`.Result` given an ORM query context.
+
+ :param cursor: a :class:`.CursorResult`, generated by a statement
+ which came from :class:`.ORMCompileState`
+
+ :param context: a :class:`.QueryContext` object
+
+ :return: a :class:`.Result` object representing ORM results
+
+ .. versionchanged:: 1.4 The instances() function now uses
+ :class:`.Result` objects and has an all new interface.
+
+ """
+
+ context.runid = _new_runid()
+
+ if context.top_level_context:
+ is_top_level = False
+ context.post_load_paths = context.top_level_context.post_load_paths
+ else:
+ is_top_level = True
+ context.post_load_paths = {}
+
+ compile_state = context.compile_state
+ filtered = compile_state._has_mapper_entities
+ single_entity = (
+ not context.load_options._only_return_tuples
+ and len(compile_state._entities) == 1
+ and compile_state._entities[0].supports_single_entity
+ )
+
+ try:
+ (process, labels, extra) = list(
+ zip(
+ *[
+ query_entity.row_processor(context, cursor)
+ for query_entity in context.compile_state._entities
+ ]
+ )
+ )
+
+ if context.yield_per and (
+ context.loaders_require_buffering
+ or context.loaders_require_uniquing
+ ):
+ raise sa_exc.InvalidRequestError(
+ "Can't use yield_per with eager loaders that require uniquing "
+ "or row buffering, e.g. joinedload() against collections "
+ "or subqueryload(). Consider the selectinload() strategy "
+ "for better flexibility in loading objects."
+ )
+
+ except Exception:
+ with util.safe_reraise():
+ cursor.close()
+
+ def _no_unique(entry):
+ raise sa_exc.InvalidRequestError(
+ "Can't use the ORM yield_per feature in conjunction with unique()"
+ )
+
+ def _not_hashable(datatype, *, legacy=False, uncertain=False):
+ if not legacy:
+
+ def go(obj):
+ if uncertain:
+ try:
+ return hash(obj)
+ except:
+ pass
+
+ raise sa_exc.InvalidRequestError(
+ "Can't apply uniqueness to row tuple containing value of "
+ f"""type {datatype!r}; {
+ 'the values returned appear to be'
+ if uncertain
+ else 'this datatype produces'
+ } non-hashable values"""
+ )
+
+ return go
+ elif not uncertain:
+ return id
+ else:
+ _use_id = False
+
+ def go(obj):
+ nonlocal _use_id
+
+ if not _use_id:
+ try:
+ return hash(obj)
+ except:
+ pass
+
+ # in #10459, we considered using a warning here, however
+ # as legacy query uses result.unique() in all cases, this
+ # would lead to too many warning cases.
+ _use_id = True
+
+ return id(obj)
+
+ return go
+
+ unique_filters = [
+ (
+ _no_unique
+ if context.yield_per
+ else (
+ _not_hashable(
+ ent.column.type, # type: ignore
+ legacy=context.load_options._legacy_uniquing,
+ uncertain=ent._null_column_type,
+ )
+ if (
+ not ent.use_id_for_hash
+ and (ent._non_hashable_value or ent._null_column_type)
+ )
+ else id if ent.use_id_for_hash else None
+ )
+ )
+ for ent in context.compile_state._entities
+ ]
+
+ row_metadata = SimpleResultMetaData(
+ labels, extra, _unique_filters=unique_filters
+ )
+
+ def chunks(size): # type: ignore
+ while True:
+ yield_per = size
+
+ context.partials = {}
+
+ if yield_per:
+ fetch = cursor.fetchmany(yield_per)
+
+ if not fetch:
+ break
+ else:
+ fetch = cursor._raw_all_rows()
+
+ if single_entity:
+ proc = process[0]
+ rows = [proc(row) for row in fetch]
+ else:
+ rows = [
+ tuple([proc(row) for proc in process]) for row in fetch
+ ]
+
+ # if we are the originating load from a query, meaning we
+ # aren't being called as a result of a nested "post load",
+ # iterate through all the collected post loaders and fire them
+ # off. Previously this used to work recursively, however that
+ # prevented deeply nested structures from being loadable
+ if is_top_level:
+ if yield_per:
+ # if using yield per, memoize the state of the
+ # collection so that it can be restored
+ top_level_post_loads = list(
+ context.post_load_paths.items()
+ )
+
+ while context.post_load_paths:
+ post_loads = list(context.post_load_paths.items())
+ context.post_load_paths.clear()
+ for path, post_load in post_loads:
+ post_load.invoke(context, path)
+
+ if yield_per:
+ context.post_load_paths.clear()
+ context.post_load_paths.update(top_level_post_loads)
+
+ yield rows
+
+ if not yield_per:
+ break
+
+ if context.execution_options.get("prebuffer_rows", False):
+ # this is a bit of a hack at the moment.
+ # I would rather have some option in the result to pre-buffer
+ # internally.
+ _prebuffered = list(chunks(None))
+
+ def chunks(size):
+ return iter(_prebuffered)
+
+ result = ChunkedIteratorResult(
+ row_metadata,
+ chunks,
+ source_supports_scalars=single_entity,
+ raw=cursor,
+ dynamic_yield_per=cursor.context._is_server_side,
+ )
+
+ # filtered and single_entity are used to indicate to legacy Query that the
+ # query has ORM entities, so legacy deduping and scalars should be called
+ # on the result.
+ result._attributes = result._attributes.union(
+ dict(filtered=filtered, is_single_entity=single_entity)
+ )
+
+ # multi_row_eager_loaders OTOH is specific to joinedload.
+ if context.compile_state.multi_row_eager_loaders:
+
+ def require_unique(obj):
+ raise sa_exc.InvalidRequestError(
+ "The unique() method must be invoked on this Result, "
+ "as it contains results that include joined eager loads "
+ "against collections"
+ )
+
+ result._unique_filter_state = (None, require_unique)
+
+ if context.yield_per:
+ result.yield_per(context.yield_per)
+
+ return result
+
+
+@util.preload_module("sqlalchemy.orm.context")
+def merge_frozen_result(session, statement, frozen_result, load=True):
+ """Merge a :class:`_engine.FrozenResult` back into a :class:`_orm.Session`,
+ returning a new :class:`_engine.Result` object with :term:`persistent`
+ objects.
+
+ See the section :ref:`do_orm_execute_re_executing` for an example.
+
+ .. seealso::
+
+ :ref:`do_orm_execute_re_executing`
+
+ :meth:`_engine.Result.freeze`
+
+ :class:`_engine.FrozenResult`
+
+ """
+ querycontext = util.preloaded.orm_context
+
+ if load:
+ # flush current contents if we expect to load data
+ session._autoflush()
+
+ ctx = querycontext.ORMSelectCompileState._create_entities_collection(
+ statement, legacy=False
+ )
+
+ autoflush = session.autoflush
+ try:
+ session.autoflush = False
+ mapped_entities = [
+ i
+ for i, e in enumerate(ctx._entities)
+ if isinstance(e, querycontext._MapperEntity)
+ ]
+ keys = [ent._label_name for ent in ctx._entities]
+
+ keyed_tuple = result_tuple(
+ keys, [ent._extra_entities for ent in ctx._entities]
+ )
+
+ result = []
+ for newrow in frozen_result.rewrite_rows():
+ for i in mapped_entities:
+ if newrow[i] is not None:
+ newrow[i] = session._merge(
+ attributes.instance_state(newrow[i]),
+ attributes.instance_dict(newrow[i]),
+ load=load,
+ _recursive={},
+ _resolve_conflict_map={},
+ )
+
+ result.append(keyed_tuple(newrow))
+
+ return frozen_result.with_new_rows(result)
+ finally:
+ session.autoflush = autoflush
+
+
+@util.became_legacy_20(
+ ":func:`_orm.merge_result`",
+ alternative="The function as well as the method on :class:`_orm.Query` "
+ "is superseded by the :func:`_orm.merge_frozen_result` function.",
+)
+@util.preload_module("sqlalchemy.orm.context")
+def merge_result(
+ query: Query[Any],
+ iterator: Union[FrozenResult, Iterable[Sequence[Any]], Iterable[object]],
+ load: bool = True,
+) -> Union[FrozenResult, Iterable[Any]]:
+ """Merge a result into the given :class:`.Query` object's Session.
+
+ See :meth:`_orm.Query.merge_result` for top-level documentation on this
+ function.
+
+ """
+
+ querycontext = util.preloaded.orm_context
+
+ session = query.session
+ if load:
+ # flush current contents if we expect to load data
+ session._autoflush()
+
+ # TODO: need test coverage and documentation for the FrozenResult
+ # use case.
+ if isinstance(iterator, FrozenResult):
+ frozen_result = iterator
+ iterator = iter(frozen_result.data)
+ else:
+ frozen_result = None
+
+ ctx = querycontext.ORMSelectCompileState._create_entities_collection(
+ query, legacy=True
+ )
+
+ autoflush = session.autoflush
+ try:
+ session.autoflush = False
+ single_entity = not frozen_result and len(ctx._entities) == 1
+
+ if single_entity:
+ if isinstance(ctx._entities[0], querycontext._MapperEntity):
+ result = [
+ session._merge(
+ attributes.instance_state(instance),
+ attributes.instance_dict(instance),
+ load=load,
+ _recursive={},
+ _resolve_conflict_map={},
+ )
+ for instance in iterator
+ ]
+ else:
+ result = list(iterator)
+ else:
+ mapped_entities = [
+ i
+ for i, e in enumerate(ctx._entities)
+ if isinstance(e, querycontext._MapperEntity)
+ ]
+ result = []
+ keys = [ent._label_name for ent in ctx._entities]
+
+ keyed_tuple = result_tuple(
+ keys, [ent._extra_entities for ent in ctx._entities]
+ )
+
+ for row in iterator:
+ newrow = list(row)
+ for i in mapped_entities:
+ if newrow[i] is not None:
+ newrow[i] = session._merge(
+ attributes.instance_state(newrow[i]),
+ attributes.instance_dict(newrow[i]),
+ load=load,
+ _recursive={},
+ _resolve_conflict_map={},
+ )
+ result.append(keyed_tuple(newrow))
+
+ if frozen_result:
+ return frozen_result.with_new_rows(result)
+ else:
+ return iter(result)
+ finally:
+ session.autoflush = autoflush
+
+
+def get_from_identity(
+ session: Session,
+ mapper: Mapper[_O],
+ key: _IdentityKeyType[_O],
+ passive: PassiveFlag,
+) -> Union[LoaderCallableStatus, Optional[_O]]:
+ """Look up the given key in the given session's identity map,
+ check the object for expired state if found.
+
+ """
+ instance = session.identity_map.get(key)
+ if instance is not None:
+ state = attributes.instance_state(instance)
+
+ if mapper.inherits and not state.mapper.isa(mapper):
+ return attributes.PASSIVE_CLASS_MISMATCH
+
+ # expired - ensure it still exists
+ if state.expired:
+ if not passive & attributes.SQL_OK:
+ # TODO: no coverage here
+ return attributes.PASSIVE_NO_RESULT
+ elif not passive & attributes.RELATED_OBJECT_OK:
+ # this mode is used within a flush and the instance's
+ # expired state will be checked soon enough, if necessary.
+ # also used by immediateloader for a mutually-dependent
+ # o2m->m2m load, :ticket:`6301`
+ return instance
+ try:
+ state._load_expired(state, passive)
+ except orm_exc.ObjectDeletedError:
+ session._remove_newly_deleted([state])
+ return None
+ return instance
+ else:
+ return None
+
+
+def load_on_ident(
+ session: Session,
+ statement: Union[Select, FromStatement],
+ key: Optional[_IdentityKeyType],
+ *,
+ load_options: Optional[Sequence[ORMOption]] = None,
+ refresh_state: Optional[InstanceState[Any]] = None,
+ with_for_update: Optional[ForUpdateArg] = None,
+ only_load_props: Optional[Iterable[str]] = None,
+ no_autoflush: bool = False,
+ bind_arguments: Mapping[str, Any] = util.EMPTY_DICT,
+ execution_options: _ExecuteOptions = util.EMPTY_DICT,
+ require_pk_cols: bool = False,
+ is_user_refresh: bool = False,
+):
+ """Load the given identity key from the database."""
+ if key is not None:
+ ident = key[1]
+ identity_token = key[2]
+ else:
+ ident = identity_token = None
+
+ return load_on_pk_identity(
+ session,
+ statement,
+ ident,
+ load_options=load_options,
+ refresh_state=refresh_state,
+ with_for_update=with_for_update,
+ only_load_props=only_load_props,
+ identity_token=identity_token,
+ no_autoflush=no_autoflush,
+ bind_arguments=bind_arguments,
+ execution_options=execution_options,
+ require_pk_cols=require_pk_cols,
+ is_user_refresh=is_user_refresh,
+ )
+
+
+def load_on_pk_identity(
+ session: Session,
+ statement: Union[Select, FromStatement],
+ primary_key_identity: Optional[Tuple[Any, ...]],
+ *,
+ load_options: Optional[Sequence[ORMOption]] = None,
+ refresh_state: Optional[InstanceState[Any]] = None,
+ with_for_update: Optional[ForUpdateArg] = None,
+ only_load_props: Optional[Iterable[str]] = None,
+ identity_token: Optional[Any] = None,
+ no_autoflush: bool = False,
+ bind_arguments: Mapping[str, Any] = util.EMPTY_DICT,
+ execution_options: _ExecuteOptions = util.EMPTY_DICT,
+ require_pk_cols: bool = False,
+ is_user_refresh: bool = False,
+):
+ """Load the given primary key identity from the database."""
+
+ query = statement
+ q = query._clone()
+
+ assert not q._is_lambda_element
+
+ if load_options is None:
+ load_options = QueryContext.default_load_options
+
+ if (
+ statement._compile_options
+ is SelectState.default_select_compile_options
+ ):
+ compile_options = ORMCompileState.default_compile_options
+ else:
+ compile_options = statement._compile_options
+
+ if primary_key_identity is not None:
+ mapper = query._propagate_attrs["plugin_subject"]
+
+ (_get_clause, _get_params) = mapper._get_clause
+
+ # None present in ident - turn those comparisons
+ # into "IS NULL"
+ if None in primary_key_identity:
+ nones = {
+ _get_params[col].key
+ for col, value in zip(mapper.primary_key, primary_key_identity)
+ if value is None
+ }
+
+ _get_clause = sql_util.adapt_criterion_to_null(_get_clause, nones)
+
+ if len(nones) == len(primary_key_identity):
+ util.warn(
+ "fully NULL primary key identity cannot load any "
+ "object. This condition may raise an error in a future "
+ "release."
+ )
+
+ q._where_criteria = (
+ sql_util._deep_annotate(_get_clause, {"_orm_adapt": True}),
+ )
+
+ params = {
+ _get_params[primary_key].key: id_val
+ for id_val, primary_key in zip(
+ primary_key_identity, mapper.primary_key
+ )
+ }
+ else:
+ params = None
+
+ if with_for_update is not None:
+ version_check = True
+ q._for_update_arg = with_for_update
+ elif query._for_update_arg is not None:
+ version_check = True
+ q._for_update_arg = query._for_update_arg
+ else:
+ version_check = False
+
+ if require_pk_cols and only_load_props:
+ if not refresh_state:
+ raise sa_exc.ArgumentError(
+ "refresh_state is required when require_pk_cols is present"
+ )
+
+ refresh_state_prokeys = refresh_state.mapper._primary_key_propkeys
+ has_changes = {
+ key
+ for key in refresh_state_prokeys.difference(only_load_props)
+ if refresh_state.attrs[key].history.has_changes()
+ }
+ if has_changes:
+ # raise if pending pk changes are present.
+ # technically, this could be limited to the case where we have
+ # relationships in the only_load_props collection to be refreshed
+ # also (and only ones that have a secondary eager loader, at that).
+ # however, the error is in place across the board so that behavior
+ # here is easier to predict. The use case it prevents is one
+ # of mutating PK attrs, leaving them unflushed,
+ # calling session.refresh(), and expecting those attrs to remain
+ # still unflushed. It seems likely someone doing all those
+ # things would be better off having the PK attributes flushed
+ # to the database before tinkering like that (session.refresh() is
+ # tinkering).
+ raise sa_exc.InvalidRequestError(
+ f"Please flush pending primary key changes on "
+ "attributes "
+ f"{has_changes} for mapper {refresh_state.mapper} before "
+ "proceeding with a refresh"
+ )
+
+ # overall, the ORM has no internal flow right now for "dont load the
+ # primary row of an object at all, but fire off
+ # selectinload/subqueryload/immediateload for some relationships".
+ # It would probably be a pretty big effort to add such a flow. So
+ # here, the case for #8703 is introduced; user asks to refresh some
+ # relationship attributes only which are
+ # selectinload/subqueryload/immediateload/ etc. (not joinedload).
+ # ORM complains there's no columns in the primary row to load.
+ # So here, we just add the PK cols if that
+ # case is detected, so that there is a SELECT emitted for the primary
+ # row.
+ #
+ # Let's just state right up front, for this one little case,
+ # the ORM here is adding a whole extra SELECT just to satisfy
+ # limitations in the internal flow. This is really not a thing
+ # SQLAlchemy finds itself doing like, ever, obviously, we are
+ # constantly working to *remove* SELECTs we don't need. We
+ # rationalize this for now based on 1. session.refresh() is not
+ # commonly used 2. session.refresh() with only relationship attrs is
+ # even less commonly used 3. the SELECT in question is very low
+ # latency.
+ #
+ # to add the flow to not include the SELECT, the quickest way
+ # might be to just manufacture a single-row result set to send off to
+ # instances(), but we'd have to weave that into context.py and all
+ # that. For 2.0.0, we have enough big changes to navigate for now.
+ #
+ mp = refresh_state.mapper._props
+ for p in only_load_props:
+ if mp[p]._is_relationship:
+ only_load_props = refresh_state_prokeys.union(only_load_props)
+ break
+
+ if refresh_state and refresh_state.load_options:
+ compile_options += {"_current_path": refresh_state.load_path.parent}
+ q = q.options(*refresh_state.load_options)
+
+ new_compile_options, load_options = _set_get_options(
+ compile_options,
+ load_options,
+ version_check=version_check,
+ only_load_props=only_load_props,
+ refresh_state=refresh_state,
+ identity_token=identity_token,
+ is_user_refresh=is_user_refresh,
+ )
+
+ q._compile_options = new_compile_options
+ q._order_by = None
+
+ if no_autoflush:
+ load_options += {"_autoflush": False}
+
+ execution_options = util.EMPTY_DICT.merge_with(
+ execution_options, {"_sa_orm_load_options": load_options}
+ )
+ result = (
+ session.execute(
+ q,
+ params=params,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ )
+ .unique()
+ .scalars()
+ )
+
+ try:
+ return result.one()
+ except orm_exc.NoResultFound:
+ return None
+
+
+def _set_get_options(
+ compile_opt,
+ load_opt,
+ populate_existing=None,
+ version_check=None,
+ only_load_props=None,
+ refresh_state=None,
+ identity_token=None,
+ is_user_refresh=None,
+):
+ compile_options = {}
+ load_options = {}
+ if version_check:
+ load_options["_version_check"] = version_check
+ if populate_existing:
+ load_options["_populate_existing"] = populate_existing
+ if refresh_state:
+ load_options["_refresh_state"] = refresh_state
+ compile_options["_for_refresh_state"] = True
+ if only_load_props:
+ compile_options["_only_load_props"] = frozenset(only_load_props)
+ if identity_token:
+ load_options["_identity_token"] = identity_token
+
+ if is_user_refresh:
+ load_options["_is_user_refresh"] = is_user_refresh
+ if load_options:
+ load_opt += load_options
+ if compile_options:
+ compile_opt += compile_options
+
+ return compile_opt, load_opt
+
+
+def _setup_entity_query(
+ compile_state,
+ mapper,
+ query_entity,
+ path,
+ adapter,
+ column_collection,
+ with_polymorphic=None,
+ only_load_props=None,
+ polymorphic_discriminator=None,
+ **kw,
+):
+ if with_polymorphic:
+ poly_properties = mapper._iterate_polymorphic_properties(
+ with_polymorphic
+ )
+ else:
+ poly_properties = mapper._polymorphic_properties
+
+ quick_populators = {}
+
+ path.set(compile_state.attributes, "memoized_setups", quick_populators)
+
+ # for the lead entities in the path, e.g. not eager loads, and
+ # assuming a user-passed aliased class, e.g. not a from_self() or any
+ # implicit aliasing, don't add columns to the SELECT that aren't
+ # in the thing that's aliased.
+ check_for_adapt = adapter and len(path) == 1 and path[-1].is_aliased_class
+
+ for value in poly_properties:
+ if only_load_props and value.key not in only_load_props:
+ continue
+ value.setup(
+ compile_state,
+ query_entity,
+ path,
+ adapter,
+ only_load_props=only_load_props,
+ column_collection=column_collection,
+ memoized_populators=quick_populators,
+ check_for_adapt=check_for_adapt,
+ **kw,
+ )
+
+ if (
+ polymorphic_discriminator is not None
+ and polymorphic_discriminator is not mapper.polymorphic_on
+ ):
+ if adapter:
+ pd = adapter.columns[polymorphic_discriminator]
+ else:
+ pd = polymorphic_discriminator
+ column_collection.append(pd)
+
+
+def _warn_for_runid_changed(state):
+ util.warn(
+ "Loading context for %s has changed within a load/refresh "
+ "handler, suggesting a row refresh operation took place. If this "
+ "event handler is expected to be "
+ "emitting row refresh operations within an existing load or refresh "
+ "operation, set restore_load_context=True when establishing the "
+ "listener to ensure the context remains unchanged when the event "
+ "handler completes." % (state_str(state),)
+ )
+
+
+def _instance_processor(
+ query_entity,
+ mapper,
+ context,
+ result,
+ path,
+ adapter,
+ only_load_props=None,
+ refresh_state=None,
+ polymorphic_discriminator=None,
+ _polymorphic_from=None,
+):
+ """Produce a mapper level row processor callable
+ which processes rows into mapped instances."""
+
+ # note that this method, most of which exists in a closure
+ # called _instance(), resists being broken out, as
+ # attempts to do so tend to add significant function
+ # call overhead. _instance() is the most
+ # performance-critical section in the whole ORM.
+
+ identity_class = mapper._identity_class
+ compile_state = context.compile_state
+
+ # look for "row getter" functions that have been assigned along
+ # with the compile state that were cached from a previous load.
+ # these are operator.itemgetter() objects that each will extract a
+ # particular column from each row.
+
+ getter_key = ("getters", mapper)
+ getters = path.get(compile_state.attributes, getter_key, None)
+
+ if getters is None:
+ # no getters, so go through a list of attributes we are loading for,
+ # and the ones that are column based will have already put information
+ # for us in another collection "memoized_setups", which represents the
+ # output of the LoaderStrategy.setup_query() method. We can just as
+ # easily call LoaderStrategy.create_row_processor for each, but by
+ # getting it all at once from setup_query we save another method call
+ # per attribute.
+ props = mapper._prop_set
+ if only_load_props is not None:
+ props = props.intersection(
+ mapper._props[k] for k in only_load_props
+ )
+
+ quick_populators = path.get(
+ context.attributes, "memoized_setups", EMPTY_DICT
+ )
+
+ todo = []
+ cached_populators = {
+ "new": [],
+ "quick": [],
+ "deferred": [],
+ "expire": [],
+ "existing": [],
+ "eager": [],
+ }
+
+ if refresh_state is None:
+ # we can also get the "primary key" tuple getter function
+ pk_cols = mapper.primary_key
+
+ if adapter:
+ pk_cols = [adapter.columns[c] for c in pk_cols]
+ primary_key_getter = result._tuple_getter(pk_cols)
+ else:
+ primary_key_getter = None
+
+ getters = {
+ "cached_populators": cached_populators,
+ "todo": todo,
+ "primary_key_getter": primary_key_getter,
+ }
+ for prop in props:
+ if prop in quick_populators:
+ # this is an inlined path just for column-based attributes.
+ col = quick_populators[prop]
+ if col is _DEFER_FOR_STATE:
+ cached_populators["new"].append(
+ (prop.key, prop._deferred_column_loader)
+ )
+ elif col is _SET_DEFERRED_EXPIRED:
+ # note that in this path, we are no longer
+ # searching in the result to see if the column might
+ # be present in some unexpected way.
+ cached_populators["expire"].append((prop.key, False))
+ elif col is _RAISE_FOR_STATE:
+ cached_populators["new"].append(
+ (prop.key, prop._raise_column_loader)
+ )
+ else:
+ getter = None
+ if adapter:
+ # this logic had been removed for all 1.4 releases
+ # up until 1.4.18; the adapter here is particularly
+ # the compound eager adapter which isn't accommodated
+ # in the quick_populators right now. The "fallback"
+ # logic below instead took over in many more cases
+ # until issue #6596 was identified.
+
+ # note there is still an issue where this codepath
+ # produces no "getter" for cases where a joined-inh
+ # mapping includes a labeled column property, meaning
+ # KeyError is caught internally and we fall back to
+ # _getter(col), which works anyway. The adapter
+ # here for joined inh without any aliasing might not
+ # be useful. Tests which see this include
+ # test.orm.inheritance.test_basic ->
+ # EagerTargetingTest.test_adapt_stringency
+ # OptimizedLoadTest.test_column_expression_joined
+ # PolymorphicOnNotLocalTest.test_polymorphic_on_column_prop # noqa: E501
+ #
+
+ adapted_col = adapter.columns[col]
+ if adapted_col is not None:
+ getter = result._getter(adapted_col, False)
+ if not getter:
+ getter = result._getter(col, False)
+ if getter:
+ cached_populators["quick"].append((prop.key, getter))
+ else:
+ # fall back to the ColumnProperty itself, which
+ # will iterate through all of its columns
+ # to see if one fits
+ prop.create_row_processor(
+ context,
+ query_entity,
+ path,
+ mapper,
+ result,
+ adapter,
+ cached_populators,
+ )
+ else:
+ # loader strategies like subqueryload, selectinload,
+ # joinedload, basically relationships, these need to interact
+ # with the context each time to work correctly.
+ todo.append(prop)
+
+ path.set(compile_state.attributes, getter_key, getters)
+
+ cached_populators = getters["cached_populators"]
+
+ populators = {key: list(value) for key, value in cached_populators.items()}
+ for prop in getters["todo"]:
+ prop.create_row_processor(
+ context, query_entity, path, mapper, result, adapter, populators
+ )
+
+ propagated_loader_options = context.propagated_loader_options
+ load_path = (
+ context.compile_state.current_path + path
+ if context.compile_state.current_path.path
+ else path
+ )
+
+ session_identity_map = context.session.identity_map
+
+ populate_existing = context.populate_existing or mapper.always_refresh
+ load_evt = bool(mapper.class_manager.dispatch.load)
+ refresh_evt = bool(mapper.class_manager.dispatch.refresh)
+ persistent_evt = bool(context.session.dispatch.loaded_as_persistent)
+ if persistent_evt:
+ loaded_as_persistent = context.session.dispatch.loaded_as_persistent
+ instance_state = attributes.instance_state
+ instance_dict = attributes.instance_dict
+ session_id = context.session.hash_key
+ runid = context.runid
+ identity_token = context.identity_token
+
+ version_check = context.version_check
+ if version_check:
+ version_id_col = mapper.version_id_col
+ if version_id_col is not None:
+ if adapter:
+ version_id_col = adapter.columns[version_id_col]
+ version_id_getter = result._getter(version_id_col)
+ else:
+ version_id_getter = None
+
+ if not refresh_state and _polymorphic_from is not None:
+ key = ("loader", path.path)
+
+ if key in context.attributes and context.attributes[key].strategy == (
+ ("selectinload_polymorphic", True),
+ ):
+ option_entities = context.attributes[key].local_opts["entities"]
+ else:
+ option_entities = None
+ selectin_load_via = mapper._should_selectin_load(
+ option_entities,
+ _polymorphic_from,
+ )
+
+ if selectin_load_via and selectin_load_via is not _polymorphic_from:
+ # only_load_props goes w/ refresh_state only, and in a refresh
+ # we are a single row query for the exact entity; polymorphic
+ # loading does not apply
+ assert only_load_props is None
+
+ callable_ = _load_subclass_via_in(
+ context,
+ path,
+ selectin_load_via,
+ _polymorphic_from,
+ option_entities,
+ )
+ PostLoad.callable_for_path(
+ context,
+ load_path,
+ selectin_load_via.mapper,
+ selectin_load_via,
+ callable_,
+ selectin_load_via,
+ )
+
+ post_load = PostLoad.for_context(context, load_path, only_load_props)
+
+ if refresh_state:
+ refresh_identity_key = refresh_state.key
+ if refresh_identity_key is None:
+ # super-rare condition; a refresh is being called
+ # on a non-instance-key instance; this is meant to only
+ # occur within a flush()
+ refresh_identity_key = mapper._identity_key_from_state(
+ refresh_state
+ )
+ else:
+ refresh_identity_key = None
+
+ primary_key_getter = getters["primary_key_getter"]
+
+ if mapper.allow_partial_pks:
+ is_not_primary_key = _none_set.issuperset
+ else:
+ is_not_primary_key = _none_set.intersection
+
+ def _instance(row):
+ # determine the state that we'll be populating
+ if refresh_identity_key:
+ # fixed state that we're refreshing
+ state = refresh_state
+ instance = state.obj()
+ dict_ = instance_dict(instance)
+ isnew = state.runid != runid
+ currentload = True
+ loaded_instance = False
+ else:
+ # look at the row, see if that identity is in the
+ # session, or we have to create a new one
+ identitykey = (
+ identity_class,
+ primary_key_getter(row),
+ identity_token,
+ )
+
+ instance = session_identity_map.get(identitykey)
+
+ if instance is not None:
+ # existing instance
+ state = instance_state(instance)
+ dict_ = instance_dict(instance)
+
+ isnew = state.runid != runid
+ currentload = not isnew
+ loaded_instance = False
+
+ if version_check and version_id_getter and not currentload:
+ _validate_version_id(
+ mapper, state, dict_, row, version_id_getter
+ )
+
+ else:
+ # create a new instance
+
+ # check for non-NULL values in the primary key columns,
+ # else no entity is returned for the row
+ if is_not_primary_key(identitykey[1]):
+ return None
+
+ isnew = True
+ currentload = True
+ loaded_instance = True
+
+ instance = mapper.class_manager.new_instance()
+
+ dict_ = instance_dict(instance)
+ state = instance_state(instance)
+ state.key = identitykey
+ state.identity_token = identity_token
+
+ # attach instance to session.
+ state.session_id = session_id
+ session_identity_map._add_unpresent(state, identitykey)
+
+ effective_populate_existing = populate_existing
+ if refresh_state is state:
+ effective_populate_existing = True
+
+ # populate. this looks at whether this state is new
+ # for this load or was existing, and whether or not this
+ # row is the first row with this identity.
+ if currentload or effective_populate_existing:
+ # full population routines. Objects here are either
+ # just created, or we are doing a populate_existing
+
+ # be conservative about setting load_path when populate_existing
+ # is in effect; want to maintain options from the original
+ # load. see test_expire->test_refresh_maintains_deferred_options
+ if isnew and (
+ propagated_loader_options or not effective_populate_existing
+ ):
+ state.load_options = propagated_loader_options
+ state.load_path = load_path
+
+ _populate_full(
+ context,
+ row,
+ state,
+ dict_,
+ isnew,
+ load_path,
+ loaded_instance,
+ effective_populate_existing,
+ populators,
+ )
+
+ if isnew:
+ # state.runid should be equal to context.runid / runid
+ # here, however for event checks we are being more conservative
+ # and checking against existing run id
+ # assert state.runid == runid
+
+ existing_runid = state.runid
+
+ if loaded_instance:
+ if load_evt:
+ state.manager.dispatch.load(state, context)
+ if state.runid != existing_runid:
+ _warn_for_runid_changed(state)
+ if persistent_evt:
+ loaded_as_persistent(context.session, state)
+ if state.runid != existing_runid:
+ _warn_for_runid_changed(state)
+ elif refresh_evt:
+ state.manager.dispatch.refresh(
+ state, context, only_load_props
+ )
+ if state.runid != runid:
+ _warn_for_runid_changed(state)
+
+ if effective_populate_existing or state.modified:
+ if refresh_state and only_load_props:
+ state._commit(dict_, only_load_props)
+ else:
+ state._commit_all(dict_, session_identity_map)
+
+ if post_load:
+ post_load.add_state(state, True)
+
+ else:
+ # partial population routines, for objects that were already
+ # in the Session, but a row matches them; apply eager loaders
+ # on existing objects, etc.
+ unloaded = state.unloaded
+ isnew = state not in context.partials
+
+ if not isnew or unloaded or populators["eager"]:
+ # state is having a partial set of its attributes
+ # refreshed. Populate those attributes,
+ # and add to the "context.partials" collection.
+
+ to_load = _populate_partial(
+ context,
+ row,
+ state,
+ dict_,
+ isnew,
+ load_path,
+ unloaded,
+ populators,
+ )
+
+ if isnew:
+ if refresh_evt:
+ existing_runid = state.runid
+ state.manager.dispatch.refresh(state, context, to_load)
+ if state.runid != existing_runid:
+ _warn_for_runid_changed(state)
+
+ state._commit(dict_, to_load)
+
+ if post_load and context.invoke_all_eagers:
+ post_load.add_state(state, False)
+
+ return instance
+
+ if mapper.polymorphic_map and not _polymorphic_from and not refresh_state:
+ # if we are doing polymorphic, dispatch to a different _instance()
+ # method specific to the subclass mapper
+ def ensure_no_pk(row):
+ identitykey = (
+ identity_class,
+ primary_key_getter(row),
+ identity_token,
+ )
+ if not is_not_primary_key(identitykey[1]):
+ return identitykey
+ else:
+ return None
+
+ _instance = _decorate_polymorphic_switch(
+ _instance,
+ context,
+ query_entity,
+ mapper,
+ result,
+ path,
+ polymorphic_discriminator,
+ adapter,
+ ensure_no_pk,
+ )
+
+ return _instance
+
+
+def _load_subclass_via_in(
+ context, path, entity, polymorphic_from, option_entities
+):
+ mapper = entity.mapper
+
+ # TODO: polymorphic_from seems to be a Mapper in all cases.
+ # this is likely not needed, but as we dont have typing in loading.py
+ # yet, err on the safe side
+ polymorphic_from_mapper = polymorphic_from.mapper
+ not_against_basemost = polymorphic_from_mapper.inherits is not None
+
+ zero_idx = len(mapper.base_mapper.primary_key) == 1
+
+ if entity.is_aliased_class or not_against_basemost:
+ q, enable_opt, disable_opt = mapper._subclass_load_via_in(
+ entity, polymorphic_from
+ )
+ else:
+ q, enable_opt, disable_opt = mapper._subclass_load_via_in_mapper
+
+ def do_load(context, path, states, load_only, effective_entity):
+ if not option_entities:
+ # filter out states for those that would have selectinloaded
+ # from another loader
+ # TODO: we are currently ignoring the case where the
+ # "selectin_polymorphic" option is used, as this is much more
+ # complex / specific / very uncommon API use
+ states = [
+ (s, v)
+ for s, v in states
+ if s.mapper._would_selectin_load_only_from_given_mapper(mapper)
+ ]
+
+ if not states:
+ return
+
+ orig_query = context.query
+
+ if path.parent:
+ enable_opt_lcl = enable_opt._prepend_path(path)
+ disable_opt_lcl = disable_opt._prepend_path(path)
+ else:
+ enable_opt_lcl = enable_opt
+ disable_opt_lcl = disable_opt
+ options = (
+ (enable_opt_lcl,) + orig_query._with_options + (disable_opt_lcl,)
+ )
+
+ q2 = q.options(*options)
+
+ q2._compile_options = context.compile_state.default_compile_options
+ q2._compile_options += {"_current_path": path.parent}
+
+ if context.populate_existing:
+ q2 = q2.execution_options(populate_existing=True)
+
+ context.session.execute(
+ q2,
+ dict(
+ primary_keys=[
+ state.key[1][0] if zero_idx else state.key[1]
+ for state, load_attrs in states
+ ]
+ ),
+ ).unique().scalars().all()
+
+ return do_load
+
+
+def _populate_full(
+ context,
+ row,
+ state,
+ dict_,
+ isnew,
+ load_path,
+ loaded_instance,
+ populate_existing,
+ populators,
+):
+ if isnew:
+ # first time we are seeing a row with this identity.
+ state.runid = context.runid
+
+ for key, getter in populators["quick"]:
+ dict_[key] = getter(row)
+ if populate_existing:
+ for key, set_callable in populators["expire"]:
+ dict_.pop(key, None)
+ if set_callable:
+ state.expired_attributes.add(key)
+ else:
+ for key, set_callable in populators["expire"]:
+ if set_callable:
+ state.expired_attributes.add(key)
+
+ for key, populator in populators["new"]:
+ populator(state, dict_, row)
+
+ elif load_path != state.load_path:
+ # new load path, e.g. object is present in more than one
+ # column position in a series of rows
+ state.load_path = load_path
+
+ # if we have data, and the data isn't in the dict, OK, let's put
+ # it in.
+ for key, getter in populators["quick"]:
+ if key not in dict_:
+ dict_[key] = getter(row)
+
+ # otherwise treat like an "already seen" row
+ for key, populator in populators["existing"]:
+ populator(state, dict_, row)
+ # TODO: allow "existing" populator to know this is
+ # a new path for the state:
+ # populator(state, dict_, row, new_path=True)
+
+ else:
+ # have already seen rows with this identity in this same path.
+ for key, populator in populators["existing"]:
+ populator(state, dict_, row)
+
+ # TODO: same path
+ # populator(state, dict_, row, new_path=False)
+
+
+def _populate_partial(
+ context, row, state, dict_, isnew, load_path, unloaded, populators
+):
+ if not isnew:
+ if unloaded:
+ # extra pass, see #8166
+ for key, getter in populators["quick"]:
+ if key in unloaded:
+ dict_[key] = getter(row)
+
+ to_load = context.partials[state]
+ for key, populator in populators["existing"]:
+ if key in to_load:
+ populator(state, dict_, row)
+ else:
+ to_load = unloaded
+ context.partials[state] = to_load
+
+ for key, getter in populators["quick"]:
+ if key in to_load:
+ dict_[key] = getter(row)
+ for key, set_callable in populators["expire"]:
+ if key in to_load:
+ dict_.pop(key, None)
+ if set_callable:
+ state.expired_attributes.add(key)
+ for key, populator in populators["new"]:
+ if key in to_load:
+ populator(state, dict_, row)
+
+ for key, populator in populators["eager"]:
+ if key not in unloaded:
+ populator(state, dict_, row)
+
+ return to_load
+
+
+def _validate_version_id(mapper, state, dict_, row, getter):
+ if mapper._get_state_attr_by_column(
+ state, dict_, mapper.version_id_col
+ ) != getter(row):
+ raise orm_exc.StaleDataError(
+ "Instance '%s' has version id '%s' which "
+ "does not match database-loaded version id '%s'."
+ % (
+ state_str(state),
+ mapper._get_state_attr_by_column(
+ state, dict_, mapper.version_id_col
+ ),
+ getter(row),
+ )
+ )
+
+
+def _decorate_polymorphic_switch(
+ instance_fn,
+ context,
+ query_entity,
+ mapper,
+ result,
+ path,
+ polymorphic_discriminator,
+ adapter,
+ ensure_no_pk,
+):
+ if polymorphic_discriminator is not None:
+ polymorphic_on = polymorphic_discriminator
+ else:
+ polymorphic_on = mapper.polymorphic_on
+ if polymorphic_on is None:
+ return instance_fn
+
+ if adapter:
+ polymorphic_on = adapter.columns[polymorphic_on]
+
+ def configure_subclass_mapper(discriminator):
+ try:
+ sub_mapper = mapper.polymorphic_map[discriminator]
+ except KeyError:
+ raise AssertionError(
+ "No such polymorphic_identity %r is defined" % discriminator
+ )
+ else:
+ if sub_mapper is mapper:
+ return None
+ elif not sub_mapper.isa(mapper):
+ return False
+
+ return _instance_processor(
+ query_entity,
+ sub_mapper,
+ context,
+ result,
+ path,
+ adapter,
+ _polymorphic_from=mapper,
+ )
+
+ polymorphic_instances = util.PopulateDict(configure_subclass_mapper)
+
+ getter = result._getter(polymorphic_on)
+
+ def polymorphic_instance(row):
+ discriminator = getter(row)
+ if discriminator is not None:
+ _instance = polymorphic_instances[discriminator]
+ if _instance:
+ return _instance(row)
+ elif _instance is False:
+ identitykey = ensure_no_pk(row)
+
+ if identitykey:
+ raise sa_exc.InvalidRequestError(
+ "Row with identity key %s can't be loaded into an "
+ "object; the polymorphic discriminator column '%s' "
+ "refers to %s, which is not a sub-mapper of "
+ "the requested %s"
+ % (
+ identitykey,
+ polymorphic_on,
+ mapper.polymorphic_map[discriminator],
+ mapper,
+ )
+ )
+ else:
+ return None
+ else:
+ return instance_fn(row)
+ else:
+ identitykey = ensure_no_pk(row)
+
+ if identitykey:
+ raise sa_exc.InvalidRequestError(
+ "Row with identity key %s can't be loaded into an "
+ "object; the polymorphic discriminator column '%s' is "
+ "NULL" % (identitykey, polymorphic_on)
+ )
+ else:
+ return None
+
+ return polymorphic_instance
+
+
+class PostLoad:
+ """Track loaders and states for "post load" operations."""
+
+ __slots__ = "loaders", "states", "load_keys"
+
+ def __init__(self):
+ self.loaders = {}
+ self.states = util.OrderedDict()
+ self.load_keys = None
+
+ def add_state(self, state, overwrite):
+ # the states for a polymorphic load here are all shared
+ # within a single PostLoad object among multiple subtypes.
+ # Filtering of callables on a per-subclass basis needs to be done at
+ # the invocation level
+ self.states[state] = overwrite
+
+ def invoke(self, context, path):
+ if not self.states:
+ return
+ path = path_registry.PathRegistry.coerce(path)
+ for (
+ effective_context,
+ token,
+ limit_to_mapper,
+ loader,
+ arg,
+ kw,
+ ) in self.loaders.values():
+ states = [
+ (state, overwrite)
+ for state, overwrite in self.states.items()
+ if state.manager.mapper.isa(limit_to_mapper)
+ ]
+ if states:
+ loader(
+ effective_context, path, states, self.load_keys, *arg, **kw
+ )
+ self.states.clear()
+
+ @classmethod
+ def for_context(cls, context, path, only_load_props):
+ pl = context.post_load_paths.get(path.path)
+ if pl is not None and only_load_props:
+ pl.load_keys = only_load_props
+ return pl
+
+ @classmethod
+ def path_exists(self, context, path, key):
+ return (
+ path.path in context.post_load_paths
+ and key in context.post_load_paths[path.path].loaders
+ )
+
+ @classmethod
+ def callable_for_path(
+ cls, context, path, limit_to_mapper, token, loader_callable, *arg, **kw
+ ):
+ if path.path in context.post_load_paths:
+ pl = context.post_load_paths[path.path]
+ else:
+ pl = context.post_load_paths[path.path] = PostLoad()
+ pl.loaders[token] = (
+ context,
+ token,
+ limit_to_mapper,
+ loader_callable,
+ arg,
+ kw,
+ )
+
+
+def load_scalar_attributes(mapper, state, attribute_names, passive):
+ """initiate a column-based attribute refresh operation."""
+
+ # assert mapper is _state_mapper(state)
+ session = state.session
+ if not session:
+ raise orm_exc.DetachedInstanceError(
+ "Instance %s is not bound to a Session; "
+ "attribute refresh operation cannot proceed" % (state_str(state))
+ )
+
+ no_autoflush = bool(passive & attributes.NO_AUTOFLUSH)
+
+ # in the case of inheritance, particularly concrete and abstract
+ # concrete inheritance, the class manager might have some keys
+ # of attributes on the superclass that we didn't actually map.
+ # These could be mapped as "concrete, don't load" or could be completely
+ # excluded from the mapping and we know nothing about them. Filter them
+ # here to prevent them from coming through.
+ if attribute_names:
+ attribute_names = attribute_names.intersection(mapper.attrs.keys())
+
+ if mapper.inherits and not mapper.concrete:
+ # load based on committed attributes in the object, formed into
+ # a truncated SELECT that only includes relevant tables. does not
+ # currently use state.key
+ statement = mapper._optimized_get_statement(state, attribute_names)
+ if statement is not None:
+ # undefer() isn't needed here because statement has the
+ # columns needed already, this implicitly undefers that column
+ stmt = FromStatement(mapper, statement)
+
+ return load_on_ident(
+ session,
+ stmt,
+ None,
+ only_load_props=attribute_names,
+ refresh_state=state,
+ no_autoflush=no_autoflush,
+ )
+
+ # normal load, use state.key as the identity to SELECT
+ has_key = bool(state.key)
+
+ if has_key:
+ identity_key = state.key
+ else:
+ # this codepath is rare - only valid when inside a flush, and the
+ # object is becoming persistent but hasn't yet been assigned
+ # an identity_key.
+ # check here to ensure we have the attrs we need.
+ pk_attrs = [
+ mapper._columntoproperty[col].key for col in mapper.primary_key
+ ]
+ if state.expired_attributes.intersection(pk_attrs):
+ raise sa_exc.InvalidRequestError(
+ "Instance %s cannot be refreshed - it's not "
+ " persistent and does not "
+ "contain a full primary key." % state_str(state)
+ )
+ identity_key = mapper._identity_key_from_state(state)
+
+ if (
+ _none_set.issubset(identity_key) and not mapper.allow_partial_pks
+ ) or _none_set.issuperset(identity_key):
+ util.warn_limited(
+ "Instance %s to be refreshed doesn't "
+ "contain a full primary key - can't be refreshed "
+ "(and shouldn't be expired, either).",
+ state_str(state),
+ )
+ return
+
+ result = load_on_ident(
+ session,
+ select(mapper).set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL),
+ identity_key,
+ refresh_state=state,
+ only_load_props=attribute_names,
+ no_autoflush=no_autoflush,
+ )
+
+ # if instance is pending, a refresh operation
+ # may not complete (even if PK attributes are assigned)
+ if has_key and result is None:
+ raise orm_exc.ObjectDeletedError(state)
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapped_collection.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapped_collection.py
new file mode 100644
index 0000000..13c6b68
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapped_collection.py
@@ -0,0 +1,560 @@
+# orm/mapped_collection.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+from __future__ import annotations
+
+import operator
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Generic
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import base
+from .collections import collection
+from .collections import collection_adapter
+from .. import exc as sa_exc
+from .. import util
+from ..sql import coercions
+from ..sql import expression
+from ..sql import roles
+from ..util.typing import Literal
+
+if TYPE_CHECKING:
+ from . import AttributeEventToken
+ from . import Mapper
+ from .collections import CollectionAdapter
+ from ..sql.elements import ColumnElement
+
+_KT = TypeVar("_KT", bound=Any)
+_VT = TypeVar("_VT", bound=Any)
+
+_F = TypeVar("_F", bound=Callable[[Any], Any])
+
+
+class _PlainColumnGetter(Generic[_KT]):
+ """Plain column getter, stores collection of Column objects
+ directly.
+
+ Serializes to a :class:`._SerializableColumnGetterV2`
+ which has more expensive __call__() performance
+ and some rare caveats.
+
+ """
+
+ __slots__ = ("cols", "composite")
+
+ def __init__(self, cols: Sequence[ColumnElement[_KT]]) -> None:
+ self.cols = cols
+ self.composite = len(cols) > 1
+
+ def __reduce__(
+ self,
+ ) -> Tuple[
+ Type[_SerializableColumnGetterV2[_KT]],
+ Tuple[Sequence[Tuple[Optional[str], Optional[str]]]],
+ ]:
+ return _SerializableColumnGetterV2._reduce_from_cols(self.cols)
+
+ def _cols(self, mapper: Mapper[_KT]) -> Sequence[ColumnElement[_KT]]:
+ return self.cols
+
+ def __call__(self, value: _KT) -> Union[_KT, Tuple[_KT, ...]]:
+ state = base.instance_state(value)
+ m = base._state_mapper(state)
+
+ key: List[_KT] = [
+ m._get_state_attr_by_column(state, state.dict, col)
+ for col in self._cols(m)
+ ]
+ if self.composite:
+ return tuple(key)
+ else:
+ obj = key[0]
+ if obj is None:
+ return _UNMAPPED_AMBIGUOUS_NONE
+ else:
+ return obj
+
+
+class _SerializableColumnGetterV2(_PlainColumnGetter[_KT]):
+ """Updated serializable getter which deals with
+ multi-table mapped classes.
+
+ Two extremely unusual cases are not supported.
+ Mappings which have tables across multiple metadata
+ objects, or which are mapped to non-Table selectables
+ linked across inheriting mappers may fail to function
+ here.
+
+ """
+
+ __slots__ = ("colkeys",)
+
+ def __init__(
+ self, colkeys: Sequence[Tuple[Optional[str], Optional[str]]]
+ ) -> None:
+ self.colkeys = colkeys
+ self.composite = len(colkeys) > 1
+
+ def __reduce__(
+ self,
+ ) -> Tuple[
+ Type[_SerializableColumnGetterV2[_KT]],
+ Tuple[Sequence[Tuple[Optional[str], Optional[str]]]],
+ ]:
+ return self.__class__, (self.colkeys,)
+
+ @classmethod
+ def _reduce_from_cols(cls, cols: Sequence[ColumnElement[_KT]]) -> Tuple[
+ Type[_SerializableColumnGetterV2[_KT]],
+ Tuple[Sequence[Tuple[Optional[str], Optional[str]]]],
+ ]:
+ def _table_key(c: ColumnElement[_KT]) -> Optional[str]:
+ if not isinstance(c.table, expression.TableClause):
+ return None
+ else:
+ return c.table.key # type: ignore
+
+ colkeys = [(c.key, _table_key(c)) for c in cols]
+ return _SerializableColumnGetterV2, (colkeys,)
+
+ def _cols(self, mapper: Mapper[_KT]) -> Sequence[ColumnElement[_KT]]:
+ cols: List[ColumnElement[_KT]] = []
+ metadata = getattr(mapper.local_table, "metadata", None)
+ for ckey, tkey in self.colkeys:
+ if tkey is None or metadata is None or tkey not in metadata:
+ cols.append(mapper.local_table.c[ckey]) # type: ignore
+ else:
+ cols.append(metadata.tables[tkey].c[ckey])
+ return cols
+
+
+def column_keyed_dict(
+ mapping_spec: Union[Type[_KT], Callable[[_KT], _VT]],
+ *,
+ ignore_unpopulated_attribute: bool = False,
+) -> Type[KeyFuncDict[_KT, _KT]]:
+ """A dictionary-based collection type with column-based keying.
+
+ .. versionchanged:: 2.0 Renamed :data:`.column_mapped_collection` to
+ :class:`.column_keyed_dict`.
+
+ Returns a :class:`.KeyFuncDict` factory which will produce new
+ dictionary keys based on the value of a particular :class:`.Column`-mapped
+ attribute on ORM mapped instances to be added to the dictionary.
+
+ .. note:: the value of the target attribute must be assigned with its
+ value at the time that the object is being added to the
+ dictionary collection. Additionally, changes to the key attribute
+ are **not tracked**, which means the key in the dictionary is not
+ automatically synchronized with the key value on the target object
+ itself. See :ref:`key_collections_mutations` for further details.
+
+ .. seealso::
+
+ :ref:`orm_dictionary_collection` - background on use
+
+ :param mapping_spec: a :class:`_schema.Column` object that is expected
+ to be mapped by the target mapper to a particular attribute on the
+ mapped class, the value of which on a particular instance is to be used
+ as the key for a new dictionary entry for that instance.
+ :param ignore_unpopulated_attribute: if True, and the mapped attribute
+ indicated by the given :class:`_schema.Column` target attribute
+ on an object is not populated at all, the operation will be silently
+ skipped. By default, an error is raised.
+
+ .. versionadded:: 2.0 an error is raised by default if the attribute
+ being used for the dictionary key is determined that it was never
+ populated with any value. The
+ :paramref:`_orm.column_keyed_dict.ignore_unpopulated_attribute`
+ parameter may be set which will instead indicate that this condition
+ should be ignored, and the append operation silently skipped.
+ This is in contrast to the behavior of the 1.x series which would
+ erroneously populate the value in the dictionary with an arbitrary key
+ value of ``None``.
+
+
+ """
+ cols = [
+ coercions.expect(roles.ColumnArgumentRole, q, argname="mapping_spec")
+ for q in util.to_list(mapping_spec)
+ ]
+ keyfunc = _PlainColumnGetter(cols)
+ return _mapped_collection_cls(
+ keyfunc,
+ ignore_unpopulated_attribute=ignore_unpopulated_attribute,
+ )
+
+
+_UNMAPPED_AMBIGUOUS_NONE = object()
+
+
+class _AttrGetter:
+ __slots__ = ("attr_name", "getter")
+
+ def __init__(self, attr_name: str):
+ self.attr_name = attr_name
+ self.getter = operator.attrgetter(attr_name)
+
+ def __call__(self, mapped_object: Any) -> Any:
+ obj = self.getter(mapped_object)
+ if obj is None:
+ state = base.instance_state(mapped_object)
+ mp = state.mapper
+ if self.attr_name in mp.attrs:
+ dict_ = state.dict
+ obj = dict_.get(self.attr_name, base.NO_VALUE)
+ if obj is None:
+ return _UNMAPPED_AMBIGUOUS_NONE
+ else:
+ return _UNMAPPED_AMBIGUOUS_NONE
+
+ return obj
+
+ def __reduce__(self) -> Tuple[Type[_AttrGetter], Tuple[str]]:
+ return _AttrGetter, (self.attr_name,)
+
+
+def attribute_keyed_dict(
+ attr_name: str, *, ignore_unpopulated_attribute: bool = False
+) -> Type[KeyFuncDict[Any, Any]]:
+ """A dictionary-based collection type with attribute-based keying.
+
+ .. versionchanged:: 2.0 Renamed :data:`.attribute_mapped_collection` to
+ :func:`.attribute_keyed_dict`.
+
+ Returns a :class:`.KeyFuncDict` factory which will produce new
+ dictionary keys based on the value of a particular named attribute on
+ ORM mapped instances to be added to the dictionary.
+
+ .. note:: the value of the target attribute must be assigned with its
+ value at the time that the object is being added to the
+ dictionary collection. Additionally, changes to the key attribute
+ are **not tracked**, which means the key in the dictionary is not
+ automatically synchronized with the key value on the target object
+ itself. See :ref:`key_collections_mutations` for further details.
+
+ .. seealso::
+
+ :ref:`orm_dictionary_collection` - background on use
+
+ :param attr_name: string name of an ORM-mapped attribute
+ on the mapped class, the value of which on a particular instance
+ is to be used as the key for a new dictionary entry for that instance.
+ :param ignore_unpopulated_attribute: if True, and the target attribute
+ on an object is not populated at all, the operation will be silently
+ skipped. By default, an error is raised.
+
+ .. versionadded:: 2.0 an error is raised by default if the attribute
+ being used for the dictionary key is determined that it was never
+ populated with any value. The
+ :paramref:`_orm.attribute_keyed_dict.ignore_unpopulated_attribute`
+ parameter may be set which will instead indicate that this condition
+ should be ignored, and the append operation silently skipped.
+ This is in contrast to the behavior of the 1.x series which would
+ erroneously populate the value in the dictionary with an arbitrary key
+ value of ``None``.
+
+
+ """
+
+ return _mapped_collection_cls(
+ _AttrGetter(attr_name),
+ ignore_unpopulated_attribute=ignore_unpopulated_attribute,
+ )
+
+
+def keyfunc_mapping(
+ keyfunc: _F,
+ *,
+ ignore_unpopulated_attribute: bool = False,
+) -> Type[KeyFuncDict[_KT, Any]]:
+ """A dictionary-based collection type with arbitrary keying.
+
+ .. versionchanged:: 2.0 Renamed :data:`.mapped_collection` to
+ :func:`.keyfunc_mapping`.
+
+ Returns a :class:`.KeyFuncDict` factory with a keying function
+ generated from keyfunc, a callable that takes an entity and returns a
+ key value.
+
+ .. note:: the given keyfunc is called only once at the time that the
+ target object is being added to the collection. Changes to the
+ effective value returned by the function are not tracked.
+
+
+ .. seealso::
+
+ :ref:`orm_dictionary_collection` - background on use
+
+ :param keyfunc: a callable that will be passed the ORM-mapped instance
+ which should then generate a new key to use in the dictionary.
+ If the value returned is :attr:`.LoaderCallableStatus.NO_VALUE`, an error
+ is raised.
+ :param ignore_unpopulated_attribute: if True, and the callable returns
+ :attr:`.LoaderCallableStatus.NO_VALUE` for a particular instance, the
+ operation will be silently skipped. By default, an error is raised.
+
+ .. versionadded:: 2.0 an error is raised by default if the callable
+ being used for the dictionary key returns
+ :attr:`.LoaderCallableStatus.NO_VALUE`, which in an ORM attribute
+ context indicates an attribute that was never populated with any value.
+ The :paramref:`_orm.mapped_collection.ignore_unpopulated_attribute`
+ parameter may be set which will instead indicate that this condition
+ should be ignored, and the append operation silently skipped. This is
+ in contrast to the behavior of the 1.x series which would erroneously
+ populate the value in the dictionary with an arbitrary key value of
+ ``None``.
+
+
+ """
+ return _mapped_collection_cls(
+ keyfunc, ignore_unpopulated_attribute=ignore_unpopulated_attribute
+ )
+
+
+class KeyFuncDict(Dict[_KT, _VT]):
+ """Base for ORM mapped dictionary classes.
+
+ Extends the ``dict`` type with additional methods needed by SQLAlchemy ORM
+ collection classes. Use of :class:`_orm.KeyFuncDict` is most directly
+ by using the :func:`.attribute_keyed_dict` or
+ :func:`.column_keyed_dict` class factories.
+ :class:`_orm.KeyFuncDict` may also serve as the base for user-defined
+ custom dictionary classes.
+
+ .. versionchanged:: 2.0 Renamed :class:`.MappedCollection` to
+ :class:`.KeyFuncDict`.
+
+ .. seealso::
+
+ :func:`_orm.attribute_keyed_dict`
+
+ :func:`_orm.column_keyed_dict`
+
+ :ref:`orm_dictionary_collection`
+
+ :ref:`orm_custom_collection`
+
+
+ """
+
+ def __init__(
+ self,
+ keyfunc: _F,
+ *dict_args: Any,
+ ignore_unpopulated_attribute: bool = False,
+ ) -> None:
+ """Create a new collection with keying provided by keyfunc.
+
+ keyfunc may be any callable that takes an object and returns an object
+ for use as a dictionary key.
+
+ The keyfunc will be called every time the ORM needs to add a member by
+ value-only (such as when loading instances from the database) or
+ remove a member. The usual cautions about dictionary keying apply-
+ ``keyfunc(object)`` should return the same output for the life of the
+ collection. Keying based on mutable properties can result in
+ unreachable instances "lost" in the collection.
+
+ """
+ self.keyfunc = keyfunc
+ self.ignore_unpopulated_attribute = ignore_unpopulated_attribute
+ super().__init__(*dict_args)
+
+ @classmethod
+ def _unreduce(
+ cls,
+ keyfunc: _F,
+ values: Dict[_KT, _KT],
+ adapter: Optional[CollectionAdapter] = None,
+ ) -> "KeyFuncDict[_KT, _KT]":
+ mp: KeyFuncDict[_KT, _KT] = KeyFuncDict(keyfunc)
+ mp.update(values)
+ # note that the adapter sets itself up onto this collection
+ # when its `__setstate__` method is called
+ return mp
+
+ def __reduce__(
+ self,
+ ) -> Tuple[
+ Callable[[_KT, _KT], KeyFuncDict[_KT, _KT]],
+ Tuple[Any, Union[Dict[_KT, _KT], Dict[_KT, _KT]], CollectionAdapter],
+ ]:
+ return (
+ KeyFuncDict._unreduce,
+ (
+ self.keyfunc,
+ dict(self),
+ collection_adapter(self),
+ ),
+ )
+
+ @util.preload_module("sqlalchemy.orm.attributes")
+ def _raise_for_unpopulated(
+ self,
+ value: _KT,
+ initiator: Union[AttributeEventToken, Literal[None, False]] = None,
+ *,
+ warn_only: bool,
+ ) -> None:
+ mapper = base.instance_state(value).mapper
+
+ attributes = util.preloaded.orm_attributes
+
+ if not isinstance(initiator, attributes.AttributeEventToken):
+ relationship = "unknown relationship"
+ elif initiator.key in mapper.attrs:
+ relationship = f"{mapper.attrs[initiator.key]}"
+ else:
+ relationship = initiator.key
+
+ if warn_only:
+ util.warn(
+ f"Attribute keyed dictionary value for "
+ f"attribute '{relationship}' was None; this will raise "
+ "in a future release. "
+ f"To skip this assignment entirely, "
+ f'Set the "ignore_unpopulated_attribute=True" '
+ f"parameter on the mapped collection factory."
+ )
+ else:
+ raise sa_exc.InvalidRequestError(
+ "In event triggered from population of "
+ f"attribute '{relationship}' "
+ "(potentially from a backref), "
+ f"can't populate value in KeyFuncDict; "
+ "dictionary key "
+ f"derived from {base.instance_str(value)} is not "
+ f"populated. Ensure appropriate state is set up on "
+ f"the {base.instance_str(value)} object "
+ f"before assigning to the {relationship} attribute. "
+ f"To skip this assignment entirely, "
+ f'Set the "ignore_unpopulated_attribute=True" '
+ f"parameter on the mapped collection factory."
+ )
+
+ @collection.appender # type: ignore[misc]
+ @collection.internally_instrumented # type: ignore[misc]
+ def set(
+ self,
+ value: _KT,
+ _sa_initiator: Union[AttributeEventToken, Literal[None, False]] = None,
+ ) -> None:
+ """Add an item by value, consulting the keyfunc for the key."""
+
+ key = self.keyfunc(value)
+
+ if key is base.NO_VALUE:
+ if not self.ignore_unpopulated_attribute:
+ self._raise_for_unpopulated(
+ value, _sa_initiator, warn_only=False
+ )
+ else:
+ return
+ elif key is _UNMAPPED_AMBIGUOUS_NONE:
+ if not self.ignore_unpopulated_attribute:
+ self._raise_for_unpopulated(
+ value, _sa_initiator, warn_only=True
+ )
+ key = None
+ else:
+ return
+
+ self.__setitem__(key, value, _sa_initiator) # type: ignore[call-arg]
+
+ @collection.remover # type: ignore[misc]
+ @collection.internally_instrumented # type: ignore[misc]
+ def remove(
+ self,
+ value: _KT,
+ _sa_initiator: Union[AttributeEventToken, Literal[None, False]] = None,
+ ) -> None:
+ """Remove an item by value, consulting the keyfunc for the key."""
+
+ key = self.keyfunc(value)
+
+ if key is base.NO_VALUE:
+ if not self.ignore_unpopulated_attribute:
+ self._raise_for_unpopulated(
+ value, _sa_initiator, warn_only=False
+ )
+ return
+ elif key is _UNMAPPED_AMBIGUOUS_NONE:
+ if not self.ignore_unpopulated_attribute:
+ self._raise_for_unpopulated(
+ value, _sa_initiator, warn_only=True
+ )
+ key = None
+ else:
+ return
+
+ # Let self[key] raise if key is not in this collection
+ # testlib.pragma exempt:__ne__
+ if self[key] != value:
+ raise sa_exc.InvalidRequestError(
+ "Can not remove '%s': collection holds '%s' for key '%s'. "
+ "Possible cause: is the KeyFuncDict key function "
+ "based on mutable properties or properties that only obtain "
+ "values after flush?" % (value, self[key], key)
+ )
+ self.__delitem__(key, _sa_initiator) # type: ignore[call-arg]
+
+
+def _mapped_collection_cls(
+ keyfunc: _F, ignore_unpopulated_attribute: bool
+) -> Type[KeyFuncDict[_KT, _KT]]:
+ class _MKeyfuncMapped(KeyFuncDict[_KT, _KT]):
+ def __init__(self, *dict_args: Any) -> None:
+ super().__init__(
+ keyfunc,
+ *dict_args,
+ ignore_unpopulated_attribute=ignore_unpopulated_attribute,
+ )
+
+ return _MKeyfuncMapped
+
+
+MappedCollection = KeyFuncDict
+"""A synonym for :class:`.KeyFuncDict`.
+
+.. versionchanged:: 2.0 Renamed :class:`.MappedCollection` to
+ :class:`.KeyFuncDict`.
+
+"""
+
+mapped_collection = keyfunc_mapping
+"""A synonym for :func:`_orm.keyfunc_mapping`.
+
+.. versionchanged:: 2.0 Renamed :data:`.mapped_collection` to
+ :func:`_orm.keyfunc_mapping`
+
+"""
+
+attribute_mapped_collection = attribute_keyed_dict
+"""A synonym for :func:`_orm.attribute_keyed_dict`.
+
+.. versionchanged:: 2.0 Renamed :data:`.attribute_mapped_collection` to
+ :func:`_orm.attribute_keyed_dict`
+
+"""
+
+column_mapped_collection = column_keyed_dict
+"""A synonym for :func:`_orm.column_keyed_dict.
+
+.. versionchanged:: 2.0 Renamed :func:`.column_mapped_collection` to
+ :func:`_orm.column_keyed_dict`
+
+"""
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py
new file mode 100644
index 0000000..0caed0e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py
@@ -0,0 +1,4420 @@
+# orm/mapper.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: allow-untyped-defs, allow-untyped-calls
+
+"""Logic to map Python classes to and from selectables.
+
+Defines the :class:`~sqlalchemy.orm.mapper.Mapper` class, the central
+configurational unit which associates a class with a database table.
+
+This is a semi-private module; the main configurational API of the ORM is
+available in :class:`~sqlalchemy.orm.`.
+
+"""
+from __future__ import annotations
+
+from collections import deque
+from functools import reduce
+from itertools import chain
+import sys
+import threading
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Collection
+from typing import Deque
+from typing import Dict
+from typing import FrozenSet
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import attributes
+from . import exc as orm_exc
+from . import instrumentation
+from . import loading
+from . import properties
+from . import util as orm_util
+from ._typing import _O
+from .base import _class_to_mapper
+from .base import _parse_mapper_argument
+from .base import _state_mapper
+from .base import PassiveFlag
+from .base import state_str
+from .interfaces import _MappedAttribute
+from .interfaces import EXT_SKIP
+from .interfaces import InspectionAttr
+from .interfaces import MapperProperty
+from .interfaces import ORMEntityColumnsClauseRole
+from .interfaces import ORMFromClauseRole
+from .interfaces import StrategizedProperty
+from .path_registry import PathRegistry
+from .. import event
+from .. import exc as sa_exc
+from .. import inspection
+from .. import log
+from .. import schema
+from .. import sql
+from .. import util
+from ..event import dispatcher
+from ..event import EventTarget
+from ..sql import base as sql_base
+from ..sql import coercions
+from ..sql import expression
+from ..sql import operators
+from ..sql import roles
+from ..sql import TableClause
+from ..sql import util as sql_util
+from ..sql import visitors
+from ..sql.cache_key import MemoizedHasCacheKey
+from ..sql.elements import KeyedColumnElement
+from ..sql.schema import Column
+from ..sql.schema import Table
+from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
+from ..util import HasMemoized
+from ..util import HasMemoized_ro_memoized_attribute
+from ..util.typing import Literal
+
+if TYPE_CHECKING:
+ from ._typing import _IdentityKeyType
+ from ._typing import _InstanceDict
+ from ._typing import _ORMColumnExprArgument
+ from ._typing import _RegistryType
+ from .decl_api import registry
+ from .dependency import DependencyProcessor
+ from .descriptor_props import CompositeProperty
+ from .descriptor_props import SynonymProperty
+ from .events import MapperEvents
+ from .instrumentation import ClassManager
+ from .path_registry import CachingEntityRegistry
+ from .properties import ColumnProperty
+ from .relationships import RelationshipProperty
+ from .state import InstanceState
+ from .util import ORMAdapter
+ from ..engine import Row
+ from ..engine import RowMapping
+ from ..sql._typing import _ColumnExpressionArgument
+ from ..sql._typing import _EquivalentColumnMap
+ from ..sql.base import ReadOnlyColumnCollection
+ from ..sql.elements import ColumnClause
+ from ..sql.elements import ColumnElement
+ from ..sql.selectable import FromClause
+ from ..util import OrderedSet
+
+
+_T = TypeVar("_T", bound=Any)
+_MP = TypeVar("_MP", bound="MapperProperty[Any]")
+_Fn = TypeVar("_Fn", bound="Callable[..., Any]")
+
+
+_WithPolymorphicArg = Union[
+ Literal["*"],
+ Tuple[
+ Union[Literal["*"], Sequence[Union["Mapper[Any]", Type[Any]]]],
+ Optional["FromClause"],
+ ],
+ Sequence[Union["Mapper[Any]", Type[Any]]],
+]
+
+
+_mapper_registries: weakref.WeakKeyDictionary[_RegistryType, bool] = (
+ weakref.WeakKeyDictionary()
+)
+
+
+def _all_registries() -> Set[registry]:
+ with _CONFIGURE_MUTEX:
+ return set(_mapper_registries)
+
+
+def _unconfigured_mappers() -> Iterator[Mapper[Any]]:
+ for reg in _all_registries():
+ yield from reg._mappers_to_configure()
+
+
+_already_compiling = False
+
+
+# a constant returned by _get_attr_by_column to indicate
+# this mapper is not handling an attribute for a particular
+# column
+NO_ATTRIBUTE = util.symbol("NO_ATTRIBUTE")
+
+# lock used to synchronize the "mapper configure" step
+_CONFIGURE_MUTEX = threading.RLock()
+
+
+@inspection._self_inspects
+@log.class_logger
+class Mapper(
+ ORMFromClauseRole,
+ ORMEntityColumnsClauseRole[_O],
+ MemoizedHasCacheKey,
+ InspectionAttr,
+ log.Identified,
+ inspection.Inspectable["Mapper[_O]"],
+ EventTarget,
+ Generic[_O],
+):
+ """Defines an association between a Python class and a database table or
+ other relational structure, so that ORM operations against the class may
+ proceed.
+
+ The :class:`_orm.Mapper` object is instantiated using mapping methods
+ present on the :class:`_orm.registry` object. For information
+ about instantiating new :class:`_orm.Mapper` objects, see
+ :ref:`orm_mapping_classes_toplevel`.
+
+ """
+
+ dispatch: dispatcher[Mapper[_O]]
+
+ _dispose_called = False
+ _configure_failed: Any = False
+ _ready_for_configure = False
+
+ @util.deprecated_params(
+ non_primary=(
+ "1.3",
+ "The :paramref:`.mapper.non_primary` parameter is deprecated, "
+ "and will be removed in a future release. The functionality "
+ "of non primary mappers is now better suited using the "
+ ":class:`.AliasedClass` construct, which can also be used "
+ "as the target of a :func:`_orm.relationship` in 1.3.",
+ ),
+ )
+ def __init__(
+ self,
+ class_: Type[_O],
+ local_table: Optional[FromClause] = None,
+ properties: Optional[Mapping[str, MapperProperty[Any]]] = None,
+ primary_key: Optional[Iterable[_ORMColumnExprArgument[Any]]] = None,
+ non_primary: bool = False,
+ inherits: Optional[Union[Mapper[Any], Type[Any]]] = None,
+ inherit_condition: Optional[_ColumnExpressionArgument[bool]] = None,
+ inherit_foreign_keys: Optional[
+ Sequence[_ORMColumnExprArgument[Any]]
+ ] = None,
+ always_refresh: bool = False,
+ version_id_col: Optional[_ORMColumnExprArgument[Any]] = None,
+ version_id_generator: Optional[
+ Union[Literal[False], Callable[[Any], Any]]
+ ] = None,
+ polymorphic_on: Optional[
+ Union[_ORMColumnExprArgument[Any], str, MapperProperty[Any]]
+ ] = None,
+ _polymorphic_map: Optional[Dict[Any, Mapper[Any]]] = None,
+ polymorphic_identity: Optional[Any] = None,
+ concrete: bool = False,
+ with_polymorphic: Optional[_WithPolymorphicArg] = None,
+ polymorphic_abstract: bool = False,
+ polymorphic_load: Optional[Literal["selectin", "inline"]] = None,
+ allow_partial_pks: bool = True,
+ batch: bool = True,
+ column_prefix: Optional[str] = None,
+ include_properties: Optional[Sequence[str]] = None,
+ exclude_properties: Optional[Sequence[str]] = None,
+ passive_updates: bool = True,
+ passive_deletes: bool = False,
+ confirm_deleted_rows: bool = True,
+ eager_defaults: Literal[True, False, "auto"] = "auto",
+ legacy_is_orphan: bool = False,
+ _compiled_cache_size: int = 100,
+ ):
+ r"""Direct constructor for a new :class:`_orm.Mapper` object.
+
+ The :class:`_orm.Mapper` constructor is not called directly, and
+ is normally invoked through the
+ use of the :class:`_orm.registry` object through either the
+ :ref:`Declarative <orm_declarative_mapping>` or
+ :ref:`Imperative <orm_imperative_mapping>` mapping styles.
+
+ .. versionchanged:: 2.0 The public facing ``mapper()`` function is
+ removed; for a classical mapping configuration, use the
+ :meth:`_orm.registry.map_imperatively` method.
+
+ Parameters documented below may be passed to either the
+ :meth:`_orm.registry.map_imperatively` method, or may be passed in the
+ ``__mapper_args__`` declarative class attribute described at
+ :ref:`orm_declarative_mapper_options`.
+
+ :param class\_: The class to be mapped. When using Declarative,
+ this argument is automatically passed as the declared class
+ itself.
+
+ :param local_table: The :class:`_schema.Table` or other
+ :class:`_sql.FromClause` (i.e. selectable) to which the class is
+ mapped. May be ``None`` if this mapper inherits from another mapper
+ using single-table inheritance. When using Declarative, this
+ argument is automatically passed by the extension, based on what is
+ configured via the :attr:`_orm.DeclarativeBase.__table__` attribute
+ or via the :class:`_schema.Table` produced as a result of
+ the :attr:`_orm.DeclarativeBase.__tablename__` attribute being
+ present.
+
+ :param polymorphic_abstract: Indicates this class will be mapped in a
+ polymorphic hierarchy, but not directly instantiated. The class is
+ mapped normally, except that it has no requirement for a
+ :paramref:`_orm.Mapper.polymorphic_identity` within an inheritance
+ hierarchy. The class however must be part of a polymorphic
+ inheritance scheme which uses
+ :paramref:`_orm.Mapper.polymorphic_on` at the base.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`orm_inheritance_abstract_poly`
+
+ :param always_refresh: If True, all query operations for this mapped
+ class will overwrite all data within object instances that already
+ exist within the session, erasing any in-memory changes with
+ whatever information was loaded from the database. Usage of this
+ flag is highly discouraged; as an alternative, see the method
+ :meth:`_query.Query.populate_existing`.
+
+ :param allow_partial_pks: Defaults to True. Indicates that a
+ composite primary key with some NULL values should be considered as
+ possibly existing within the database. This affects whether a
+ mapper will assign an incoming row to an existing identity, as well
+ as if :meth:`.Session.merge` will check the database first for a
+ particular primary key value. A "partial primary key" can occur if
+ one has mapped to an OUTER JOIN, for example.
+
+ :param batch: Defaults to ``True``, indicating that save operations
+ of multiple entities can be batched together for efficiency.
+ Setting to False indicates
+ that an instance will be fully saved before saving the next
+ instance. This is used in the extremely rare case that a
+ :class:`.MapperEvents` listener requires being called
+ in between individual row persistence operations.
+
+ :param column_prefix: A string which will be prepended
+ to the mapped attribute name when :class:`_schema.Column`
+ objects are automatically assigned as attributes to the
+ mapped class. Does not affect :class:`.Column` objects that
+ are mapped explicitly in the :paramref:`.Mapper.properties`
+ dictionary.
+
+ This parameter is typically useful with imperative mappings
+ that keep the :class:`.Table` object separate. Below, assuming
+ the ``user_table`` :class:`.Table` object has columns named
+ ``user_id``, ``user_name``, and ``password``::
+
+ class User(Base):
+ __table__ = user_table
+ __mapper_args__ = {'column_prefix':'_'}
+
+ The above mapping will assign the ``user_id``, ``user_name``, and
+ ``password`` columns to attributes named ``_user_id``,
+ ``_user_name``, and ``_password`` on the mapped ``User`` class.
+
+ The :paramref:`.Mapper.column_prefix` parameter is uncommon in
+ modern use. For dealing with reflected tables, a more flexible
+ approach to automating a naming scheme is to intercept the
+ :class:`.Column` objects as they are reflected; see the section
+ :ref:`mapper_automated_reflection_schemes` for notes on this usage
+ pattern.
+
+ :param concrete: If True, indicates this mapper should use concrete
+ table inheritance with its parent mapper.
+
+ See the section :ref:`concrete_inheritance` for an example.
+
+ :param confirm_deleted_rows: defaults to True; when a DELETE occurs
+ of one more rows based on specific primary keys, a warning is
+ emitted when the number of rows matched does not equal the number
+ of rows expected. This parameter may be set to False to handle the
+ case where database ON DELETE CASCADE rules may be deleting some of
+ those rows automatically. The warning may be changed to an
+ exception in a future release.
+
+ :param eager_defaults: if True, the ORM will immediately fetch the
+ value of server-generated default values after an INSERT or UPDATE,
+ rather than leaving them as expired to be fetched on next access.
+ This can be used for event schemes where the server-generated values
+ are needed immediately before the flush completes.
+
+ The fetch of values occurs either by using ``RETURNING`` inline
+ with the ``INSERT`` or ``UPDATE`` statement, or by adding an
+ additional ``SELECT`` statement subsequent to the ``INSERT`` or
+ ``UPDATE``, if the backend does not support ``RETURNING``.
+
+ The use of ``RETURNING`` is extremely performant in particular for
+ ``INSERT`` statements where SQLAlchemy can take advantage of
+ :ref:`insertmanyvalues <engine_insertmanyvalues>`, whereas the use of
+ an additional ``SELECT`` is relatively poor performing, adding
+ additional SQL round trips which would be unnecessary if these new
+ attributes are not to be accessed in any case.
+
+ For this reason, :paramref:`.Mapper.eager_defaults` defaults to the
+ string value ``"auto"``, which indicates that server defaults for
+ INSERT should be fetched using ``RETURNING`` if the backing database
+ supports it and if the dialect in use supports "insertmanyreturning"
+ for an INSERT statement. If the backing database does not support
+ ``RETURNING`` or "insertmanyreturning" is not available, server
+ defaults will not be fetched.
+
+ .. versionchanged:: 2.0.0rc1 added the "auto" option for
+ :paramref:`.Mapper.eager_defaults`
+
+ .. seealso::
+
+ :ref:`orm_server_defaults`
+
+ .. versionchanged:: 2.0.0 RETURNING now works with multiple rows
+ INSERTed at once using the
+ :ref:`insertmanyvalues <engine_insertmanyvalues>` feature, which
+ among other things allows the :paramref:`.Mapper.eager_defaults`
+ feature to be very performant on supporting backends.
+
+ :param exclude_properties: A list or set of string column names to
+ be excluded from mapping.
+
+ .. seealso::
+
+ :ref:`include_exclude_cols`
+
+ :param include_properties: An inclusive list or set of string column
+ names to map.
+
+ .. seealso::
+
+ :ref:`include_exclude_cols`
+
+ :param inherits: A mapped class or the corresponding
+ :class:`_orm.Mapper`
+ of one indicating a superclass to which this :class:`_orm.Mapper`
+ should *inherit* from. The mapped class here must be a subclass
+ of the other mapper's class. When using Declarative, this argument
+ is passed automatically as a result of the natural class
+ hierarchy of the declared classes.
+
+ .. seealso::
+
+ :ref:`inheritance_toplevel`
+
+ :param inherit_condition: For joined table inheritance, a SQL
+ expression which will
+ define how the two tables are joined; defaults to a natural join
+ between the two tables.
+
+ :param inherit_foreign_keys: When ``inherit_condition`` is used and
+ the columns present are missing a :class:`_schema.ForeignKey`
+ configuration, this parameter can be used to specify which columns
+ are "foreign". In most cases can be left as ``None``.
+
+ :param legacy_is_orphan: Boolean, defaults to ``False``.
+ When ``True``, specifies that "legacy" orphan consideration
+ is to be applied to objects mapped by this mapper, which means
+ that a pending (that is, not persistent) object is auto-expunged
+ from an owning :class:`.Session` only when it is de-associated
+ from *all* parents that specify a ``delete-orphan`` cascade towards
+ this mapper. The new default behavior is that the object is
+ auto-expunged when it is de-associated with *any* of its parents
+ that specify ``delete-orphan`` cascade. This behavior is more
+ consistent with that of a persistent object, and allows behavior to
+ be consistent in more scenarios independently of whether or not an
+ orphan object has been flushed yet or not.
+
+ See the change note and example at :ref:`legacy_is_orphan_addition`
+ for more detail on this change.
+
+ :param non_primary: Specify that this :class:`_orm.Mapper`
+ is in addition
+ to the "primary" mapper, that is, the one used for persistence.
+ The :class:`_orm.Mapper` created here may be used for ad-hoc
+ mapping of the class to an alternate selectable, for loading
+ only.
+
+ .. seealso::
+
+ :ref:`relationship_aliased_class` - the new pattern that removes
+ the need for the :paramref:`_orm.Mapper.non_primary` flag.
+
+ :param passive_deletes: Indicates DELETE behavior of foreign key
+ columns when a joined-table inheritance entity is being deleted.
+ Defaults to ``False`` for a base mapper; for an inheriting mapper,
+ defaults to ``False`` unless the value is set to ``True``
+ on the superclass mapper.
+
+ When ``True``, it is assumed that ON DELETE CASCADE is configured
+ on the foreign key relationships that link this mapper's table
+ to its superclass table, so that when the unit of work attempts
+ to delete the entity, it need only emit a DELETE statement for the
+ superclass table, and not this table.
+
+ When ``False``, a DELETE statement is emitted for this mapper's
+ table individually. If the primary key attributes local to this
+ table are unloaded, then a SELECT must be emitted in order to
+ validate these attributes; note that the primary key columns
+ of a joined-table subclass are not part of the "primary key" of
+ the object as a whole.
+
+ Note that a value of ``True`` is **always** forced onto the
+ subclass mappers; that is, it's not possible for a superclass
+ to specify passive_deletes without this taking effect for
+ all subclass mappers.
+
+ .. seealso::
+
+ :ref:`passive_deletes` - description of similar feature as
+ used with :func:`_orm.relationship`
+
+ :paramref:`.mapper.passive_updates` - supporting ON UPDATE
+ CASCADE for joined-table inheritance mappers
+
+ :param passive_updates: Indicates UPDATE behavior of foreign key
+ columns when a primary key column changes on a joined-table
+ inheritance mapping. Defaults to ``True``.
+
+ When True, it is assumed that ON UPDATE CASCADE is configured on
+ the foreign key in the database, and that the database will handle
+ propagation of an UPDATE from a source column to dependent columns
+ on joined-table rows.
+
+ When False, it is assumed that the database does not enforce
+ referential integrity and will not be issuing its own CASCADE
+ operation for an update. The unit of work process will
+ emit an UPDATE statement for the dependent columns during a
+ primary key change.
+
+ .. seealso::
+
+ :ref:`passive_updates` - description of a similar feature as
+ used with :func:`_orm.relationship`
+
+ :paramref:`.mapper.passive_deletes` - supporting ON DELETE
+ CASCADE for joined-table inheritance mappers
+
+ :param polymorphic_load: Specifies "polymorphic loading" behavior
+ for a subclass in an inheritance hierarchy (joined and single
+ table inheritance only). Valid values are:
+
+ * "'inline'" - specifies this class should be part of
+ the "with_polymorphic" mappers, e.g. its columns will be included
+ in a SELECT query against the base.
+
+ * "'selectin'" - specifies that when instances of this class
+ are loaded, an additional SELECT will be emitted to retrieve
+ the columns specific to this subclass. The SELECT uses
+ IN to fetch multiple subclasses at once.
+
+ .. versionadded:: 1.2
+
+ .. seealso::
+
+ :ref:`with_polymorphic_mapper_config`
+
+ :ref:`polymorphic_selectin`
+
+ :param polymorphic_on: Specifies the column, attribute, or
+ SQL expression used to determine the target class for an
+ incoming row, when inheriting classes are present.
+
+ May be specified as a string attribute name, or as a SQL
+ expression such as a :class:`_schema.Column` or in a Declarative
+ mapping a :func:`_orm.mapped_column` object. It is typically
+ expected that the SQL expression corresponds to a column in the
+ base-most mapped :class:`.Table`::
+
+ class Employee(Base):
+ __tablename__ = 'employee'
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ discriminator: Mapped[str] = mapped_column(String(50))
+
+ __mapper_args__ = {
+ "polymorphic_on":discriminator,
+ "polymorphic_identity":"employee"
+ }
+
+ It may also be specified
+ as a SQL expression, as in this example where we
+ use the :func:`.case` construct to provide a conditional
+ approach::
+
+ class Employee(Base):
+ __tablename__ = 'employee'
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ discriminator: Mapped[str] = mapped_column(String(50))
+
+ __mapper_args__ = {
+ "polymorphic_on":case(
+ (discriminator == "EN", "engineer"),
+ (discriminator == "MA", "manager"),
+ else_="employee"),
+ "polymorphic_identity":"employee"
+ }
+
+ It may also refer to any attribute using its string name,
+ which is of particular use when using annotated column
+ configurations::
+
+ class Employee(Base):
+ __tablename__ = 'employee'
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ discriminator: Mapped[str]
+
+ __mapper_args__ = {
+ "polymorphic_on": "discriminator",
+ "polymorphic_identity": "employee"
+ }
+
+ When setting ``polymorphic_on`` to reference an
+ attribute or expression that's not present in the
+ locally mapped :class:`_schema.Table`, yet the value
+ of the discriminator should be persisted to the database,
+ the value of the
+ discriminator is not automatically set on new
+ instances; this must be handled by the user,
+ either through manual means or via event listeners.
+ A typical approach to establishing such a listener
+ looks like::
+
+ from sqlalchemy import event
+ from sqlalchemy.orm import object_mapper
+
+ @event.listens_for(Employee, "init", propagate=True)
+ def set_identity(instance, *arg, **kw):
+ mapper = object_mapper(instance)
+ instance.discriminator = mapper.polymorphic_identity
+
+ Where above, we assign the value of ``polymorphic_identity``
+ for the mapped class to the ``discriminator`` attribute,
+ thus persisting the value to the ``discriminator`` column
+ in the database.
+
+ .. warning::
+
+ Currently, **only one discriminator column may be set**, typically
+ on the base-most class in the hierarchy. "Cascading" polymorphic
+ columns are not yet supported.
+
+ .. seealso::
+
+ :ref:`inheritance_toplevel`
+
+ :param polymorphic_identity: Specifies the value which
+ identifies this particular class as returned by the column expression
+ referred to by the :paramref:`_orm.Mapper.polymorphic_on` setting. As
+ rows are received, the value corresponding to the
+ :paramref:`_orm.Mapper.polymorphic_on` column expression is compared
+ to this value, indicating which subclass should be used for the newly
+ reconstructed object.
+
+ .. seealso::
+
+ :ref:`inheritance_toplevel`
+
+ :param properties: A dictionary mapping the string names of object
+ attributes to :class:`.MapperProperty` instances, which define the
+ persistence behavior of that attribute. Note that
+ :class:`_schema.Column`
+ objects present in
+ the mapped :class:`_schema.Table` are automatically placed into
+ ``ColumnProperty`` instances upon mapping, unless overridden.
+ When using Declarative, this argument is passed automatically,
+ based on all those :class:`.MapperProperty` instances declared
+ in the declared class body.
+
+ .. seealso::
+
+ :ref:`orm_mapping_properties` - in the
+ :ref:`orm_mapping_classes_toplevel`
+
+ :param primary_key: A list of :class:`_schema.Column`
+ objects, or alternatively string names of attribute names which
+ refer to :class:`_schema.Column`, which define
+ the primary key to be used against this mapper's selectable unit.
+ This is normally simply the primary key of the ``local_table``, but
+ can be overridden here.
+
+ .. versionchanged:: 2.0.2 :paramref:`_orm.Mapper.primary_key`
+ arguments may be indicated as string attribute names as well.
+
+ .. seealso::
+
+ :ref:`mapper_primary_key` - background and example use
+
+ :param version_id_col: A :class:`_schema.Column`
+ that will be used to keep a running version id of rows
+ in the table. This is used to detect concurrent updates or
+ the presence of stale data in a flush. The methodology is to
+ detect if an UPDATE statement does not match the last known
+ version id, a
+ :class:`~sqlalchemy.orm.exc.StaleDataError` exception is
+ thrown.
+ By default, the column must be of :class:`.Integer` type,
+ unless ``version_id_generator`` specifies an alternative version
+ generator.
+
+ .. seealso::
+
+ :ref:`mapper_version_counter` - discussion of version counting
+ and rationale.
+
+ :param version_id_generator: Define how new version ids should
+ be generated. Defaults to ``None``, which indicates that
+ a simple integer counting scheme be employed. To provide a custom
+ versioning scheme, provide a callable function of the form::
+
+ def generate_version(version):
+ return next_version
+
+ Alternatively, server-side versioning functions such as triggers,
+ or programmatic versioning schemes outside of the version id
+ generator may be used, by specifying the value ``False``.
+ Please see :ref:`server_side_version_counter` for a discussion
+ of important points when using this option.
+
+ .. seealso::
+
+ :ref:`custom_version_counter`
+
+ :ref:`server_side_version_counter`
+
+
+ :param with_polymorphic: A tuple in the form ``(<classes>,
+ <selectable>)`` indicating the default style of "polymorphic"
+ loading, that is, which tables are queried at once. <classes> is
+ any single or list of mappers and/or classes indicating the
+ inherited classes that should be loaded at once. The special value
+ ``'*'`` may be used to indicate all descending classes should be
+ loaded immediately. The second tuple argument <selectable>
+ indicates a selectable that will be used to query for multiple
+ classes.
+
+ The :paramref:`_orm.Mapper.polymorphic_load` parameter may be
+ preferable over the use of :paramref:`_orm.Mapper.with_polymorphic`
+ in modern mappings to indicate a per-subclass technique of
+ indicating polymorphic loading styles.
+
+ .. seealso::
+
+ :ref:`with_polymorphic_mapper_config`
+
+ """
+ self.class_ = util.assert_arg_type(class_, type, "class_")
+ self._sort_key = "%s.%s" % (
+ self.class_.__module__,
+ self.class_.__name__,
+ )
+
+ self._primary_key_argument = util.to_list(primary_key)
+ self.non_primary = non_primary
+
+ self.always_refresh = always_refresh
+
+ if isinstance(version_id_col, MapperProperty):
+ self.version_id_prop = version_id_col
+ self.version_id_col = None
+ else:
+ self.version_id_col = (
+ coercions.expect(
+ roles.ColumnArgumentOrKeyRole,
+ version_id_col,
+ argname="version_id_col",
+ )
+ if version_id_col is not None
+ else None
+ )
+
+ if version_id_generator is False:
+ self.version_id_generator = False
+ elif version_id_generator is None:
+ self.version_id_generator = lambda x: (x or 0) + 1
+ else:
+ self.version_id_generator = version_id_generator
+
+ self.concrete = concrete
+ self.single = False
+
+ if inherits is not None:
+ self.inherits = _parse_mapper_argument(inherits)
+ else:
+ self.inherits = None
+
+ if local_table is not None:
+ self.local_table = coercions.expect(
+ roles.StrictFromClauseRole,
+ local_table,
+ disable_inspection=True,
+ argname="local_table",
+ )
+ elif self.inherits:
+ # note this is a new flow as of 2.0 so that
+ # .local_table need not be Optional
+ self.local_table = self.inherits.local_table
+ self.single = True
+ else:
+ raise sa_exc.ArgumentError(
+ f"Mapper[{self.class_.__name__}(None)] has None for a "
+ "primary table argument and does not specify 'inherits'"
+ )
+
+ if inherit_condition is not None:
+ self.inherit_condition = coercions.expect(
+ roles.OnClauseRole, inherit_condition
+ )
+ else:
+ self.inherit_condition = None
+
+ self.inherit_foreign_keys = inherit_foreign_keys
+ self._init_properties = dict(properties) if properties else {}
+ self._delete_orphans = []
+ self.batch = batch
+ self.eager_defaults = eager_defaults
+ self.column_prefix = column_prefix
+
+ # interim - polymorphic_on is further refined in
+ # _configure_polymorphic_setter
+ self.polymorphic_on = (
+ coercions.expect( # type: ignore
+ roles.ColumnArgumentOrKeyRole,
+ polymorphic_on,
+ argname="polymorphic_on",
+ )
+ if polymorphic_on is not None
+ else None
+ )
+ self.polymorphic_abstract = polymorphic_abstract
+ self._dependency_processors = []
+ self.validators = util.EMPTY_DICT
+ self.passive_updates = passive_updates
+ self.passive_deletes = passive_deletes
+ self.legacy_is_orphan = legacy_is_orphan
+ self._clause_adapter = None
+ self._requires_row_aliasing = False
+ self._inherits_equated_pairs = None
+ self._memoized_values = {}
+ self._compiled_cache_size = _compiled_cache_size
+ self._reconstructor = None
+ self.allow_partial_pks = allow_partial_pks
+
+ if self.inherits and not self.concrete:
+ self.confirm_deleted_rows = False
+ else:
+ self.confirm_deleted_rows = confirm_deleted_rows
+
+ self._set_with_polymorphic(with_polymorphic)
+ self.polymorphic_load = polymorphic_load
+
+ # our 'polymorphic identity', a string name that when located in a
+ # result set row indicates this Mapper should be used to construct
+ # the object instance for that row.
+ self.polymorphic_identity = polymorphic_identity
+
+ # a dictionary of 'polymorphic identity' names, associating those
+ # names with Mappers that will be used to construct object instances
+ # upon a select operation.
+ if _polymorphic_map is None:
+ self.polymorphic_map = {}
+ else:
+ self.polymorphic_map = _polymorphic_map
+
+ if include_properties is not None:
+ self.include_properties = util.to_set(include_properties)
+ else:
+ self.include_properties = None
+ if exclude_properties:
+ self.exclude_properties = util.to_set(exclude_properties)
+ else:
+ self.exclude_properties = None
+
+ # prevent this mapper from being constructed
+ # while a configure_mappers() is occurring (and defer a
+ # configure_mappers() until construction succeeds)
+ with _CONFIGURE_MUTEX:
+ cast("MapperEvents", self.dispatch._events)._new_mapper_instance(
+ class_, self
+ )
+ self._configure_inheritance()
+ self._configure_class_instrumentation()
+ self._configure_properties()
+ self._configure_polymorphic_setter()
+ self._configure_pks()
+ self.registry._flag_new_mapper(self)
+ self._log("constructed")
+ self._expire_memoizations()
+
+ self.dispatch.after_mapper_constructed(self, self.class_)
+
+ def _prefer_eager_defaults(self, dialect, table):
+ if self.eager_defaults == "auto":
+ if not table.implicit_returning:
+ return False
+
+ return (
+ table in self._server_default_col_keys
+ and dialect.insert_executemany_returning
+ )
+ else:
+ return self.eager_defaults
+
+ def _gen_cache_key(self, anon_map, bindparams):
+ return (self,)
+
+ # ### BEGIN
+ # ATTRIBUTE DECLARATIONS START HERE
+
+ is_mapper = True
+ """Part of the inspection API."""
+
+ represents_outer_join = False
+
+ registry: _RegistryType
+
+ @property
+ def mapper(self) -> Mapper[_O]:
+ """Part of the inspection API.
+
+ Returns self.
+
+ """
+ return self
+
+ @property
+ def entity(self):
+ r"""Part of the inspection API.
+
+ Returns self.class\_.
+
+ """
+ return self.class_
+
+ class_: Type[_O]
+ """The class to which this :class:`_orm.Mapper` is mapped."""
+
+ _identity_class: Type[_O]
+
+ _delete_orphans: List[Tuple[str, Type[Any]]]
+ _dependency_processors: List[DependencyProcessor]
+ _memoized_values: Dict[Any, Callable[[], Any]]
+ _inheriting_mappers: util.WeakSequence[Mapper[Any]]
+ _all_tables: Set[TableClause]
+ _polymorphic_attr_key: Optional[str]
+
+ _pks_by_table: Dict[FromClause, OrderedSet[ColumnClause[Any]]]
+ _cols_by_table: Dict[FromClause, OrderedSet[ColumnElement[Any]]]
+
+ _props: util.OrderedDict[str, MapperProperty[Any]]
+ _init_properties: Dict[str, MapperProperty[Any]]
+
+ _columntoproperty: _ColumnMapping
+
+ _set_polymorphic_identity: Optional[Callable[[InstanceState[_O]], None]]
+ _validate_polymorphic_identity: Optional[
+ Callable[[Mapper[_O], InstanceState[_O], _InstanceDict], None]
+ ]
+
+ tables: Sequence[TableClause]
+ """A sequence containing the collection of :class:`_schema.Table`
+ or :class:`_schema.TableClause` objects which this :class:`_orm.Mapper`
+ is aware of.
+
+ If the mapper is mapped to a :class:`_expression.Join`, or an
+ :class:`_expression.Alias`
+ representing a :class:`_expression.Select`, the individual
+ :class:`_schema.Table`
+ objects that comprise the full construct will be represented here.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ validators: util.immutabledict[str, Tuple[str, Dict[str, Any]]]
+ """An immutable dictionary of attributes which have been decorated
+ using the :func:`_orm.validates` decorator.
+
+ The dictionary contains string attribute names as keys
+ mapped to the actual validation method.
+
+ """
+
+ always_refresh: bool
+ allow_partial_pks: bool
+ version_id_col: Optional[ColumnElement[Any]]
+
+ with_polymorphic: Optional[
+ Tuple[
+ Union[Literal["*"], Sequence[Union[Mapper[Any], Type[Any]]]],
+ Optional[FromClause],
+ ]
+ ]
+
+ version_id_generator: Optional[Union[Literal[False], Callable[[Any], Any]]]
+
+ local_table: FromClause
+ """The immediate :class:`_expression.FromClause` to which this
+ :class:`_orm.Mapper` refers.
+
+ Typically is an instance of :class:`_schema.Table`, may be any
+ :class:`.FromClause`.
+
+ The "local" table is the
+ selectable that the :class:`_orm.Mapper` is directly responsible for
+ managing from an attribute access and flush perspective. For
+ non-inheriting mappers, :attr:`.Mapper.local_table` will be the same
+ as :attr:`.Mapper.persist_selectable`. For inheriting mappers,
+ :attr:`.Mapper.local_table` refers to the specific portion of
+ :attr:`.Mapper.persist_selectable` that includes the columns to which
+ this :class:`.Mapper` is loading/persisting, such as a particular
+ :class:`.Table` within a join.
+
+ .. seealso::
+
+ :attr:`_orm.Mapper.persist_selectable`.
+
+ :attr:`_orm.Mapper.selectable`.
+
+ """
+
+ persist_selectable: FromClause
+ """The :class:`_expression.FromClause` to which this :class:`_orm.Mapper`
+ is mapped.
+
+ Typically is an instance of :class:`_schema.Table`, may be any
+ :class:`.FromClause`.
+
+ The :attr:`_orm.Mapper.persist_selectable` is similar to
+ :attr:`.Mapper.local_table`, but represents the :class:`.FromClause` that
+ represents the inheriting class hierarchy overall in an inheritance
+ scenario.
+
+ :attr.`.Mapper.persist_selectable` is also separate from the
+ :attr:`.Mapper.selectable` attribute, the latter of which may be an
+ alternate subquery used for selecting columns.
+ :attr.`.Mapper.persist_selectable` is oriented towards columns that
+ will be written on a persist operation.
+
+ .. seealso::
+
+ :attr:`_orm.Mapper.selectable`.
+
+ :attr:`_orm.Mapper.local_table`.
+
+ """
+
+ inherits: Optional[Mapper[Any]]
+ """References the :class:`_orm.Mapper` which this :class:`_orm.Mapper`
+ inherits from, if any.
+
+ """
+
+ inherit_condition: Optional[ColumnElement[bool]]
+
+ configured: bool = False
+ """Represent ``True`` if this :class:`_orm.Mapper` has been configured.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ .. seealso::
+
+ :func:`.configure_mappers`.
+
+ """
+
+ concrete: bool
+ """Represent ``True`` if this :class:`_orm.Mapper` is a concrete
+ inheritance mapper.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ primary_key: Tuple[Column[Any], ...]
+ """An iterable containing the collection of :class:`_schema.Column`
+ objects
+ which comprise the 'primary key' of the mapped table, from the
+ perspective of this :class:`_orm.Mapper`.
+
+ This list is against the selectable in
+ :attr:`_orm.Mapper.persist_selectable`.
+ In the case of inheriting mappers, some columns may be managed by a
+ superclass mapper. For example, in the case of a
+ :class:`_expression.Join`, the
+ primary key is determined by all of the primary key columns across all
+ tables referenced by the :class:`_expression.Join`.
+
+ The list is also not necessarily the same as the primary key column
+ collection associated with the underlying tables; the :class:`_orm.Mapper`
+ features a ``primary_key`` argument that can override what the
+ :class:`_orm.Mapper` considers as primary key columns.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ class_manager: ClassManager[_O]
+ """The :class:`.ClassManager` which maintains event listeners
+ and class-bound descriptors for this :class:`_orm.Mapper`.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ single: bool
+ """Represent ``True`` if this :class:`_orm.Mapper` is a single table
+ inheritance mapper.
+
+ :attr:`_orm.Mapper.local_table` will be ``None`` if this flag is set.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ non_primary: bool
+ """Represent ``True`` if this :class:`_orm.Mapper` is a "non-primary"
+ mapper, e.g. a mapper that is used only to select rows but not for
+ persistence management.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ polymorphic_on: Optional[KeyedColumnElement[Any]]
+ """The :class:`_schema.Column` or SQL expression specified as the
+ ``polymorphic_on`` argument
+ for this :class:`_orm.Mapper`, within an inheritance scenario.
+
+ This attribute is normally a :class:`_schema.Column` instance but
+ may also be an expression, such as one derived from
+ :func:`.cast`.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ polymorphic_map: Dict[Any, Mapper[Any]]
+ """A mapping of "polymorphic identity" identifiers mapped to
+ :class:`_orm.Mapper` instances, within an inheritance scenario.
+
+ The identifiers can be of any type which is comparable to the
+ type of column represented by :attr:`_orm.Mapper.polymorphic_on`.
+
+ An inheritance chain of mappers will all reference the same
+ polymorphic map object. The object is used to correlate incoming
+ result rows to target mappers.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ polymorphic_identity: Optional[Any]
+ """Represent an identifier which is matched against the
+ :attr:`_orm.Mapper.polymorphic_on` column during result row loading.
+
+ Used only with inheritance, this object can be of any type which is
+ comparable to the type of column represented by
+ :attr:`_orm.Mapper.polymorphic_on`.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ base_mapper: Mapper[Any]
+ """The base-most :class:`_orm.Mapper` in an inheritance chain.
+
+ In a non-inheriting scenario, this attribute will always be this
+ :class:`_orm.Mapper`. In an inheritance scenario, it references
+ the :class:`_orm.Mapper` which is parent to all other :class:`_orm.Mapper`
+ objects in the inheritance chain.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ columns: ReadOnlyColumnCollection[str, Column[Any]]
+ """A collection of :class:`_schema.Column` or other scalar expression
+ objects maintained by this :class:`_orm.Mapper`.
+
+ The collection behaves the same as that of the ``c`` attribute on
+ any :class:`_schema.Table` object,
+ except that only those columns included in
+ this mapping are present, and are keyed based on the attribute name
+ defined in the mapping, not necessarily the ``key`` attribute of the
+ :class:`_schema.Column` itself. Additionally, scalar expressions mapped
+ by :func:`.column_property` are also present here.
+
+ This is a *read only* attribute determined during mapper construction.
+ Behavior is undefined if directly modified.
+
+ """
+
+ c: ReadOnlyColumnCollection[str, Column[Any]]
+ """A synonym for :attr:`_orm.Mapper.columns`."""
+
+ @util.non_memoized_property
+ @util.deprecated("1.3", "Use .persist_selectable")
+ def mapped_table(self):
+ return self.persist_selectable
+
+ @util.memoized_property
+ def _path_registry(self) -> CachingEntityRegistry:
+ return PathRegistry.per_mapper(self)
+
+ def _configure_inheritance(self):
+ """Configure settings related to inheriting and/or inherited mappers
+ being present."""
+
+ # a set of all mappers which inherit from this one.
+ self._inheriting_mappers = util.WeakSequence()
+
+ if self.inherits:
+ if not issubclass(self.class_, self.inherits.class_):
+ raise sa_exc.ArgumentError(
+ "Class '%s' does not inherit from '%s'"
+ % (self.class_.__name__, self.inherits.class_.__name__)
+ )
+
+ self.dispatch._update(self.inherits.dispatch)
+
+ if self.non_primary != self.inherits.non_primary:
+ np = not self.non_primary and "primary" or "non-primary"
+ raise sa_exc.ArgumentError(
+ "Inheritance of %s mapper for class '%s' is "
+ "only allowed from a %s mapper"
+ % (np, self.class_.__name__, np)
+ )
+
+ if self.single:
+ self.persist_selectable = self.inherits.persist_selectable
+ elif self.local_table is not self.inherits.local_table:
+ if self.concrete:
+ self.persist_selectable = self.local_table
+ for mapper in self.iterate_to_root():
+ if mapper.polymorphic_on is not None:
+ mapper._requires_row_aliasing = True
+ else:
+ if self.inherit_condition is None:
+ # figure out inherit condition from our table to the
+ # immediate table of the inherited mapper, not its
+ # full table which could pull in other stuff we don't
+ # want (allows test/inheritance.InheritTest4 to pass)
+ try:
+ self.inherit_condition = sql_util.join_condition(
+ self.inherits.local_table, self.local_table
+ )
+ except sa_exc.NoForeignKeysError as nfe:
+ assert self.inherits.local_table is not None
+ assert self.local_table is not None
+ raise sa_exc.NoForeignKeysError(
+ "Can't determine the inherit condition "
+ "between inherited table '%s' and "
+ "inheriting "
+ "table '%s'; tables have no "
+ "foreign key relationships established. "
+ "Please ensure the inheriting table has "
+ "a foreign key relationship to the "
+ "inherited "
+ "table, or provide an "
+ "'on clause' using "
+ "the 'inherit_condition' mapper argument."
+ % (
+ self.inherits.local_table.description,
+ self.local_table.description,
+ )
+ ) from nfe
+ except sa_exc.AmbiguousForeignKeysError as afe:
+ assert self.inherits.local_table is not None
+ assert self.local_table is not None
+ raise sa_exc.AmbiguousForeignKeysError(
+ "Can't determine the inherit condition "
+ "between inherited table '%s' and "
+ "inheriting "
+ "table '%s'; tables have more than one "
+ "foreign key relationship established. "
+ "Please specify the 'on clause' using "
+ "the 'inherit_condition' mapper argument."
+ % (
+ self.inherits.local_table.description,
+ self.local_table.description,
+ )
+ ) from afe
+ assert self.inherits.persist_selectable is not None
+ self.persist_selectable = sql.join(
+ self.inherits.persist_selectable,
+ self.local_table,
+ self.inherit_condition,
+ )
+
+ fks = util.to_set(self.inherit_foreign_keys)
+ self._inherits_equated_pairs = sql_util.criterion_as_pairs(
+ self.persist_selectable.onclause,
+ consider_as_foreign_keys=fks,
+ )
+ else:
+ self.persist_selectable = self.local_table
+
+ if self.polymorphic_identity is None:
+ self._identity_class = self.class_
+
+ if (
+ not self.polymorphic_abstract
+ and self.inherits.base_mapper.polymorphic_on is not None
+ ):
+ util.warn(
+ f"{self} does not indicate a 'polymorphic_identity', "
+ "yet is part of an inheritance hierarchy that has a "
+ f"'polymorphic_on' column of "
+ f"'{self.inherits.base_mapper.polymorphic_on}'. "
+ "If this is an intermediary class that should not be "
+ "instantiated, the class may either be left unmapped, "
+ "or may include the 'polymorphic_abstract=True' "
+ "parameter in its Mapper arguments. To leave the "
+ "class unmapped when using Declarative, set the "
+ "'__abstract__ = True' attribute on the class."
+ )
+ elif self.concrete:
+ self._identity_class = self.class_
+ else:
+ self._identity_class = self.inherits._identity_class
+
+ if self.version_id_col is None:
+ self.version_id_col = self.inherits.version_id_col
+ self.version_id_generator = self.inherits.version_id_generator
+ elif (
+ self.inherits.version_id_col is not None
+ and self.version_id_col is not self.inherits.version_id_col
+ ):
+ util.warn(
+ "Inheriting version_id_col '%s' does not match inherited "
+ "version_id_col '%s' and will not automatically populate "
+ "the inherited versioning column. "
+ "version_id_col should only be specified on "
+ "the base-most mapper that includes versioning."
+ % (
+ self.version_id_col.description,
+ self.inherits.version_id_col.description,
+ )
+ )
+
+ self.polymorphic_map = self.inherits.polymorphic_map
+ self.batch = self.inherits.batch
+ self.inherits._inheriting_mappers.append(self)
+ self.base_mapper = self.inherits.base_mapper
+ self.passive_updates = self.inherits.passive_updates
+ self.passive_deletes = (
+ self.inherits.passive_deletes or self.passive_deletes
+ )
+ self._all_tables = self.inherits._all_tables
+
+ if self.polymorphic_identity is not None:
+ if self.polymorphic_identity in self.polymorphic_map:
+ util.warn(
+ "Reassigning polymorphic association for identity %r "
+ "from %r to %r: Check for duplicate use of %r as "
+ "value for polymorphic_identity."
+ % (
+ self.polymorphic_identity,
+ self.polymorphic_map[self.polymorphic_identity],
+ self,
+ self.polymorphic_identity,
+ )
+ )
+ self.polymorphic_map[self.polymorphic_identity] = self
+
+ if self.polymorphic_load and self.concrete:
+ raise sa_exc.ArgumentError(
+ "polymorphic_load is not currently supported "
+ "with concrete table inheritance"
+ )
+ if self.polymorphic_load == "inline":
+ self.inherits._add_with_polymorphic_subclass(self)
+ elif self.polymorphic_load == "selectin":
+ pass
+ elif self.polymorphic_load is not None:
+ raise sa_exc.ArgumentError(
+ "unknown argument for polymorphic_load: %r"
+ % self.polymorphic_load
+ )
+
+ else:
+ self._all_tables = set()
+ self.base_mapper = self
+ assert self.local_table is not None
+ self.persist_selectable = self.local_table
+ if self.polymorphic_identity is not None:
+ self.polymorphic_map[self.polymorphic_identity] = self
+ self._identity_class = self.class_
+
+ if self.persist_selectable is None:
+ raise sa_exc.ArgumentError(
+ "Mapper '%s' does not have a persist_selectable specified."
+ % self
+ )
+
+ def _set_with_polymorphic(
+ self, with_polymorphic: Optional[_WithPolymorphicArg]
+ ) -> None:
+ if with_polymorphic == "*":
+ self.with_polymorphic = ("*", None)
+ elif isinstance(with_polymorphic, (tuple, list)):
+ if isinstance(with_polymorphic[0], (str, tuple, list)):
+ self.with_polymorphic = cast(
+ """Tuple[
+ Union[
+ Literal["*"],
+ Sequence[Union["Mapper[Any]", Type[Any]]],
+ ],
+ Optional["FromClause"],
+ ]""",
+ with_polymorphic,
+ )
+ else:
+ self.with_polymorphic = (with_polymorphic, None)
+ elif with_polymorphic is not None:
+ raise sa_exc.ArgumentError(
+ f"Invalid setting for with_polymorphic: {with_polymorphic!r}"
+ )
+ else:
+ self.with_polymorphic = None
+
+ if self.with_polymorphic and self.with_polymorphic[1] is not None:
+ self.with_polymorphic = (
+ self.with_polymorphic[0],
+ coercions.expect(
+ roles.StrictFromClauseRole,
+ self.with_polymorphic[1],
+ allow_select=True,
+ ),
+ )
+
+ if self.configured:
+ self._expire_memoizations()
+
+ def _add_with_polymorphic_subclass(self, mapper):
+ subcl = mapper.class_
+ if self.with_polymorphic is None:
+ self._set_with_polymorphic((subcl,))
+ elif self.with_polymorphic[0] != "*":
+ assert isinstance(self.with_polymorphic[0], tuple)
+ self._set_with_polymorphic(
+ (self.with_polymorphic[0] + (subcl,), self.with_polymorphic[1])
+ )
+
+ def _set_concrete_base(self, mapper):
+ """Set the given :class:`_orm.Mapper` as the 'inherits' for this
+ :class:`_orm.Mapper`, assuming this :class:`_orm.Mapper` is concrete
+ and does not already have an inherits."""
+
+ assert self.concrete
+ assert not self.inherits
+ assert isinstance(mapper, Mapper)
+ self.inherits = mapper
+ self.inherits.polymorphic_map.update(self.polymorphic_map)
+ self.polymorphic_map = self.inherits.polymorphic_map
+ for mapper in self.iterate_to_root():
+ if mapper.polymorphic_on is not None:
+ mapper._requires_row_aliasing = True
+ self.batch = self.inherits.batch
+ for mp in self.self_and_descendants:
+ mp.base_mapper = self.inherits.base_mapper
+ self.inherits._inheriting_mappers.append(self)
+ self.passive_updates = self.inherits.passive_updates
+ self._all_tables = self.inherits._all_tables
+
+ for key, prop in mapper._props.items():
+ if key not in self._props and not self._should_exclude(
+ key, key, local=False, column=None
+ ):
+ self._adapt_inherited_property(key, prop, False)
+
+ def _set_polymorphic_on(self, polymorphic_on):
+ self.polymorphic_on = polymorphic_on
+ self._configure_polymorphic_setter(True)
+
+ def _configure_class_instrumentation(self):
+ """If this mapper is to be a primary mapper (i.e. the
+ non_primary flag is not set), associate this Mapper with the
+ given class and entity name.
+
+ Subsequent calls to ``class_mapper()`` for the ``class_`` / ``entity``
+ name combination will return this mapper. Also decorate the
+ `__init__` method on the mapped class to include optional
+ auto-session attachment logic.
+
+ """
+
+ # we expect that declarative has applied the class manager
+ # already and set up a registry. if this is None,
+ # this raises as of 2.0.
+ manager = attributes.opt_manager_of_class(self.class_)
+
+ if self.non_primary:
+ if not manager or not manager.is_mapped:
+ raise sa_exc.InvalidRequestError(
+ "Class %s has no primary mapper configured. Configure "
+ "a primary mapper first before setting up a non primary "
+ "Mapper." % self.class_
+ )
+ self.class_manager = manager
+
+ assert manager.registry is not None
+ self.registry = manager.registry
+ self._identity_class = manager.mapper._identity_class
+ manager.registry._add_non_primary_mapper(self)
+ return
+
+ if manager is None or not manager.registry:
+ raise sa_exc.InvalidRequestError(
+ "The _mapper() function and Mapper() constructor may not be "
+ "invoked directly outside of a declarative registry."
+ " Please use the sqlalchemy.orm.registry.map_imperatively() "
+ "function for a classical mapping."
+ )
+
+ self.dispatch.instrument_class(self, self.class_)
+
+ # this invokes the class_instrument event and sets up
+ # the __init__ method. documented behavior is that this must
+ # occur after the instrument_class event above.
+ # yes two events with the same two words reversed and different APIs.
+ # :(
+
+ manager = instrumentation.register_class(
+ self.class_,
+ mapper=self,
+ expired_attribute_loader=util.partial(
+ loading.load_scalar_attributes, self
+ ),
+ # finalize flag means instrument the __init__ method
+ # and call the class_instrument event
+ finalize=True,
+ )
+
+ self.class_manager = manager
+
+ assert manager.registry is not None
+ self.registry = manager.registry
+
+ # The remaining members can be added by any mapper,
+ # e_name None or not.
+ if manager.mapper is None:
+ return
+
+ event.listen(manager, "init", _event_on_init, raw=True)
+
+ for key, method in util.iterate_attributes(self.class_):
+ if key == "__init__" and hasattr(method, "_sa_original_init"):
+ method = method._sa_original_init
+ if hasattr(method, "__func__"):
+ method = method.__func__
+ if callable(method):
+ if hasattr(method, "__sa_reconstructor__"):
+ self._reconstructor = method
+ event.listen(manager, "load", _event_on_load, raw=True)
+ elif hasattr(method, "__sa_validators__"):
+ validation_opts = method.__sa_validation_opts__
+ for name in method.__sa_validators__:
+ if name in self.validators:
+ raise sa_exc.InvalidRequestError(
+ "A validation function for mapped "
+ "attribute %r on mapper %s already exists."
+ % (name, self)
+ )
+ self.validators = self.validators.union(
+ {name: (method, validation_opts)}
+ )
+
+ def _set_dispose_flags(self) -> None:
+ self.configured = True
+ self._ready_for_configure = True
+ self._dispose_called = True
+
+ self.__dict__.pop("_configure_failed", None)
+
+ def _str_arg_to_mapped_col(self, argname: str, key: str) -> Column[Any]:
+ try:
+ prop = self._props[key]
+ except KeyError as err:
+ raise sa_exc.ArgumentError(
+ f"Can't determine {argname} column '{key}' - "
+ "no attribute is mapped to this name."
+ ) from err
+ try:
+ expr = prop.expression
+ except AttributeError as ae:
+ raise sa_exc.ArgumentError(
+ f"Can't determine {argname} column '{key}'; "
+ "property does not refer to a single mapped Column"
+ ) from ae
+ if not isinstance(expr, Column):
+ raise sa_exc.ArgumentError(
+ f"Can't determine {argname} column '{key}'; "
+ "property does not refer to a single "
+ "mapped Column"
+ )
+ return expr
+
+ def _configure_pks(self) -> None:
+ self.tables = sql_util.find_tables(self.persist_selectable)
+
+ self._all_tables.update(t for t in self.tables)
+
+ self._pks_by_table = {}
+ self._cols_by_table = {}
+
+ all_cols = util.column_set(
+ chain(*[col.proxy_set for col in self._columntoproperty])
+ )
+
+ pk_cols = util.column_set(c for c in all_cols if c.primary_key)
+
+ # identify primary key columns which are also mapped by this mapper.
+ for fc in set(self.tables).union([self.persist_selectable]):
+ if fc.primary_key and pk_cols.issuperset(fc.primary_key):
+ # ordering is important since it determines the ordering of
+ # mapper.primary_key (and therefore query.get())
+ self._pks_by_table[fc] = util.ordered_column_set( # type: ignore # noqa: E501
+ fc.primary_key
+ ).intersection(
+ pk_cols
+ )
+ self._cols_by_table[fc] = util.ordered_column_set(fc.c).intersection( # type: ignore # noqa: E501
+ all_cols
+ )
+
+ if self._primary_key_argument:
+ coerced_pk_arg = [
+ (
+ self._str_arg_to_mapped_col("primary_key", c)
+ if isinstance(c, str)
+ else c
+ )
+ for c in (
+ coercions.expect(
+ roles.DDLConstraintColumnRole,
+ coerce_pk,
+ argname="primary_key",
+ )
+ for coerce_pk in self._primary_key_argument
+ )
+ ]
+ else:
+ coerced_pk_arg = None
+
+ # if explicit PK argument sent, add those columns to the
+ # primary key mappings
+ if coerced_pk_arg:
+ for k in coerced_pk_arg:
+ if k.table not in self._pks_by_table:
+ self._pks_by_table[k.table] = util.OrderedSet()
+ self._pks_by_table[k.table].add(k)
+
+ # otherwise, see that we got a full PK for the mapped table
+ elif (
+ self.persist_selectable not in self._pks_by_table
+ or len(self._pks_by_table[self.persist_selectable]) == 0
+ ):
+ raise sa_exc.ArgumentError(
+ "Mapper %s could not assemble any primary "
+ "key columns for mapped table '%s'"
+ % (self, self.persist_selectable.description)
+ )
+ elif self.local_table not in self._pks_by_table and isinstance(
+ self.local_table, schema.Table
+ ):
+ util.warn(
+ "Could not assemble any primary "
+ "keys for locally mapped table '%s' - "
+ "no rows will be persisted in this Table."
+ % self.local_table.description
+ )
+
+ if (
+ self.inherits
+ and not self.concrete
+ and not self._primary_key_argument
+ ):
+ # if inheriting, the "primary key" for this mapper is
+ # that of the inheriting (unless concrete or explicit)
+ self.primary_key = self.inherits.primary_key
+ else:
+ # determine primary key from argument or persist_selectable pks
+ primary_key: Collection[ColumnElement[Any]]
+
+ if coerced_pk_arg:
+ primary_key = [
+ cc if cc is not None else c
+ for cc, c in (
+ (self.persist_selectable.corresponding_column(c), c)
+ for c in coerced_pk_arg
+ )
+ ]
+ else:
+ # if heuristically determined PKs, reduce to the minimal set
+ # of columns by eliminating FK->PK pairs for a multi-table
+ # expression. May over-reduce for some kinds of UNIONs
+ # / CTEs; use explicit PK argument for these special cases
+ primary_key = sql_util.reduce_columns(
+ self._pks_by_table[self.persist_selectable],
+ ignore_nonexistent_tables=True,
+ )
+
+ if len(primary_key) == 0:
+ raise sa_exc.ArgumentError(
+ "Mapper %s could not assemble any primary "
+ "key columns for mapped table '%s'"
+ % (self, self.persist_selectable.description)
+ )
+
+ self.primary_key = tuple(primary_key)
+ self._log("Identified primary key columns: %s", primary_key)
+
+ # determine cols that aren't expressed within our tables; mark these
+ # as "read only" properties which are refreshed upon INSERT/UPDATE
+ self._readonly_props = {
+ self._columntoproperty[col]
+ for col in self._columntoproperty
+ if self._columntoproperty[col] not in self._identity_key_props
+ and (
+ not hasattr(col, "table")
+ or col.table not in self._cols_by_table
+ )
+ }
+
+ def _configure_properties(self) -> None:
+ self.columns = self.c = sql_base.ColumnCollection() # type: ignore
+
+ # object attribute names mapped to MapperProperty objects
+ self._props = util.OrderedDict()
+
+ # table columns mapped to MapperProperty
+ self._columntoproperty = _ColumnMapping(self)
+
+ explicit_col_props_by_column: Dict[
+ KeyedColumnElement[Any], Tuple[str, ColumnProperty[Any]]
+ ] = {}
+ explicit_col_props_by_key: Dict[str, ColumnProperty[Any]] = {}
+
+ # step 1: go through properties that were explicitly passed
+ # in the properties dictionary. For Columns that are local, put them
+ # aside in a separate collection we will reconcile with the Table
+ # that's given. For other properties, set them up in _props now.
+ if self._init_properties:
+ for key, prop_arg in self._init_properties.items():
+ if not isinstance(prop_arg, MapperProperty):
+ possible_col_prop = self._make_prop_from_column(
+ key, prop_arg
+ )
+ else:
+ possible_col_prop = prop_arg
+
+ # issue #8705. if the explicit property is actually a
+ # Column that is local to the local Table, don't set it up
+ # in ._props yet, integrate it into the order given within
+ # the Table.
+
+ _map_as_property_now = True
+ if isinstance(possible_col_prop, properties.ColumnProperty):
+ for given_col in possible_col_prop.columns:
+ if self.local_table.c.contains_column(given_col):
+ _map_as_property_now = False
+ explicit_col_props_by_key[key] = possible_col_prop
+ explicit_col_props_by_column[given_col] = (
+ key,
+ possible_col_prop,
+ )
+
+ if _map_as_property_now:
+ self._configure_property(
+ key,
+ possible_col_prop,
+ init=False,
+ )
+
+ # step 2: pull properties from the inherited mapper. reconcile
+ # columns with those which are explicit above. for properties that
+ # are only in the inheriting mapper, set them up as local props
+ if self.inherits:
+ for key, inherited_prop in self.inherits._props.items():
+ if self._should_exclude(key, key, local=False, column=None):
+ continue
+
+ incoming_prop = explicit_col_props_by_key.get(key)
+ if incoming_prop:
+ new_prop = self._reconcile_prop_with_incoming_columns(
+ key,
+ inherited_prop,
+ warn_only=False,
+ incoming_prop=incoming_prop,
+ )
+ explicit_col_props_by_key[key] = new_prop
+
+ for inc_col in incoming_prop.columns:
+ explicit_col_props_by_column[inc_col] = (
+ key,
+ new_prop,
+ )
+ elif key not in self._props:
+ self._adapt_inherited_property(key, inherited_prop, False)
+
+ # step 3. Iterate through all columns in the persist selectable.
+ # this includes not only columns in the local table / fromclause,
+ # but also those columns in the superclass table if we are joined
+ # inh or single inh mapper. map these columns as well. additional
+ # reconciliation against inherited columns occurs here also.
+
+ for column in self.persist_selectable.columns:
+ if column in explicit_col_props_by_column:
+ # column was explicitly passed to properties; configure
+ # it now in the order in which it corresponds to the
+ # Table / selectable
+ key, prop = explicit_col_props_by_column[column]
+ self._configure_property(key, prop, init=False)
+ continue
+
+ elif column in self._columntoproperty:
+ continue
+
+ column_key = (self.column_prefix or "") + column.key
+ if self._should_exclude(
+ column.key,
+ column_key,
+ local=self.local_table.c.contains_column(column),
+ column=column,
+ ):
+ continue
+
+ # adjust the "key" used for this column to that
+ # of the inheriting mapper
+ for mapper in self.iterate_to_root():
+ if column in mapper._columntoproperty:
+ column_key = mapper._columntoproperty[column].key
+
+ self._configure_property(
+ column_key,
+ column,
+ init=False,
+ setparent=True,
+ )
+
+ def _configure_polymorphic_setter(self, init=False):
+ """Configure an attribute on the mapper representing the
+ 'polymorphic_on' column, if applicable, and not
+ already generated by _configure_properties (which is typical).
+
+ Also create a setter function which will assign this
+ attribute to the value of the 'polymorphic_identity'
+ upon instance construction, also if applicable. This
+ routine will run when an instance is created.
+
+ """
+ setter = False
+ polymorphic_key: Optional[str] = None
+
+ if self.polymorphic_on is not None:
+ setter = True
+
+ if isinstance(self.polymorphic_on, str):
+ # polymorphic_on specified as a string - link
+ # it to mapped ColumnProperty
+ try:
+ self.polymorphic_on = self._props[self.polymorphic_on]
+ except KeyError as err:
+ raise sa_exc.ArgumentError(
+ "Can't determine polymorphic_on "
+ "value '%s' - no attribute is "
+ "mapped to this name." % self.polymorphic_on
+ ) from err
+
+ if self.polymorphic_on in self._columntoproperty:
+ # polymorphic_on is a column that is already mapped
+ # to a ColumnProperty
+ prop = self._columntoproperty[self.polymorphic_on]
+ elif isinstance(self.polymorphic_on, MapperProperty):
+ # polymorphic_on is directly a MapperProperty,
+ # ensure it's a ColumnProperty
+ if not isinstance(
+ self.polymorphic_on, properties.ColumnProperty
+ ):
+ raise sa_exc.ArgumentError(
+ "Only direct column-mapped "
+ "property or SQL expression "
+ "can be passed for polymorphic_on"
+ )
+ prop = self.polymorphic_on
+ else:
+ # polymorphic_on is a Column or SQL expression and
+ # doesn't appear to be mapped. this means it can be 1.
+ # only present in the with_polymorphic selectable or
+ # 2. a totally standalone SQL expression which we'd
+ # hope is compatible with this mapper's persist_selectable
+ col = self.persist_selectable.corresponding_column(
+ self.polymorphic_on
+ )
+ if col is None:
+ # polymorphic_on doesn't derive from any
+ # column/expression isn't present in the mapped
+ # table. we will make a "hidden" ColumnProperty
+ # for it. Just check that if it's directly a
+ # schema.Column and we have with_polymorphic, it's
+ # likely a user error if the schema.Column isn't
+ # represented somehow in either persist_selectable or
+ # with_polymorphic. Otherwise as of 0.7.4 we
+ # just go with it and assume the user wants it
+ # that way (i.e. a CASE statement)
+ setter = False
+ instrument = False
+ col = self.polymorphic_on
+ if isinstance(col, schema.Column) and (
+ self.with_polymorphic is None
+ or self.with_polymorphic[1] is None
+ or self.with_polymorphic[1].corresponding_column(col)
+ is None
+ ):
+ raise sa_exc.InvalidRequestError(
+ "Could not map polymorphic_on column "
+ "'%s' to the mapped table - polymorphic "
+ "loads will not function properly"
+ % col.description
+ )
+ else:
+ # column/expression that polymorphic_on derives from
+ # is present in our mapped table
+ # and is probably mapped, but polymorphic_on itself
+ # is not. This happens when
+ # the polymorphic_on is only directly present in the
+ # with_polymorphic selectable, as when use
+ # polymorphic_union.
+ # we'll make a separate ColumnProperty for it.
+ instrument = True
+ key = getattr(col, "key", None)
+ if key:
+ if self._should_exclude(key, key, False, col):
+ raise sa_exc.InvalidRequestError(
+ "Cannot exclude or override the "
+ "discriminator column %r" % key
+ )
+ else:
+ self.polymorphic_on = col = col.label("_sa_polymorphic_on")
+ key = col.key
+
+ prop = properties.ColumnProperty(col, _instrument=instrument)
+ self._configure_property(key, prop, init=init, setparent=True)
+
+ # the actual polymorphic_on should be the first public-facing
+ # column in the property
+ self.polymorphic_on = prop.columns[0]
+ polymorphic_key = prop.key
+ else:
+ # no polymorphic_on was set.
+ # check inheriting mappers for one.
+ for mapper in self.iterate_to_root():
+ # determine if polymorphic_on of the parent
+ # should be propagated here. If the col
+ # is present in our mapped table, or if our mapped
+ # table is the same as the parent (i.e. single table
+ # inheritance), we can use it
+ if mapper.polymorphic_on is not None:
+ if self.persist_selectable is mapper.persist_selectable:
+ self.polymorphic_on = mapper.polymorphic_on
+ else:
+ self.polymorphic_on = (
+ self.persist_selectable
+ ).corresponding_column(mapper.polymorphic_on)
+ # we can use the parent mapper's _set_polymorphic_identity
+ # directly; it ensures the polymorphic_identity of the
+ # instance's mapper is used so is portable to subclasses.
+ if self.polymorphic_on is not None:
+ self._set_polymorphic_identity = (
+ mapper._set_polymorphic_identity
+ )
+ self._polymorphic_attr_key = (
+ mapper._polymorphic_attr_key
+ )
+ self._validate_polymorphic_identity = (
+ mapper._validate_polymorphic_identity
+ )
+ else:
+ self._set_polymorphic_identity = None
+ self._polymorphic_attr_key = None
+ return
+
+ if self.polymorphic_abstract and self.polymorphic_on is None:
+ raise sa_exc.InvalidRequestError(
+ "The Mapper.polymorphic_abstract parameter may only be used "
+ "on a mapper hierarchy which includes the "
+ "Mapper.polymorphic_on parameter at the base of the hierarchy."
+ )
+
+ if setter:
+
+ def _set_polymorphic_identity(state):
+ dict_ = state.dict
+ # TODO: what happens if polymorphic_on column attribute name
+ # does not match .key?
+
+ polymorphic_identity = (
+ state.manager.mapper.polymorphic_identity
+ )
+ if (
+ polymorphic_identity is None
+ and state.manager.mapper.polymorphic_abstract
+ ):
+ raise sa_exc.InvalidRequestError(
+ f"Can't instantiate class for {state.manager.mapper}; "
+ "mapper is marked polymorphic_abstract=True"
+ )
+
+ state.get_impl(polymorphic_key).set(
+ state,
+ dict_,
+ polymorphic_identity,
+ None,
+ )
+
+ self._polymorphic_attr_key = polymorphic_key
+
+ def _validate_polymorphic_identity(mapper, state, dict_):
+ if (
+ polymorphic_key in dict_
+ and dict_[polymorphic_key]
+ not in mapper._acceptable_polymorphic_identities
+ ):
+ util.warn_limited(
+ "Flushing object %s with "
+ "incompatible polymorphic identity %r; the "
+ "object may not refresh and/or load correctly",
+ (state_str(state), dict_[polymorphic_key]),
+ )
+
+ self._set_polymorphic_identity = _set_polymorphic_identity
+ self._validate_polymorphic_identity = (
+ _validate_polymorphic_identity
+ )
+ else:
+ self._polymorphic_attr_key = None
+ self._set_polymorphic_identity = None
+
+ _validate_polymorphic_identity = None
+
+ @HasMemoized.memoized_attribute
+ def _version_id_prop(self):
+ if self.version_id_col is not None:
+ return self._columntoproperty[self.version_id_col]
+ else:
+ return None
+
+ @HasMemoized.memoized_attribute
+ def _acceptable_polymorphic_identities(self):
+ identities = set()
+
+ stack = deque([self])
+ while stack:
+ item = stack.popleft()
+ if item.persist_selectable is self.persist_selectable:
+ identities.add(item.polymorphic_identity)
+ stack.extend(item._inheriting_mappers)
+
+ return identities
+
+ @HasMemoized.memoized_attribute
+ def _prop_set(self):
+ return frozenset(self._props.values())
+
+ @util.preload_module("sqlalchemy.orm.descriptor_props")
+ def _adapt_inherited_property(self, key, prop, init):
+ descriptor_props = util.preloaded.orm_descriptor_props
+
+ if not self.concrete:
+ self._configure_property(key, prop, init=False, setparent=False)
+ elif key not in self._props:
+ # determine if the class implements this attribute; if not,
+ # or if it is implemented by the attribute that is handling the
+ # given superclass-mapped property, then we need to report that we
+ # can't use this at the instance level since we are a concrete
+ # mapper and we don't map this. don't trip user-defined
+ # descriptors that might have side effects when invoked.
+ implementing_attribute = self.class_manager._get_class_attr_mro(
+ key, prop
+ )
+ if implementing_attribute is prop or (
+ isinstance(
+ implementing_attribute, attributes.InstrumentedAttribute
+ )
+ and implementing_attribute._parententity is prop.parent
+ ):
+ self._configure_property(
+ key,
+ descriptor_props.ConcreteInheritedProperty(),
+ init=init,
+ setparent=True,
+ )
+
+ @util.preload_module("sqlalchemy.orm.descriptor_props")
+ def _configure_property(
+ self,
+ key: str,
+ prop_arg: Union[KeyedColumnElement[Any], MapperProperty[Any]],
+ *,
+ init: bool = True,
+ setparent: bool = True,
+ warn_for_existing: bool = False,
+ ) -> MapperProperty[Any]:
+ descriptor_props = util.preloaded.orm_descriptor_props
+ self._log(
+ "_configure_property(%s, %s)", key, prop_arg.__class__.__name__
+ )
+
+ if not isinstance(prop_arg, MapperProperty):
+ prop: MapperProperty[Any] = self._property_from_column(
+ key, prop_arg
+ )
+ else:
+ prop = prop_arg
+
+ if isinstance(prop, properties.ColumnProperty):
+ col = self.persist_selectable.corresponding_column(prop.columns[0])
+
+ # if the column is not present in the mapped table,
+ # test if a column has been added after the fact to the
+ # parent table (or their parent, etc.) [ticket:1570]
+ if col is None and self.inherits:
+ path = [self]
+ for m in self.inherits.iterate_to_root():
+ col = m.local_table.corresponding_column(prop.columns[0])
+ if col is not None:
+ for m2 in path:
+ m2.persist_selectable._refresh_for_new_column(col)
+ col = self.persist_selectable.corresponding_column(
+ prop.columns[0]
+ )
+ break
+ path.append(m)
+
+ # subquery expression, column not present in the mapped
+ # selectable.
+ if col is None:
+ col = prop.columns[0]
+
+ # column is coming in after _readonly_props was
+ # initialized; check for 'readonly'
+ if hasattr(self, "_readonly_props") and (
+ not hasattr(col, "table")
+ or col.table not in self._cols_by_table
+ ):
+ self._readonly_props.add(prop)
+
+ else:
+ # if column is coming in after _cols_by_table was
+ # initialized, ensure the col is in the right set
+ if (
+ hasattr(self, "_cols_by_table")
+ and col.table in self._cols_by_table
+ and col not in self._cols_by_table[col.table]
+ ):
+ self._cols_by_table[col.table].add(col)
+
+ # if this properties.ColumnProperty represents the "polymorphic
+ # discriminator" column, mark it. We'll need this when rendering
+ # columns in SELECT statements.
+ if not hasattr(prop, "_is_polymorphic_discriminator"):
+ prop._is_polymorphic_discriminator = (
+ col is self.polymorphic_on
+ or prop.columns[0] is self.polymorphic_on
+ )
+
+ if isinstance(col, expression.Label):
+ # new in 1.4, get column property against expressions
+ # to be addressable in subqueries
+ col.key = col._tq_key_label = key
+
+ self.columns.add(col, key)
+
+ for col in prop.columns:
+ for proxy_col in col.proxy_set:
+ self._columntoproperty[proxy_col] = prop
+
+ if getattr(prop, "key", key) != key:
+ util.warn(
+ f"ORM mapped property {self.class_.__name__}.{prop.key} being "
+ "assigned to attribute "
+ f"{key!r} is already associated with "
+ f"attribute {prop.key!r}. The attribute will be de-associated "
+ f"from {prop.key!r}."
+ )
+
+ prop.key = key
+
+ if setparent:
+ prop.set_parent(self, init)
+
+ if key in self._props and getattr(
+ self._props[key], "_mapped_by_synonym", False
+ ):
+ syn = self._props[key]._mapped_by_synonym
+ raise sa_exc.ArgumentError(
+ "Can't call map_column=True for synonym %r=%r, "
+ "a ColumnProperty already exists keyed to the name "
+ "%r for column %r" % (syn, key, key, syn)
+ )
+
+ # replacement cases
+
+ # case one: prop is replacing a prop that we have mapped. this is
+ # independent of whatever might be in the actual class dictionary
+ if (
+ key in self._props
+ and not isinstance(
+ self._props[key], descriptor_props.ConcreteInheritedProperty
+ )
+ and not isinstance(prop, descriptor_props.SynonymProperty)
+ ):
+ if warn_for_existing:
+ util.warn_deprecated(
+ f"User-placed attribute {self.class_.__name__}.{key} on "
+ f"{self} is replacing an existing ORM-mapped attribute. "
+ "Behavior is not fully defined in this case. This "
+ "use is deprecated and will raise an error in a future "
+ "release",
+ "2.0",
+ )
+ oldprop = self._props[key]
+ self._path_registry.pop(oldprop, None)
+
+ # case two: prop is replacing an attribute on the class of some kind.
+ # we have to be more careful here since it's normal when using
+ # Declarative that all the "declared attributes" on the class
+ # get replaced.
+ elif (
+ warn_for_existing
+ and self.class_.__dict__.get(key, None) is not None
+ and not isinstance(prop, descriptor_props.SynonymProperty)
+ and not isinstance(
+ self._props.get(key, None),
+ descriptor_props.ConcreteInheritedProperty,
+ )
+ ):
+ util.warn_deprecated(
+ f"User-placed attribute {self.class_.__name__}.{key} on "
+ f"{self} is replacing an existing class-bound "
+ "attribute of the same name. "
+ "Behavior is not fully defined in this case. This "
+ "use is deprecated and will raise an error in a future "
+ "release",
+ "2.0",
+ )
+
+ self._props[key] = prop
+
+ if not self.non_primary:
+ prop.instrument_class(self)
+
+ for mapper in self._inheriting_mappers:
+ mapper._adapt_inherited_property(key, prop, init)
+
+ if init:
+ prop.init()
+ prop.post_instrument_class(self)
+
+ if self.configured:
+ self._expire_memoizations()
+
+ return prop
+
+ def _make_prop_from_column(
+ self,
+ key: str,
+ column: Union[
+ Sequence[KeyedColumnElement[Any]], KeyedColumnElement[Any]
+ ],
+ ) -> ColumnProperty[Any]:
+ columns = util.to_list(column)
+ mapped_column = []
+ for c in columns:
+ mc = self.persist_selectable.corresponding_column(c)
+ if mc is None:
+ mc = self.local_table.corresponding_column(c)
+ if mc is not None:
+ # if the column is in the local table but not the
+ # mapped table, this corresponds to adding a
+ # column after the fact to the local table.
+ # [ticket:1523]
+ self.persist_selectable._refresh_for_new_column(mc)
+ mc = self.persist_selectable.corresponding_column(c)
+ if mc is None:
+ raise sa_exc.ArgumentError(
+ "When configuring property '%s' on %s, "
+ "column '%s' is not represented in the mapper's "
+ "table. Use the `column_property()` function to "
+ "force this column to be mapped as a read-only "
+ "attribute." % (key, self, c)
+ )
+ mapped_column.append(mc)
+ return properties.ColumnProperty(*mapped_column)
+
+ def _reconcile_prop_with_incoming_columns(
+ self,
+ key: str,
+ existing_prop: MapperProperty[Any],
+ warn_only: bool,
+ incoming_prop: Optional[ColumnProperty[Any]] = None,
+ single_column: Optional[KeyedColumnElement[Any]] = None,
+ ) -> ColumnProperty[Any]:
+ if incoming_prop and (
+ self.concrete
+ or not isinstance(existing_prop, properties.ColumnProperty)
+ ):
+ return incoming_prop
+
+ existing_column = existing_prop.columns[0]
+
+ if incoming_prop and existing_column in incoming_prop.columns:
+ return incoming_prop
+
+ if incoming_prop is None:
+ assert single_column is not None
+ incoming_column = single_column
+ equated_pair_key = (existing_prop.columns[0], incoming_column)
+ else:
+ assert single_column is None
+ incoming_column = incoming_prop.columns[0]
+ equated_pair_key = (incoming_column, existing_prop.columns[0])
+
+ if (
+ (
+ not self._inherits_equated_pairs
+ or (equated_pair_key not in self._inherits_equated_pairs)
+ )
+ and not existing_column.shares_lineage(incoming_column)
+ and existing_column is not self.version_id_col
+ and incoming_column is not self.version_id_col
+ ):
+ msg = (
+ "Implicitly combining column %s with column "
+ "%s under attribute '%s'. Please configure one "
+ "or more attributes for these same-named columns "
+ "explicitly."
+ % (
+ existing_prop.columns[-1],
+ incoming_column,
+ key,
+ )
+ )
+ if warn_only:
+ util.warn(msg)
+ else:
+ raise sa_exc.InvalidRequestError(msg)
+
+ # existing properties.ColumnProperty from an inheriting
+ # mapper. make a copy and append our column to it
+ # breakpoint()
+ new_prop = existing_prop.copy()
+
+ new_prop.columns.insert(0, incoming_column)
+ self._log(
+ "inserting column to existing list "
+ "in properties.ColumnProperty %s",
+ key,
+ )
+ return new_prop # type: ignore
+
+ @util.preload_module("sqlalchemy.orm.descriptor_props")
+ def _property_from_column(
+ self,
+ key: str,
+ column: KeyedColumnElement[Any],
+ ) -> ColumnProperty[Any]:
+ """generate/update a :class:`.ColumnProperty` given a
+ :class:`_schema.Column` or other SQL expression object."""
+
+ descriptor_props = util.preloaded.orm_descriptor_props
+
+ prop = self._props.get(key)
+
+ if isinstance(prop, properties.ColumnProperty):
+ return self._reconcile_prop_with_incoming_columns(
+ key,
+ prop,
+ single_column=column,
+ warn_only=prop.parent is not self,
+ )
+ elif prop is None or isinstance(
+ prop, descriptor_props.ConcreteInheritedProperty
+ ):
+ return self._make_prop_from_column(key, column)
+ else:
+ raise sa_exc.ArgumentError(
+ "WARNING: when configuring property '%s' on %s, "
+ "column '%s' conflicts with property '%r'. "
+ "To resolve this, map the column to the class under a "
+ "different name in the 'properties' dictionary. Or, "
+ "to remove all awareness of the column entirely "
+ "(including its availability as a foreign key), "
+ "use the 'include_properties' or 'exclude_properties' "
+ "mapper arguments to control specifically which table "
+ "columns get mapped." % (key, self, column.key, prop)
+ )
+
+ @util.langhelpers.tag_method_for_warnings(
+ "This warning originated from the `configure_mappers()` process, "
+ "which was invoked automatically in response to a user-initiated "
+ "operation.",
+ sa_exc.SAWarning,
+ )
+ def _check_configure(self) -> None:
+ if self.registry._new_mappers:
+ _configure_registries({self.registry}, cascade=True)
+
+ def _post_configure_properties(self) -> None:
+ """Call the ``init()`` method on all ``MapperProperties``
+ attached to this mapper.
+
+ This is a deferred configuration step which is intended
+ to execute once all mappers have been constructed.
+
+ """
+
+ self._log("_post_configure_properties() started")
+ l = [(key, prop) for key, prop in self._props.items()]
+ for key, prop in l:
+ self._log("initialize prop %s", key)
+
+ if prop.parent is self and not prop._configure_started:
+ prop.init()
+
+ if prop._configure_finished:
+ prop.post_instrument_class(self)
+
+ self._log("_post_configure_properties() complete")
+ self.configured = True
+
+ def add_properties(self, dict_of_properties):
+ """Add the given dictionary of properties to this mapper,
+ using `add_property`.
+
+ """
+ for key, value in dict_of_properties.items():
+ self.add_property(key, value)
+
+ def add_property(
+ self, key: str, prop: Union[Column[Any], MapperProperty[Any]]
+ ) -> None:
+ """Add an individual MapperProperty to this mapper.
+
+ If the mapper has not been configured yet, just adds the
+ property to the initial properties dictionary sent to the
+ constructor. If this Mapper has already been configured, then
+ the given MapperProperty is configured immediately.
+
+ """
+ prop = self._configure_property(
+ key, prop, init=self.configured, warn_for_existing=True
+ )
+ assert isinstance(prop, MapperProperty)
+ self._init_properties[key] = prop
+
+ def _expire_memoizations(self) -> None:
+ for mapper in self.iterate_to_root():
+ mapper._reset_memoizations()
+
+ @property
+ def _log_desc(self) -> str:
+ return (
+ "("
+ + self.class_.__name__
+ + "|"
+ + (
+ self.local_table is not None
+ and self.local_table.description
+ or str(self.local_table)
+ )
+ + (self.non_primary and "|non-primary" or "")
+ + ")"
+ )
+
+ def _log(self, msg: str, *args: Any) -> None:
+ self.logger.info("%s " + msg, *((self._log_desc,) + args))
+
+ def _log_debug(self, msg: str, *args: Any) -> None:
+ self.logger.debug("%s " + msg, *((self._log_desc,) + args))
+
+ def __repr__(self) -> str:
+ return "<Mapper at 0x%x; %s>" % (id(self), self.class_.__name__)
+
+ def __str__(self) -> str:
+ return "Mapper[%s%s(%s)]" % (
+ self.class_.__name__,
+ self.non_primary and " (non-primary)" or "",
+ (
+ self.local_table.description
+ if self.local_table is not None
+ else self.persist_selectable.description
+ ),
+ )
+
+ def _is_orphan(self, state: InstanceState[_O]) -> bool:
+ orphan_possible = False
+ for mapper in self.iterate_to_root():
+ for key, cls in mapper._delete_orphans:
+ orphan_possible = True
+
+ has_parent = attributes.manager_of_class(cls).has_parent(
+ state, key, optimistic=state.has_identity
+ )
+
+ if self.legacy_is_orphan and has_parent:
+ return False
+ elif not self.legacy_is_orphan and not has_parent:
+ return True
+
+ if self.legacy_is_orphan:
+ return orphan_possible
+ else:
+ return False
+
+ def has_property(self, key: str) -> bool:
+ return key in self._props
+
+ def get_property(
+ self, key: str, _configure_mappers: bool = False
+ ) -> MapperProperty[Any]:
+ """return a MapperProperty associated with the given key."""
+
+ if _configure_mappers:
+ self._check_configure()
+
+ try:
+ return self._props[key]
+ except KeyError as err:
+ raise sa_exc.InvalidRequestError(
+ f"Mapper '{self}' has no property '{key}'. If this property "
+ "was indicated from other mappers or configure events, ensure "
+ "registry.configure() has been called."
+ ) from err
+
+ def get_property_by_column(
+ self, column: ColumnElement[_T]
+ ) -> MapperProperty[_T]:
+ """Given a :class:`_schema.Column` object, return the
+ :class:`.MapperProperty` which maps this column."""
+
+ return self._columntoproperty[column]
+
+ @property
+ def iterate_properties(self):
+ """return an iterator of all MapperProperty objects."""
+
+ return iter(self._props.values())
+
+ def _mappers_from_spec(
+ self, spec: Any, selectable: Optional[FromClause]
+ ) -> Sequence[Mapper[Any]]:
+ """given a with_polymorphic() argument, return the set of mappers it
+ represents.
+
+ Trims the list of mappers to just those represented within the given
+ selectable, if present. This helps some more legacy-ish mappings.
+
+ """
+ if spec == "*":
+ mappers = list(self.self_and_descendants)
+ elif spec:
+ mapper_set = set()
+ for m in util.to_list(spec):
+ m = _class_to_mapper(m)
+ if not m.isa(self):
+ raise sa_exc.InvalidRequestError(
+ "%r does not inherit from %r" % (m, self)
+ )
+
+ if selectable is None:
+ mapper_set.update(m.iterate_to_root())
+ else:
+ mapper_set.add(m)
+ mappers = [m for m in self.self_and_descendants if m in mapper_set]
+ else:
+ mappers = []
+
+ if selectable is not None:
+ tables = set(
+ sql_util.find_tables(selectable, include_aliases=True)
+ )
+ mappers = [m for m in mappers if m.local_table in tables]
+ return mappers
+
+ def _selectable_from_mappers(
+ self, mappers: Iterable[Mapper[Any]], innerjoin: bool
+ ) -> FromClause:
+ """given a list of mappers (assumed to be within this mapper's
+ inheritance hierarchy), construct an outerjoin amongst those mapper's
+ mapped tables.
+
+ """
+ from_obj = self.persist_selectable
+ for m in mappers:
+ if m is self:
+ continue
+ if m.concrete:
+ raise sa_exc.InvalidRequestError(
+ "'with_polymorphic()' requires 'selectable' argument "
+ "when concrete-inheriting mappers are used."
+ )
+ elif not m.single:
+ if innerjoin:
+ from_obj = from_obj.join(
+ m.local_table, m.inherit_condition
+ )
+ else:
+ from_obj = from_obj.outerjoin(
+ m.local_table, m.inherit_condition
+ )
+
+ return from_obj
+
+ @HasMemoized.memoized_attribute
+ def _version_id_has_server_side_value(self) -> bool:
+ vid_col = self.version_id_col
+
+ if vid_col is None:
+ return False
+
+ elif not isinstance(vid_col, Column):
+ return True
+ else:
+ return vid_col.server_default is not None or (
+ vid_col.default is not None
+ and (
+ not vid_col.default.is_scalar
+ and not vid_col.default.is_callable
+ )
+ )
+
+ @HasMemoized.memoized_attribute
+ def _single_table_criterion(self):
+ if self.single and self.inherits and self.polymorphic_on is not None:
+ return self.polymorphic_on._annotate(
+ {"parententity": self, "parentmapper": self}
+ ).in_(
+ [
+ m.polymorphic_identity
+ for m in self.self_and_descendants
+ if not m.polymorphic_abstract
+ ]
+ )
+ else:
+ return None
+
+ @HasMemoized.memoized_attribute
+ def _has_aliased_polymorphic_fromclause(self):
+ """return True if with_polymorphic[1] is an aliased fromclause,
+ like a subquery.
+
+ As of #8168, polymorphic adaption with ORMAdapter is used only
+ if this is present.
+
+ """
+ return self.with_polymorphic and isinstance(
+ self.with_polymorphic[1],
+ expression.AliasedReturnsRows,
+ )
+
+ @HasMemoized.memoized_attribute
+ def _should_select_with_poly_adapter(self):
+ """determine if _MapperEntity or _ORMColumnEntity will need to use
+ polymorphic adaption when setting up a SELECT as well as fetching
+ rows for mapped classes and subclasses against this Mapper.
+
+ moved here from context.py for #8456 to generalize the ruleset
+ for this condition.
+
+ """
+
+ # this has been simplified as of #8456.
+ # rule is: if we have a with_polymorphic or a concrete-style
+ # polymorphic selectable, *or* if the base mapper has either of those,
+ # we turn on the adaption thing. if not, we do *no* adaption.
+ #
+ # (UPDATE for #8168: the above comment was not accurate, as we were
+ # still saying "do polymorphic" if we were using an auto-generated
+ # flattened JOIN for with_polymorphic.)
+ #
+ # this splits the behavior among the "regular" joined inheritance
+ # and single inheritance mappers, vs. the "weird / difficult"
+ # concrete and joined inh mappings that use a with_polymorphic of
+ # some kind or polymorphic_union.
+ #
+ # note we have some tests in test_polymorphic_rel that query against
+ # a subclass, then refer to the superclass that has a with_polymorphic
+ # on it (such as test_join_from_polymorphic_explicit_aliased_three).
+ # these tests actually adapt the polymorphic selectable (like, the
+ # UNION or the SELECT subquery with JOIN in it) to be just the simple
+ # subclass table. Hence even if we are a "plain" inheriting mapper
+ # but our base has a wpoly on it, we turn on adaption. This is a
+ # legacy case we should probably disable.
+ #
+ #
+ # UPDATE: simplified way more as of #8168. polymorphic adaption
+ # is turned off even if with_polymorphic is set, as long as there
+ # is no user-defined aliased selectable / subquery configured.
+ # this scales back the use of polymorphic adaption in practice
+ # to basically no cases except for concrete inheritance with a
+ # polymorphic base class.
+ #
+ return (
+ self._has_aliased_polymorphic_fromclause
+ or self._requires_row_aliasing
+ or (self.base_mapper._has_aliased_polymorphic_fromclause)
+ or self.base_mapper._requires_row_aliasing
+ )
+
+ @HasMemoized.memoized_attribute
+ def _with_polymorphic_mappers(self) -> Sequence[Mapper[Any]]:
+ self._check_configure()
+
+ if not self.with_polymorphic:
+ return []
+ return self._mappers_from_spec(*self.with_polymorphic)
+
+ @HasMemoized.memoized_attribute
+ def _post_inspect(self):
+ """This hook is invoked by attribute inspection.
+
+ E.g. when Query calls:
+
+ coercions.expect(roles.ColumnsClauseRole, ent, keep_inspect=True)
+
+ This allows the inspection process run a configure mappers hook.
+
+ """
+ self._check_configure()
+
+ @HasMemoized_ro_memoized_attribute
+ def _with_polymorphic_selectable(self) -> FromClause:
+ if not self.with_polymorphic:
+ return self.persist_selectable
+
+ spec, selectable = self.with_polymorphic
+ if selectable is not None:
+ return selectable
+ else:
+ return self._selectable_from_mappers(
+ self._mappers_from_spec(spec, selectable), False
+ )
+
+ with_polymorphic_mappers = _with_polymorphic_mappers
+ """The list of :class:`_orm.Mapper` objects included in the
+ default "polymorphic" query.
+
+ """
+
+ @HasMemoized_ro_memoized_attribute
+ def _insert_cols_evaluating_none(self):
+ return {
+ table: frozenset(
+ col for col in columns if col.type.should_evaluate_none
+ )
+ for table, columns in self._cols_by_table.items()
+ }
+
+ @HasMemoized.memoized_attribute
+ def _insert_cols_as_none(self):
+ return {
+ table: frozenset(
+ col.key
+ for col in columns
+ if not col.primary_key
+ and not col.server_default
+ and not col.default
+ and not col.type.should_evaluate_none
+ )
+ for table, columns in self._cols_by_table.items()
+ }
+
+ @HasMemoized.memoized_attribute
+ def _propkey_to_col(self):
+ return {
+ table: {self._columntoproperty[col].key: col for col in columns}
+ for table, columns in self._cols_by_table.items()
+ }
+
+ @HasMemoized.memoized_attribute
+ def _pk_keys_by_table(self):
+ return {
+ table: frozenset([col.key for col in pks])
+ for table, pks in self._pks_by_table.items()
+ }
+
+ @HasMemoized.memoized_attribute
+ def _pk_attr_keys_by_table(self):
+ return {
+ table: frozenset([self._columntoproperty[col].key for col in pks])
+ for table, pks in self._pks_by_table.items()
+ }
+
+ @HasMemoized.memoized_attribute
+ def _server_default_cols(
+ self,
+ ) -> Mapping[FromClause, FrozenSet[Column[Any]]]:
+ return {
+ table: frozenset(
+ [
+ col
+ for col in cast("Iterable[Column[Any]]", columns)
+ if col.server_default is not None
+ or (
+ col.default is not None
+ and col.default.is_clause_element
+ )
+ ]
+ )
+ for table, columns in self._cols_by_table.items()
+ }
+
+ @HasMemoized.memoized_attribute
+ def _server_onupdate_default_cols(
+ self,
+ ) -> Mapping[FromClause, FrozenSet[Column[Any]]]:
+ return {
+ table: frozenset(
+ [
+ col
+ for col in cast("Iterable[Column[Any]]", columns)
+ if col.server_onupdate is not None
+ or (
+ col.onupdate is not None
+ and col.onupdate.is_clause_element
+ )
+ ]
+ )
+ for table, columns in self._cols_by_table.items()
+ }
+
+ @HasMemoized.memoized_attribute
+ def _server_default_col_keys(self) -> Mapping[FromClause, FrozenSet[str]]:
+ return {
+ table: frozenset(col.key for col in cols if col.key is not None)
+ for table, cols in self._server_default_cols.items()
+ }
+
+ @HasMemoized.memoized_attribute
+ def _server_onupdate_default_col_keys(
+ self,
+ ) -> Mapping[FromClause, FrozenSet[str]]:
+ return {
+ table: frozenset(col.key for col in cols if col.key is not None)
+ for table, cols in self._server_onupdate_default_cols.items()
+ }
+
+ @HasMemoized.memoized_attribute
+ def _server_default_plus_onupdate_propkeys(self) -> Set[str]:
+ result: Set[str] = set()
+
+ col_to_property = self._columntoproperty
+ for table, columns in self._server_default_cols.items():
+ result.update(
+ col_to_property[col].key
+ for col in columns.intersection(col_to_property)
+ )
+ for table, columns in self._server_onupdate_default_cols.items():
+ result.update(
+ col_to_property[col].key
+ for col in columns.intersection(col_to_property)
+ )
+ return result
+
+ @HasMemoized.memoized_instancemethod
+ def __clause_element__(self):
+ annotations: Dict[str, Any] = {
+ "entity_namespace": self,
+ "parententity": self,
+ "parentmapper": self,
+ }
+ if self.persist_selectable is not self.local_table:
+ # joined table inheritance, with polymorphic selectable,
+ # etc.
+ annotations["dml_table"] = self.local_table._annotate(
+ {
+ "entity_namespace": self,
+ "parententity": self,
+ "parentmapper": self,
+ }
+ )._set_propagate_attrs(
+ {"compile_state_plugin": "orm", "plugin_subject": self}
+ )
+
+ return self.selectable._annotate(annotations)._set_propagate_attrs(
+ {"compile_state_plugin": "orm", "plugin_subject": self}
+ )
+
+ @util.memoized_property
+ def select_identity_token(self):
+ return (
+ expression.null()
+ ._annotate(
+ {
+ "entity_namespace": self,
+ "parententity": self,
+ "parentmapper": self,
+ "identity_token": True,
+ }
+ )
+ ._set_propagate_attrs(
+ {"compile_state_plugin": "orm", "plugin_subject": self}
+ )
+ )
+
+ @property
+ def selectable(self) -> FromClause:
+ """The :class:`_schema.FromClause` construct this
+ :class:`_orm.Mapper` selects from by default.
+
+ Normally, this is equivalent to :attr:`.persist_selectable`, unless
+ the ``with_polymorphic`` feature is in use, in which case the
+ full "polymorphic" selectable is returned.
+
+ """
+ return self._with_polymorphic_selectable
+
+ def _with_polymorphic_args(
+ self,
+ spec: Any = None,
+ selectable: Union[Literal[False, None], FromClause] = False,
+ innerjoin: bool = False,
+ ) -> Tuple[Sequence[Mapper[Any]], FromClause]:
+ if selectable not in (None, False):
+ selectable = coercions.expect(
+ roles.StrictFromClauseRole, selectable, allow_select=True
+ )
+
+ if self.with_polymorphic:
+ if not spec:
+ spec = self.with_polymorphic[0]
+ if selectable is False:
+ selectable = self.with_polymorphic[1]
+ elif selectable is False:
+ selectable = None
+ mappers = self._mappers_from_spec(spec, selectable)
+ if selectable is not None:
+ return mappers, selectable
+ else:
+ return mappers, self._selectable_from_mappers(mappers, innerjoin)
+
+ @HasMemoized.memoized_attribute
+ def _polymorphic_properties(self):
+ return list(
+ self._iterate_polymorphic_properties(
+ self._with_polymorphic_mappers
+ )
+ )
+
+ @property
+ def _all_column_expressions(self):
+ poly_properties = self._polymorphic_properties
+ adapter = self._polymorphic_adapter
+
+ return [
+ adapter.columns[c] if adapter else c
+ for prop in poly_properties
+ if isinstance(prop, properties.ColumnProperty)
+ and prop._renders_in_subqueries
+ for c in prop.columns
+ ]
+
+ def _columns_plus_keys(self, polymorphic_mappers=()):
+ if polymorphic_mappers:
+ poly_properties = self._iterate_polymorphic_properties(
+ polymorphic_mappers
+ )
+ else:
+ poly_properties = self._polymorphic_properties
+
+ return [
+ (prop.key, prop.columns[0])
+ for prop in poly_properties
+ if isinstance(prop, properties.ColumnProperty)
+ ]
+
+ @HasMemoized.memoized_attribute
+ def _polymorphic_adapter(self) -> Optional[orm_util.ORMAdapter]:
+ if self._has_aliased_polymorphic_fromclause:
+ return orm_util.ORMAdapter(
+ orm_util._TraceAdaptRole.MAPPER_POLYMORPHIC_ADAPTER,
+ self,
+ selectable=self.selectable,
+ equivalents=self._equivalent_columns,
+ limit_on_entity=False,
+ )
+ else:
+ return None
+
+ def _iterate_polymorphic_properties(self, mappers=None):
+ """Return an iterator of MapperProperty objects which will render into
+ a SELECT."""
+ if mappers is None:
+ mappers = self._with_polymorphic_mappers
+
+ if not mappers:
+ for c in self.iterate_properties:
+ yield c
+ else:
+ # in the polymorphic case, filter out discriminator columns
+ # from other mappers, as these are sometimes dependent on that
+ # mapper's polymorphic selectable (which we don't want rendered)
+ for c in util.unique_list(
+ chain(
+ *[
+ list(mapper.iterate_properties)
+ for mapper in [self] + mappers
+ ]
+ )
+ ):
+ if getattr(c, "_is_polymorphic_discriminator", False) and (
+ self.polymorphic_on is None
+ or c.columns[0] is not self.polymorphic_on
+ ):
+ continue
+ yield c
+
+ @HasMemoized.memoized_attribute
+ def attrs(self) -> util.ReadOnlyProperties[MapperProperty[Any]]:
+ """A namespace of all :class:`.MapperProperty` objects
+ associated this mapper.
+
+ This is an object that provides each property based on
+ its key name. For instance, the mapper for a
+ ``User`` class which has ``User.name`` attribute would
+ provide ``mapper.attrs.name``, which would be the
+ :class:`.ColumnProperty` representing the ``name``
+ column. The namespace object can also be iterated,
+ which would yield each :class:`.MapperProperty`.
+
+ :class:`_orm.Mapper` has several pre-filtered views
+ of this attribute which limit the types of properties
+ returned, including :attr:`.synonyms`, :attr:`.column_attrs`,
+ :attr:`.relationships`, and :attr:`.composites`.
+
+ .. warning::
+
+ The :attr:`_orm.Mapper.attrs` accessor namespace is an
+ instance of :class:`.OrderedProperties`. This is
+ a dictionary-like object which includes a small number of
+ named methods such as :meth:`.OrderedProperties.items`
+ and :meth:`.OrderedProperties.values`. When
+ accessing attributes dynamically, favor using the dict-access
+ scheme, e.g. ``mapper.attrs[somename]`` over
+ ``getattr(mapper.attrs, somename)`` to avoid name collisions.
+
+ .. seealso::
+
+ :attr:`_orm.Mapper.all_orm_descriptors`
+
+ """
+
+ self._check_configure()
+ return util.ReadOnlyProperties(self._props)
+
+ @HasMemoized.memoized_attribute
+ def all_orm_descriptors(self) -> util.ReadOnlyProperties[InspectionAttr]:
+ """A namespace of all :class:`.InspectionAttr` attributes associated
+ with the mapped class.
+
+ These attributes are in all cases Python :term:`descriptors`
+ associated with the mapped class or its superclasses.
+
+ This namespace includes attributes that are mapped to the class
+ as well as attributes declared by extension modules.
+ It includes any Python descriptor type that inherits from
+ :class:`.InspectionAttr`. This includes
+ :class:`.QueryableAttribute`, as well as extension types such as
+ :class:`.hybrid_property`, :class:`.hybrid_method` and
+ :class:`.AssociationProxy`.
+
+ To distinguish between mapped attributes and extension attributes,
+ the attribute :attr:`.InspectionAttr.extension_type` will refer
+ to a constant that distinguishes between different extension types.
+
+ The sorting of the attributes is based on the following rules:
+
+ 1. Iterate through the class and its superclasses in order from
+ subclass to superclass (i.e. iterate through ``cls.__mro__``)
+
+ 2. For each class, yield the attributes in the order in which they
+ appear in ``__dict__``, with the exception of those in step
+ 3 below. In Python 3.6 and above this ordering will be the
+ same as that of the class' construction, with the exception
+ of attributes that were added after the fact by the application
+ or the mapper.
+
+ 3. If a certain attribute key is also in the superclass ``__dict__``,
+ then it's included in the iteration for that class, and not the
+ class in which it first appeared.
+
+ The above process produces an ordering that is deterministic in terms
+ of the order in which attributes were assigned to the class.
+
+ .. versionchanged:: 1.3.19 ensured deterministic ordering for
+ :meth:`_orm.Mapper.all_orm_descriptors`.
+
+ When dealing with a :class:`.QueryableAttribute`, the
+ :attr:`.QueryableAttribute.property` attribute refers to the
+ :class:`.MapperProperty` property, which is what you get when
+ referring to the collection of mapped properties via
+ :attr:`_orm.Mapper.attrs`.
+
+ .. warning::
+
+ The :attr:`_orm.Mapper.all_orm_descriptors`
+ accessor namespace is an
+ instance of :class:`.OrderedProperties`. This is
+ a dictionary-like object which includes a small number of
+ named methods such as :meth:`.OrderedProperties.items`
+ and :meth:`.OrderedProperties.values`. When
+ accessing attributes dynamically, favor using the dict-access
+ scheme, e.g. ``mapper.all_orm_descriptors[somename]`` over
+ ``getattr(mapper.all_orm_descriptors, somename)`` to avoid name
+ collisions.
+
+ .. seealso::
+
+ :attr:`_orm.Mapper.attrs`
+
+ """
+ return util.ReadOnlyProperties(
+ dict(self.class_manager._all_sqla_attributes())
+ )
+
+ @HasMemoized.memoized_attribute
+ @util.preload_module("sqlalchemy.orm.descriptor_props")
+ def _pk_synonyms(self) -> Dict[str, str]:
+ """return a dictionary of {syn_attribute_name: pk_attr_name} for
+ all synonyms that refer to primary key columns
+
+ """
+ descriptor_props = util.preloaded.orm_descriptor_props
+
+ pk_keys = {prop.key for prop in self._identity_key_props}
+
+ return {
+ syn.key: syn.name
+ for k, syn in self._props.items()
+ if isinstance(syn, descriptor_props.SynonymProperty)
+ and syn.name in pk_keys
+ }
+
+ @HasMemoized.memoized_attribute
+ @util.preload_module("sqlalchemy.orm.descriptor_props")
+ def synonyms(self) -> util.ReadOnlyProperties[SynonymProperty[Any]]:
+ """Return a namespace of all :class:`.Synonym`
+ properties maintained by this :class:`_orm.Mapper`.
+
+ .. seealso::
+
+ :attr:`_orm.Mapper.attrs` - namespace of all
+ :class:`.MapperProperty`
+ objects.
+
+ """
+ descriptor_props = util.preloaded.orm_descriptor_props
+
+ return self._filter_properties(descriptor_props.SynonymProperty)
+
+ @property
+ def entity_namespace(self):
+ return self.class_
+
+ @HasMemoized.memoized_attribute
+ def column_attrs(self) -> util.ReadOnlyProperties[ColumnProperty[Any]]:
+ """Return a namespace of all :class:`.ColumnProperty`
+ properties maintained by this :class:`_orm.Mapper`.
+
+ .. seealso::
+
+ :attr:`_orm.Mapper.attrs` - namespace of all
+ :class:`.MapperProperty`
+ objects.
+
+ """
+ return self._filter_properties(properties.ColumnProperty)
+
+ @HasMemoized.memoized_attribute
+ @util.preload_module("sqlalchemy.orm.relationships")
+ def relationships(
+ self,
+ ) -> util.ReadOnlyProperties[RelationshipProperty[Any]]:
+ """A namespace of all :class:`.Relationship` properties
+ maintained by this :class:`_orm.Mapper`.
+
+ .. warning::
+
+ the :attr:`_orm.Mapper.relationships` accessor namespace is an
+ instance of :class:`.OrderedProperties`. This is
+ a dictionary-like object which includes a small number of
+ named methods such as :meth:`.OrderedProperties.items`
+ and :meth:`.OrderedProperties.values`. When
+ accessing attributes dynamically, favor using the dict-access
+ scheme, e.g. ``mapper.relationships[somename]`` over
+ ``getattr(mapper.relationships, somename)`` to avoid name
+ collisions.
+
+ .. seealso::
+
+ :attr:`_orm.Mapper.attrs` - namespace of all
+ :class:`.MapperProperty`
+ objects.
+
+ """
+ return self._filter_properties(
+ util.preloaded.orm_relationships.RelationshipProperty
+ )
+
+ @HasMemoized.memoized_attribute
+ @util.preload_module("sqlalchemy.orm.descriptor_props")
+ def composites(self) -> util.ReadOnlyProperties[CompositeProperty[Any]]:
+ """Return a namespace of all :class:`.Composite`
+ properties maintained by this :class:`_orm.Mapper`.
+
+ .. seealso::
+
+ :attr:`_orm.Mapper.attrs` - namespace of all
+ :class:`.MapperProperty`
+ objects.
+
+ """
+ return self._filter_properties(
+ util.preloaded.orm_descriptor_props.CompositeProperty
+ )
+
+ def _filter_properties(
+ self, type_: Type[_MP]
+ ) -> util.ReadOnlyProperties[_MP]:
+ self._check_configure()
+ return util.ReadOnlyProperties(
+ util.OrderedDict(
+ (k, v) for k, v in self._props.items() if isinstance(v, type_)
+ )
+ )
+
+ @HasMemoized.memoized_attribute
+ def _get_clause(self):
+ """create a "get clause" based on the primary key. this is used
+ by query.get() and many-to-one lazyloads to load this item
+ by primary key.
+
+ """
+ params = [
+ (
+ primary_key,
+ sql.bindparam("pk_%d" % idx, type_=primary_key.type),
+ )
+ for idx, primary_key in enumerate(self.primary_key, 1)
+ ]
+ return (
+ sql.and_(*[k == v for (k, v) in params]),
+ util.column_dict(params),
+ )
+
+ @HasMemoized.memoized_attribute
+ def _equivalent_columns(self) -> _EquivalentColumnMap:
+ """Create a map of all equivalent columns, based on
+ the determination of column pairs that are equated to
+ one another based on inherit condition. This is designed
+ to work with the queries that util.polymorphic_union
+ comes up with, which often don't include the columns from
+ the base table directly (including the subclass table columns
+ only).
+
+ The resulting structure is a dictionary of columns mapped
+ to lists of equivalent columns, e.g.::
+
+ {
+ tablea.col1:
+ {tableb.col1, tablec.col1},
+ tablea.col2:
+ {tabled.col2}
+ }
+
+ """
+ result: _EquivalentColumnMap = {}
+
+ def visit_binary(binary):
+ if binary.operator == operators.eq:
+ if binary.left in result:
+ result[binary.left].add(binary.right)
+ else:
+ result[binary.left] = {binary.right}
+ if binary.right in result:
+ result[binary.right].add(binary.left)
+ else:
+ result[binary.right] = {binary.left}
+
+ for mapper in self.base_mapper.self_and_descendants:
+ if mapper.inherit_condition is not None:
+ visitors.traverse(
+ mapper.inherit_condition, {}, {"binary": visit_binary}
+ )
+
+ return result
+
+ def _is_userland_descriptor(self, assigned_name: str, obj: Any) -> bool:
+ if isinstance(
+ obj,
+ (
+ _MappedAttribute,
+ instrumentation.ClassManager,
+ expression.ColumnElement,
+ ),
+ ):
+ return False
+ else:
+ return assigned_name not in self._dataclass_fields
+
+ @HasMemoized.memoized_attribute
+ def _dataclass_fields(self):
+ return [f.name for f in util.dataclass_fields(self.class_)]
+
+ def _should_exclude(self, name, assigned_name, local, column):
+ """determine whether a particular property should be implicitly
+ present on the class.
+
+ This occurs when properties are propagated from an inherited class, or
+ are applied from the columns present in the mapped table.
+
+ """
+
+ if column is not None and sql_base._never_select_column(column):
+ return True
+
+ # check for class-bound attributes and/or descriptors,
+ # either local or from an inherited class
+ # ignore dataclass field default values
+ if local:
+ if self.class_.__dict__.get(
+ assigned_name, None
+ ) is not None and self._is_userland_descriptor(
+ assigned_name, self.class_.__dict__[assigned_name]
+ ):
+ return True
+ else:
+ attr = self.class_manager._get_class_attr_mro(assigned_name, None)
+ if attr is not None and self._is_userland_descriptor(
+ assigned_name, attr
+ ):
+ return True
+
+ if (
+ self.include_properties is not None
+ and name not in self.include_properties
+ and (column is None or column not in self.include_properties)
+ ):
+ self._log("not including property %s" % (name))
+ return True
+
+ if self.exclude_properties is not None and (
+ name in self.exclude_properties
+ or (column is not None and column in self.exclude_properties)
+ ):
+ self._log("excluding property %s" % (name))
+ return True
+
+ return False
+
+ def common_parent(self, other: Mapper[Any]) -> bool:
+ """Return true if the given mapper shares a
+ common inherited parent as this mapper."""
+
+ return self.base_mapper is other.base_mapper
+
+ def is_sibling(self, other: Mapper[Any]) -> bool:
+ """return true if the other mapper is an inheriting sibling to this
+ one. common parent but different branch
+
+ """
+ return (
+ self.base_mapper is other.base_mapper
+ and not self.isa(other)
+ and not other.isa(self)
+ )
+
+ def _canload(
+ self, state: InstanceState[Any], allow_subtypes: bool
+ ) -> bool:
+ s = self.primary_mapper()
+ if self.polymorphic_on is not None or allow_subtypes:
+ return _state_mapper(state).isa(s)
+ else:
+ return _state_mapper(state) is s
+
+ def isa(self, other: Mapper[Any]) -> bool:
+ """Return True if the this mapper inherits from the given mapper."""
+
+ m: Optional[Mapper[Any]] = self
+ while m and m is not other:
+ m = m.inherits
+ return bool(m)
+
+ def iterate_to_root(self) -> Iterator[Mapper[Any]]:
+ m: Optional[Mapper[Any]] = self
+ while m:
+ yield m
+ m = m.inherits
+
+ @HasMemoized.memoized_attribute
+ def self_and_descendants(self) -> Sequence[Mapper[Any]]:
+ """The collection including this mapper and all descendant mappers.
+
+ This includes not just the immediately inheriting mappers but
+ all their inheriting mappers as well.
+
+ """
+ descendants = []
+ stack = deque([self])
+ while stack:
+ item = stack.popleft()
+ descendants.append(item)
+ stack.extend(item._inheriting_mappers)
+ return util.WeakSequence(descendants)
+
+ def polymorphic_iterator(self) -> Iterator[Mapper[Any]]:
+ """Iterate through the collection including this mapper and
+ all descendant mappers.
+
+ This includes not just the immediately inheriting mappers but
+ all their inheriting mappers as well.
+
+ To iterate through an entire hierarchy, use
+ ``mapper.base_mapper.polymorphic_iterator()``.
+
+ """
+ return iter(self.self_and_descendants)
+
+ def primary_mapper(self) -> Mapper[Any]:
+ """Return the primary mapper corresponding to this mapper's class key
+ (class)."""
+
+ return self.class_manager.mapper
+
+ @property
+ def primary_base_mapper(self) -> Mapper[Any]:
+ return self.class_manager.mapper.base_mapper
+
+ def _result_has_identity_key(self, result, adapter=None):
+ pk_cols: Sequence[ColumnClause[Any]] = self.primary_key
+ if adapter:
+ pk_cols = [adapter.columns[c] for c in pk_cols]
+ rk = result.keys()
+ for col in pk_cols:
+ if col not in rk:
+ return False
+ else:
+ return True
+
+ def identity_key_from_row(
+ self,
+ row: Optional[Union[Row[Any], RowMapping]],
+ identity_token: Optional[Any] = None,
+ adapter: Optional[ORMAdapter] = None,
+ ) -> _IdentityKeyType[_O]:
+ """Return an identity-map key for use in storing/retrieving an
+ item from the identity map.
+
+ :param row: A :class:`.Row` or :class:`.RowMapping` produced from a
+ result set that selected from the ORM mapped primary key columns.
+
+ .. versionchanged:: 2.0
+ :class:`.Row` or :class:`.RowMapping` are accepted
+ for the "row" argument
+
+ """
+ pk_cols: Sequence[ColumnClause[Any]] = self.primary_key
+ if adapter:
+ pk_cols = [adapter.columns[c] for c in pk_cols]
+
+ if hasattr(row, "_mapping"):
+ mapping = row._mapping # type: ignore
+ else:
+ mapping = cast("Mapping[Any, Any]", row)
+
+ return (
+ self._identity_class,
+ tuple(mapping[column] for column in pk_cols), # type: ignore
+ identity_token,
+ )
+
+ def identity_key_from_primary_key(
+ self,
+ primary_key: Tuple[Any, ...],
+ identity_token: Optional[Any] = None,
+ ) -> _IdentityKeyType[_O]:
+ """Return an identity-map key for use in storing/retrieving an
+ item from an identity map.
+
+ :param primary_key: A list of values indicating the identifier.
+
+ """
+ return (
+ self._identity_class,
+ tuple(primary_key),
+ identity_token,
+ )
+
+ def identity_key_from_instance(self, instance: _O) -> _IdentityKeyType[_O]:
+ """Return the identity key for the given instance, based on
+ its primary key attributes.
+
+ If the instance's state is expired, calling this method
+ will result in a database check to see if the object has been deleted.
+ If the row no longer exists,
+ :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised.
+
+ This value is typically also found on the instance state under the
+ attribute name `key`.
+
+ """
+ state = attributes.instance_state(instance)
+ return self._identity_key_from_state(state, PassiveFlag.PASSIVE_OFF)
+
+ def _identity_key_from_state(
+ self,
+ state: InstanceState[_O],
+ passive: PassiveFlag = PassiveFlag.PASSIVE_RETURN_NO_VALUE,
+ ) -> _IdentityKeyType[_O]:
+ dict_ = state.dict
+ manager = state.manager
+ return (
+ self._identity_class,
+ tuple(
+ [
+ manager[prop.key].impl.get(state, dict_, passive)
+ for prop in self._identity_key_props
+ ]
+ ),
+ state.identity_token,
+ )
+
+ def primary_key_from_instance(self, instance: _O) -> Tuple[Any, ...]:
+ """Return the list of primary key values for the given
+ instance.
+
+ If the instance's state is expired, calling this method
+ will result in a database check to see if the object has been deleted.
+ If the row no longer exists,
+ :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised.
+
+ """
+ state = attributes.instance_state(instance)
+ identity_key = self._identity_key_from_state(
+ state, PassiveFlag.PASSIVE_OFF
+ )
+ return identity_key[1]
+
+ @HasMemoized.memoized_attribute
+ def _persistent_sortkey_fn(self):
+ key_fns = [col.type.sort_key_function for col in self.primary_key]
+
+ if set(key_fns).difference([None]):
+
+ def key(state):
+ return tuple(
+ key_fn(val) if key_fn is not None else val
+ for key_fn, val in zip(key_fns, state.key[1])
+ )
+
+ else:
+
+ def key(state):
+ return state.key[1]
+
+ return key
+
+ @HasMemoized.memoized_attribute
+ def _identity_key_props(self):
+ return [self._columntoproperty[col] for col in self.primary_key]
+
+ @HasMemoized.memoized_attribute
+ def _all_pk_cols(self):
+ collection: Set[ColumnClause[Any]] = set()
+ for table in self.tables:
+ collection.update(self._pks_by_table[table])
+ return collection
+
+ @HasMemoized.memoized_attribute
+ def _should_undefer_in_wildcard(self):
+ cols: Set[ColumnElement[Any]] = set(self.primary_key)
+ if self.polymorphic_on is not None:
+ cols.add(self.polymorphic_on)
+ return cols
+
+ @HasMemoized.memoized_attribute
+ def _primary_key_propkeys(self):
+ return {self._columntoproperty[col].key for col in self._all_pk_cols}
+
+ def _get_state_attr_by_column(
+ self,
+ state: InstanceState[_O],
+ dict_: _InstanceDict,
+ column: ColumnElement[Any],
+ passive: PassiveFlag = PassiveFlag.PASSIVE_RETURN_NO_VALUE,
+ ) -> Any:
+ prop = self._columntoproperty[column]
+ return state.manager[prop.key].impl.get(state, dict_, passive=passive)
+
+ def _set_committed_state_attr_by_column(self, state, dict_, column, value):
+ prop = self._columntoproperty[column]
+ state.manager[prop.key].impl.set_committed_value(state, dict_, value)
+
+ def _set_state_attr_by_column(self, state, dict_, column, value):
+ prop = self._columntoproperty[column]
+ state.manager[prop.key].impl.set(state, dict_, value, None)
+
+ def _get_committed_attr_by_column(self, obj, column):
+ state = attributes.instance_state(obj)
+ dict_ = attributes.instance_dict(obj)
+ return self._get_committed_state_attr_by_column(
+ state, dict_, column, passive=PassiveFlag.PASSIVE_OFF
+ )
+
+ def _get_committed_state_attr_by_column(
+ self, state, dict_, column, passive=PassiveFlag.PASSIVE_RETURN_NO_VALUE
+ ):
+ prop = self._columntoproperty[column]
+ return state.manager[prop.key].impl.get_committed_value(
+ state, dict_, passive=passive
+ )
+
+ def _optimized_get_statement(self, state, attribute_names):
+ """assemble a WHERE clause which retrieves a given state by primary
+ key, using a minimized set of tables.
+
+ Applies to a joined-table inheritance mapper where the
+ requested attribute names are only present on joined tables,
+ not the base table. The WHERE clause attempts to include
+ only those tables to minimize joins.
+
+ """
+ props = self._props
+
+ col_attribute_names = set(attribute_names).intersection(
+ state.mapper.column_attrs.keys()
+ )
+ tables: Set[FromClause] = set(
+ chain(
+ *[
+ sql_util.find_tables(c, check_columns=True)
+ for key in col_attribute_names
+ for c in props[key].columns
+ ]
+ )
+ )
+
+ if self.base_mapper.local_table in tables:
+ return None
+
+ def visit_binary(binary):
+ leftcol = binary.left
+ rightcol = binary.right
+ if leftcol is None or rightcol is None:
+ return
+
+ if leftcol.table not in tables:
+ leftval = self._get_committed_state_attr_by_column(
+ state,
+ state.dict,
+ leftcol,
+ passive=PassiveFlag.PASSIVE_NO_INITIALIZE,
+ )
+ if leftval in orm_util._none_set:
+ raise _OptGetColumnsNotAvailable()
+ binary.left = sql.bindparam(
+ None, leftval, type_=binary.right.type
+ )
+ elif rightcol.table not in tables:
+ rightval = self._get_committed_state_attr_by_column(
+ state,
+ state.dict,
+ rightcol,
+ passive=PassiveFlag.PASSIVE_NO_INITIALIZE,
+ )
+ if rightval in orm_util._none_set:
+ raise _OptGetColumnsNotAvailable()
+ binary.right = sql.bindparam(
+ None, rightval, type_=binary.right.type
+ )
+
+ allconds: List[ColumnElement[bool]] = []
+
+ start = False
+
+ # as of #7507, from the lowest base table on upwards,
+ # we include all intermediary tables.
+
+ for mapper in reversed(list(self.iterate_to_root())):
+ if mapper.local_table in tables:
+ start = True
+ elif not isinstance(mapper.local_table, expression.TableClause):
+ return None
+ if start and not mapper.single:
+ assert mapper.inherits
+ assert not mapper.concrete
+ assert mapper.inherit_condition is not None
+ allconds.append(mapper.inherit_condition)
+ tables.add(mapper.local_table)
+
+ # only the bottom table needs its criteria to be altered to fit
+ # the primary key ident - the rest of the tables upwards to the
+ # descendant-most class should all be present and joined to each
+ # other.
+ try:
+ _traversed = visitors.cloned_traverse(
+ allconds[0], {}, {"binary": visit_binary}
+ )
+ except _OptGetColumnsNotAvailable:
+ return None
+ else:
+ allconds[0] = _traversed
+
+ cond = sql.and_(*allconds)
+
+ cols = []
+ for key in col_attribute_names:
+ cols.extend(props[key].columns)
+ return (
+ sql.select(*cols)
+ .where(cond)
+ .set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL)
+ )
+
+ def _iterate_to_target_viawpoly(self, mapper):
+ if self.isa(mapper):
+ prev = self
+ for m in self.iterate_to_root():
+ yield m
+
+ if m is not prev and prev not in m._with_polymorphic_mappers:
+ break
+
+ prev = m
+ if m is mapper:
+ break
+
+ @HasMemoized.memoized_attribute
+ def _would_selectinload_combinations_cache(self):
+ return {}
+
+ def _would_selectin_load_only_from_given_mapper(self, super_mapper):
+ """return True if this mapper would "selectin" polymorphic load based
+ on the given super mapper, and not from a setting from a subclass.
+
+ given::
+
+ class A:
+ ...
+
+ class B(A):
+ __mapper_args__ = {"polymorphic_load": "selectin"}
+
+ class C(B):
+ ...
+
+ class D(B):
+ __mapper_args__ = {"polymorphic_load": "selectin"}
+
+ ``inspect(C)._would_selectin_load_only_from_given_mapper(inspect(B))``
+ returns True, because C does selectin loading because of B's setting.
+
+ OTOH, ``inspect(D)
+ ._would_selectin_load_only_from_given_mapper(inspect(B))``
+ returns False, because D does selectin loading because of its own
+ setting; when we are doing a selectin poly load from B, we want to
+ filter out D because it would already have its own selectin poly load
+ set up separately.
+
+ Added as part of #9373.
+
+ """
+ cache = self._would_selectinload_combinations_cache
+
+ try:
+ return cache[super_mapper]
+ except KeyError:
+ pass
+
+ # assert that given object is a supermapper, meaning we already
+ # strong reference it directly or indirectly. this allows us
+ # to not worry that we are creating new strongrefs to unrelated
+ # mappers or other objects.
+ assert self.isa(super_mapper)
+
+ mapper = super_mapper
+ for m in self._iterate_to_target_viawpoly(mapper):
+ if m.polymorphic_load == "selectin":
+ retval = m is super_mapper
+ break
+ else:
+ retval = False
+
+ cache[super_mapper] = retval
+ return retval
+
+ def _should_selectin_load(self, enabled_via_opt, polymorphic_from):
+ if not enabled_via_opt:
+ # common case, takes place for all polymorphic loads
+ mapper = polymorphic_from
+ for m in self._iterate_to_target_viawpoly(mapper):
+ if m.polymorphic_load == "selectin":
+ return m
+ else:
+ # uncommon case, selectin load options were used
+ enabled_via_opt = set(enabled_via_opt)
+ enabled_via_opt_mappers = {e.mapper: e for e in enabled_via_opt}
+ for entity in enabled_via_opt.union([polymorphic_from]):
+ mapper = entity.mapper
+ for m in self._iterate_to_target_viawpoly(mapper):
+ if (
+ m.polymorphic_load == "selectin"
+ or m in enabled_via_opt_mappers
+ ):
+ return enabled_via_opt_mappers.get(m, m)
+
+ return None
+
+ @util.preload_module("sqlalchemy.orm.strategy_options")
+ def _subclass_load_via_in(self, entity, polymorphic_from):
+ """Assemble a that can load the columns local to
+ this subclass as a SELECT with IN.
+
+ """
+ strategy_options = util.preloaded.orm_strategy_options
+
+ assert self.inherits
+
+ if self.polymorphic_on is not None:
+ polymorphic_prop = self._columntoproperty[self.polymorphic_on]
+ keep_props = set([polymorphic_prop] + self._identity_key_props)
+ else:
+ keep_props = set(self._identity_key_props)
+
+ disable_opt = strategy_options.Load(entity)
+ enable_opt = strategy_options.Load(entity)
+
+ classes_to_include = {self}
+ m: Optional[Mapper[Any]] = self.inherits
+ while (
+ m is not None
+ and m is not polymorphic_from
+ and m.polymorphic_load == "selectin"
+ ):
+ classes_to_include.add(m)
+ m = m.inherits
+
+ for prop in self.attrs:
+ # skip prop keys that are not instrumented on the mapped class.
+ # this is primarily the "_sa_polymorphic_on" property that gets
+ # created for an ad-hoc polymorphic_on SQL expression, issue #8704
+ if prop.key not in self.class_manager:
+ continue
+
+ if prop.parent in classes_to_include or prop in keep_props:
+ # "enable" options, to turn on the properties that we want to
+ # load by default (subject to options from the query)
+ if not isinstance(prop, StrategizedProperty):
+ continue
+
+ enable_opt = enable_opt._set_generic_strategy(
+ # convert string name to an attribute before passing
+ # to loader strategy. note this must be in terms
+ # of given entity, such as AliasedClass, etc.
+ (getattr(entity.entity_namespace, prop.key),),
+ dict(prop.strategy_key),
+ _reconcile_to_other=True,
+ )
+ else:
+ # "disable" options, to turn off the properties from the
+ # superclass that we *don't* want to load, applied after
+ # the options from the query to override them
+ disable_opt = disable_opt._set_generic_strategy(
+ # convert string name to an attribute before passing
+ # to loader strategy. note this must be in terms
+ # of given entity, such as AliasedClass, etc.
+ (getattr(entity.entity_namespace, prop.key),),
+ {"do_nothing": True},
+ _reconcile_to_other=False,
+ )
+
+ primary_key = [
+ sql_util._deep_annotate(pk, {"_orm_adapt": True})
+ for pk in self.primary_key
+ ]
+
+ in_expr: ColumnElement[Any]
+
+ if len(primary_key) > 1:
+ in_expr = sql.tuple_(*primary_key)
+ else:
+ in_expr = primary_key[0]
+
+ if entity.is_aliased_class:
+ assert entity.mapper is self
+
+ q = sql.select(entity).set_label_style(
+ LABEL_STYLE_TABLENAME_PLUS_COL
+ )
+
+ in_expr = entity._adapter.traverse(in_expr)
+ primary_key = [entity._adapter.traverse(k) for k in primary_key]
+ q = q.where(
+ in_expr.in_(sql.bindparam("primary_keys", expanding=True))
+ ).order_by(*primary_key)
+ else:
+ q = sql.select(self).set_label_style(
+ LABEL_STYLE_TABLENAME_PLUS_COL
+ )
+ q = q.where(
+ in_expr.in_(sql.bindparam("primary_keys", expanding=True))
+ ).order_by(*primary_key)
+
+ return q, enable_opt, disable_opt
+
+ @HasMemoized.memoized_attribute
+ def _subclass_load_via_in_mapper(self):
+ # the default is loading this mapper against the basemost mapper
+ return self._subclass_load_via_in(self, self.base_mapper)
+
+ def cascade_iterator(
+ self,
+ type_: str,
+ state: InstanceState[_O],
+ halt_on: Optional[Callable[[InstanceState[Any]], bool]] = None,
+ ) -> Iterator[
+ Tuple[object, Mapper[Any], InstanceState[Any], _InstanceDict]
+ ]:
+ r"""Iterate each element and its mapper in an object graph,
+ for all relationships that meet the given cascade rule.
+
+ :param type\_:
+ The name of the cascade rule (i.e. ``"save-update"``, ``"delete"``,
+ etc.).
+
+ .. note:: the ``"all"`` cascade is not accepted here. For a generic
+ object traversal function, see :ref:`faq_walk_objects`.
+
+ :param state:
+ The lead InstanceState. child items will be processed per
+ the relationships defined for this object's mapper.
+
+ :return: the method yields individual object instances.
+
+ .. seealso::
+
+ :ref:`unitofwork_cascades`
+
+ :ref:`faq_walk_objects` - illustrates a generic function to
+ traverse all objects without relying on cascades.
+
+ """
+ visited_states: Set[InstanceState[Any]] = set()
+ prp, mpp = object(), object()
+
+ assert state.mapper.isa(self)
+
+ # this is actually a recursive structure, fully typing it seems
+ # a little too difficult for what it's worth here
+ visitables: Deque[
+ Tuple[
+ Deque[Any],
+ object,
+ Optional[InstanceState[Any]],
+ Optional[_InstanceDict],
+ ]
+ ]
+
+ visitables = deque(
+ [(deque(state.mapper._props.values()), prp, state, state.dict)]
+ )
+
+ while visitables:
+ iterator, item_type, parent_state, parent_dict = visitables[-1]
+ if not iterator:
+ visitables.pop()
+ continue
+
+ if item_type is prp:
+ prop = iterator.popleft()
+ if not prop.cascade or type_ not in prop.cascade:
+ continue
+ assert parent_state is not None
+ assert parent_dict is not None
+ queue = deque(
+ prop.cascade_iterator(
+ type_,
+ parent_state,
+ parent_dict,
+ visited_states,
+ halt_on,
+ )
+ )
+ if queue:
+ visitables.append((queue, mpp, None, None))
+ elif item_type is mpp:
+ (
+ instance,
+ instance_mapper,
+ corresponding_state,
+ corresponding_dict,
+ ) = iterator.popleft()
+ yield (
+ instance,
+ instance_mapper,
+ corresponding_state,
+ corresponding_dict,
+ )
+ visitables.append(
+ (
+ deque(instance_mapper._props.values()),
+ prp,
+ corresponding_state,
+ corresponding_dict,
+ )
+ )
+
+ @HasMemoized.memoized_attribute
+ def _compiled_cache(self):
+ return util.LRUCache(self._compiled_cache_size)
+
+ @HasMemoized.memoized_attribute
+ def _multiple_persistence_tables(self):
+ return len(self.tables) > 1
+
+ @HasMemoized.memoized_attribute
+ def _sorted_tables(self):
+ table_to_mapper: Dict[TableClause, Mapper[Any]] = {}
+
+ for mapper in self.base_mapper.self_and_descendants:
+ for t in mapper.tables:
+ table_to_mapper.setdefault(t, mapper)
+
+ extra_dependencies = []
+ for table, mapper in table_to_mapper.items():
+ super_ = mapper.inherits
+ if super_:
+ extra_dependencies.extend(
+ [(super_table, table) for super_table in super_.tables]
+ )
+
+ def skip(fk):
+ # attempt to skip dependencies that are not
+ # significant to the inheritance chain
+ # for two tables that are related by inheritance.
+ # while that dependency may be important, it's technically
+ # not what we mean to sort on here.
+ parent = table_to_mapper.get(fk.parent.table)
+ dep = table_to_mapper.get(fk.column.table)
+ if (
+ parent is not None
+ and dep is not None
+ and dep is not parent
+ and dep.inherit_condition is not None
+ ):
+ cols = set(sql_util._find_columns(dep.inherit_condition))
+ if parent.inherit_condition is not None:
+ cols = cols.union(
+ sql_util._find_columns(parent.inherit_condition)
+ )
+ return fk.parent not in cols and fk.column not in cols
+ else:
+ return fk.parent not in cols
+ return False
+
+ sorted_ = sql_util.sort_tables(
+ table_to_mapper,
+ skip_fn=skip,
+ extra_dependencies=extra_dependencies,
+ )
+
+ ret = util.OrderedDict()
+ for t in sorted_:
+ ret[t] = table_to_mapper[t]
+ return ret
+
+ def _memo(self, key: Any, callable_: Callable[[], _T]) -> _T:
+ if key in self._memoized_values:
+ return cast(_T, self._memoized_values[key])
+ else:
+ self._memoized_values[key] = value = callable_()
+ return value
+
+ @util.memoized_property
+ def _table_to_equated(self):
+ """memoized map of tables to collections of columns to be
+ synchronized upwards to the base mapper."""
+
+ result: util.defaultdict[
+ Table,
+ List[
+ Tuple[
+ Mapper[Any],
+ List[Tuple[ColumnElement[Any], ColumnElement[Any]]],
+ ]
+ ],
+ ] = util.defaultdict(list)
+
+ def set_union(x, y):
+ return x.union(y)
+
+ for table in self._sorted_tables:
+ cols = set(table.c)
+
+ for m in self.iterate_to_root():
+ if m._inherits_equated_pairs and cols.intersection(
+ reduce(
+ set_union,
+ [l.proxy_set for l, r in m._inherits_equated_pairs],
+ )
+ ):
+ result[table].append((m, m._inherits_equated_pairs))
+
+ return result
+
+
+class _OptGetColumnsNotAvailable(Exception):
+ pass
+
+
+def configure_mappers() -> None:
+ """Initialize the inter-mapper relationships of all mappers that
+ have been constructed thus far across all :class:`_orm.registry`
+ collections.
+
+ The configure step is used to reconcile and initialize the
+ :func:`_orm.relationship` linkages between mapped classes, as well as to
+ invoke configuration events such as the
+ :meth:`_orm.MapperEvents.before_configured` and
+ :meth:`_orm.MapperEvents.after_configured`, which may be used by ORM
+ extensions or user-defined extension hooks.
+
+ Mapper configuration is normally invoked automatically, the first time
+ mappings from a particular :class:`_orm.registry` are used, as well as
+ whenever mappings are used and additional not-yet-configured mappers have
+ been constructed. The automatic configuration process however is local only
+ to the :class:`_orm.registry` involving the target mapper and any related
+ :class:`_orm.registry` objects which it may depend on; this is
+ equivalent to invoking the :meth:`_orm.registry.configure` method
+ on a particular :class:`_orm.registry`.
+
+ By contrast, the :func:`_orm.configure_mappers` function will invoke the
+ configuration process on all :class:`_orm.registry` objects that
+ exist in memory, and may be useful for scenarios where many individual
+ :class:`_orm.registry` objects that are nonetheless interrelated are
+ in use.
+
+ .. versionchanged:: 1.4
+
+ As of SQLAlchemy 1.4.0b2, this function works on a
+ per-:class:`_orm.registry` basis, locating all :class:`_orm.registry`
+ objects present and invoking the :meth:`_orm.registry.configure` method
+ on each. The :meth:`_orm.registry.configure` method may be preferred to
+ limit the configuration of mappers to those local to a particular
+ :class:`_orm.registry` and/or declarative base class.
+
+ Points at which automatic configuration is invoked include when a mapped
+ class is instantiated into an instance, as well as when ORM queries
+ are emitted using :meth:`.Session.query` or :meth:`_orm.Session.execute`
+ with an ORM-enabled statement.
+
+ The mapper configure process, whether invoked by
+ :func:`_orm.configure_mappers` or from :meth:`_orm.registry.configure`,
+ provides several event hooks that can be used to augment the mapper
+ configuration step. These hooks include:
+
+ * :meth:`.MapperEvents.before_configured` - called once before
+ :func:`.configure_mappers` or :meth:`_orm.registry.configure` does any
+ work; this can be used to establish additional options, properties, or
+ related mappings before the operation proceeds.
+
+ * :meth:`.MapperEvents.mapper_configured` - called as each individual
+ :class:`_orm.Mapper` is configured within the process; will include all
+ mapper state except for backrefs set up by other mappers that are still
+ to be configured.
+
+ * :meth:`.MapperEvents.after_configured` - called once after
+ :func:`.configure_mappers` or :meth:`_orm.registry.configure` is
+ complete; at this stage, all :class:`_orm.Mapper` objects that fall
+ within the scope of the configuration operation will be fully configured.
+ Note that the calling application may still have other mappings that
+ haven't been produced yet, such as if they are in modules as yet
+ unimported, and may also have mappings that are still to be configured,
+ if they are in other :class:`_orm.registry` collections not part of the
+ current scope of configuration.
+
+ """
+
+ _configure_registries(_all_registries(), cascade=True)
+
+
+def _configure_registries(
+ registries: Set[_RegistryType], cascade: bool
+) -> None:
+ for reg in registries:
+ if reg._new_mappers:
+ break
+ else:
+ return
+
+ with _CONFIGURE_MUTEX:
+ global _already_compiling
+ if _already_compiling:
+ return
+ _already_compiling = True
+ try:
+ # double-check inside mutex
+ for reg in registries:
+ if reg._new_mappers:
+ break
+ else:
+ return
+
+ Mapper.dispatch._for_class(Mapper).before_configured() # type: ignore # noqa: E501
+ # initialize properties on all mappers
+ # note that _mapper_registry is unordered, which
+ # may randomly conceal/reveal issues related to
+ # the order of mapper compilation
+
+ _do_configure_registries(registries, cascade)
+ finally:
+ _already_compiling = False
+ Mapper.dispatch._for_class(Mapper).after_configured() # type: ignore
+
+
+@util.preload_module("sqlalchemy.orm.decl_api")
+def _do_configure_registries(
+ registries: Set[_RegistryType], cascade: bool
+) -> None:
+ registry = util.preloaded.orm_decl_api.registry
+
+ orig = set(registries)
+
+ for reg in registry._recurse_with_dependencies(registries):
+ has_skip = False
+
+ for mapper in reg._mappers_to_configure():
+ run_configure = None
+
+ for fn in mapper.dispatch.before_mapper_configured:
+ run_configure = fn(mapper, mapper.class_)
+ if run_configure is EXT_SKIP:
+ has_skip = True
+ break
+ if run_configure is EXT_SKIP:
+ continue
+
+ if getattr(mapper, "_configure_failed", False):
+ e = sa_exc.InvalidRequestError(
+ "One or more mappers failed to initialize - "
+ "can't proceed with initialization of other "
+ "mappers. Triggering mapper: '%s'. "
+ "Original exception was: %s"
+ % (mapper, mapper._configure_failed)
+ )
+ e._configure_failed = mapper._configure_failed # type: ignore
+ raise e
+
+ if not mapper.configured:
+ try:
+ mapper._post_configure_properties()
+ mapper._expire_memoizations()
+ mapper.dispatch.mapper_configured(mapper, mapper.class_)
+ except Exception:
+ exc = sys.exc_info()[1]
+ if not hasattr(exc, "_configure_failed"):
+ mapper._configure_failed = exc
+ raise
+ if not has_skip:
+ reg._new_mappers = False
+
+ if not cascade and reg._dependencies.difference(orig):
+ raise sa_exc.InvalidRequestError(
+ "configure was called with cascade=False but "
+ "additional registries remain"
+ )
+
+
+@util.preload_module("sqlalchemy.orm.decl_api")
+def _dispose_registries(registries: Set[_RegistryType], cascade: bool) -> None:
+ registry = util.preloaded.orm_decl_api.registry
+
+ orig = set(registries)
+
+ for reg in registry._recurse_with_dependents(registries):
+ if not cascade and reg._dependents.difference(orig):
+ raise sa_exc.InvalidRequestError(
+ "Registry has dependent registries that are not disposed; "
+ "pass cascade=True to clear these also"
+ )
+
+ while reg._managers:
+ try:
+ manager, _ = reg._managers.popitem()
+ except KeyError:
+ # guard against race between while and popitem
+ pass
+ else:
+ reg._dispose_manager_and_mapper(manager)
+
+ reg._non_primary_mappers.clear()
+ reg._dependents.clear()
+ for dep in reg._dependencies:
+ dep._dependents.discard(reg)
+ reg._dependencies.clear()
+ # this wasn't done in the 1.3 clear_mappers() and in fact it
+ # was a bug, as it could cause configure_mappers() to invoke
+ # the "before_configured" event even though mappers had all been
+ # disposed.
+ reg._new_mappers = False
+
+
+def reconstructor(fn):
+ """Decorate a method as the 'reconstructor' hook.
+
+ Designates a single method as the "reconstructor", an ``__init__``-like
+ method that will be called by the ORM after the instance has been
+ loaded from the database or otherwise reconstituted.
+
+ .. tip::
+
+ The :func:`_orm.reconstructor` decorator makes use of the
+ :meth:`_orm.InstanceEvents.load` event hook, which can be
+ used directly.
+
+ The reconstructor will be invoked with no arguments. Scalar
+ (non-collection) database-mapped attributes of the instance will
+ be available for use within the function. Eagerly-loaded
+ collections are generally not yet available and will usually only
+ contain the first element. ORM state changes made to objects at
+ this stage will not be recorded for the next flush() operation, so
+ the activity within a reconstructor should be conservative.
+
+ .. seealso::
+
+ :meth:`.InstanceEvents.load`
+
+ """
+ fn.__sa_reconstructor__ = True
+ return fn
+
+
+def validates(
+ *names: str, include_removes: bool = False, include_backrefs: bool = True
+) -> Callable[[_Fn], _Fn]:
+ r"""Decorate a method as a 'validator' for one or more named properties.
+
+ Designates a method as a validator, a method which receives the
+ name of the attribute as well as a value to be assigned, or in the
+ case of a collection, the value to be added to the collection.
+ The function can then raise validation exceptions to halt the
+ process from continuing (where Python's built-in ``ValueError``
+ and ``AssertionError`` exceptions are reasonable choices), or can
+ modify or replace the value before proceeding. The function should
+ otherwise return the given value.
+
+ Note that a validator for a collection **cannot** issue a load of that
+ collection within the validation routine - this usage raises
+ an assertion to avoid recursion overflows. This is a reentrant
+ condition which is not supported.
+
+ :param \*names: list of attribute names to be validated.
+ :param include_removes: if True, "remove" events will be
+ sent as well - the validation function must accept an additional
+ argument "is_remove" which will be a boolean.
+
+ :param include_backrefs: defaults to ``True``; if ``False``, the
+ validation function will not emit if the originator is an attribute
+ event related via a backref. This can be used for bi-directional
+ :func:`.validates` usage where only one validator should emit per
+ attribute operation.
+
+ .. versionchanged:: 2.0.16 This paramter inadvertently defaulted to
+ ``False`` for releases 2.0.0 through 2.0.15. Its correct default
+ of ``True`` is restored in 2.0.16.
+
+ .. seealso::
+
+ :ref:`simple_validators` - usage examples for :func:`.validates`
+
+ """
+
+ def wrap(fn: _Fn) -> _Fn:
+ fn.__sa_validators__ = names # type: ignore[attr-defined]
+ fn.__sa_validation_opts__ = { # type: ignore[attr-defined]
+ "include_removes": include_removes,
+ "include_backrefs": include_backrefs,
+ }
+ return fn
+
+ return wrap
+
+
+def _event_on_load(state, ctx):
+ instrumenting_mapper = state.manager.mapper
+
+ if instrumenting_mapper._reconstructor:
+ instrumenting_mapper._reconstructor(state.obj())
+
+
+def _event_on_init(state, args, kwargs):
+ """Run init_instance hooks.
+
+ This also includes mapper compilation, normally not needed
+ here but helps with some piecemeal configuration
+ scenarios (such as in the ORM tutorial).
+
+ """
+
+ instrumenting_mapper = state.manager.mapper
+ if instrumenting_mapper:
+ instrumenting_mapper._check_configure()
+ if instrumenting_mapper._set_polymorphic_identity:
+ instrumenting_mapper._set_polymorphic_identity(state)
+
+
+class _ColumnMapping(Dict["ColumnElement[Any]", "MapperProperty[Any]"]):
+ """Error reporting helper for mapper._columntoproperty."""
+
+ __slots__ = ("mapper",)
+
+ def __init__(self, mapper):
+ # TODO: weakref would be a good idea here
+ self.mapper = mapper
+
+ def __missing__(self, column):
+ prop = self.mapper._props.get(column)
+ if prop:
+ raise orm_exc.UnmappedColumnError(
+ "Column '%s.%s' is not available, due to "
+ "conflicting property '%s':%r"
+ % (column.table.name, column.name, column.key, prop)
+ )
+ raise orm_exc.UnmappedColumnError(
+ "No column %s is configured on mapper %s..."
+ % (column, self.mapper)
+ )
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/path_registry.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/path_registry.py
new file mode 100644
index 0000000..76484b3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/path_registry.py
@@ -0,0 +1,808 @@
+# orm/path_registry.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+"""Path tracking utilities, representing mapper graph traversals.
+
+"""
+
+from __future__ import annotations
+
+from functools import reduce
+from itertools import chain
+import logging
+import operator
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from . import base as orm_base
+from ._typing import insp_is_mapper_property
+from .. import exc
+from .. import util
+from ..sql import visitors
+from ..sql.cache_key import HasCacheKey
+
+if TYPE_CHECKING:
+ from ._typing import _InternalEntityType
+ from .interfaces import MapperProperty
+ from .mapper import Mapper
+ from .relationships import RelationshipProperty
+ from .util import AliasedInsp
+ from ..sql.cache_key import _CacheKeyTraversalType
+ from ..sql.elements import BindParameter
+ from ..sql.visitors import anon_map
+ from ..util.typing import _LiteralStar
+ from ..util.typing import TypeGuard
+
+ def is_root(path: PathRegistry) -> TypeGuard[RootRegistry]: ...
+
+ def is_entity(path: PathRegistry) -> TypeGuard[AbstractEntityRegistry]: ...
+
+else:
+ is_root = operator.attrgetter("is_root")
+ is_entity = operator.attrgetter("is_entity")
+
+
+_SerializedPath = List[Any]
+_StrPathToken = str
+_PathElementType = Union[
+ _StrPathToken, "_InternalEntityType[Any]", "MapperProperty[Any]"
+]
+
+# the representation is in fact
+# a tuple with alternating:
+# [_InternalEntityType[Any], Union[str, MapperProperty[Any]],
+# _InternalEntityType[Any], Union[str, MapperProperty[Any]], ...]
+# this might someday be a tuple of 2-tuples instead, but paths can be
+# chopped at odd intervals as well so this is less flexible
+_PathRepresentation = Tuple[_PathElementType, ...]
+
+# NOTE: these names are weird since the array is 0-indexed,
+# the "_Odd" entries are at 0, 2, 4, etc
+_OddPathRepresentation = Sequence["_InternalEntityType[Any]"]
+_EvenPathRepresentation = Sequence[Union["MapperProperty[Any]", str]]
+
+
+log = logging.getLogger(__name__)
+
+
+def _unreduce_path(path: _SerializedPath) -> PathRegistry:
+ return PathRegistry.deserialize(path)
+
+
+_WILDCARD_TOKEN: _LiteralStar = "*"
+_DEFAULT_TOKEN = "_sa_default"
+
+
+class PathRegistry(HasCacheKey):
+ """Represent query load paths and registry functions.
+
+ Basically represents structures like:
+
+ (<User mapper>, "orders", <Order mapper>, "items", <Item mapper>)
+
+ These structures are generated by things like
+ query options (joinedload(), subqueryload(), etc.) and are
+ used to compose keys stored in the query._attributes dictionary
+ for various options.
+
+ They are then re-composed at query compile/result row time as
+ the query is formed and as rows are fetched, where they again
+ serve to compose keys to look up options in the context.attributes
+ dictionary, which is copied from query._attributes.
+
+ The path structure has a limited amount of caching, where each
+ "root" ultimately pulls from a fixed registry associated with
+ the first mapper, that also contains elements for each of its
+ property keys. However paths longer than two elements, which
+ are the exception rather than the rule, are generated on an
+ as-needed basis.
+
+ """
+
+ __slots__ = ()
+
+ is_token = False
+ is_root = False
+ has_entity = False
+ is_property = False
+ is_entity = False
+
+ is_unnatural: bool
+
+ path: _PathRepresentation
+ natural_path: _PathRepresentation
+ parent: Optional[PathRegistry]
+ root: RootRegistry
+
+ _cache_key_traversal: _CacheKeyTraversalType = [
+ ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key_list)
+ ]
+
+ def __eq__(self, other: Any) -> bool:
+ try:
+ return other is not None and self.path == other._path_for_compare
+ except AttributeError:
+ util.warn(
+ "Comparison of PathRegistry to %r is not supported"
+ % (type(other))
+ )
+ return False
+
+ def __ne__(self, other: Any) -> bool:
+ try:
+ return other is None or self.path != other._path_for_compare
+ except AttributeError:
+ util.warn(
+ "Comparison of PathRegistry to %r is not supported"
+ % (type(other))
+ )
+ return True
+
+ @property
+ def _path_for_compare(self) -> Optional[_PathRepresentation]:
+ return self.path
+
+ def odd_element(self, index: int) -> _InternalEntityType[Any]:
+ return self.path[index] # type: ignore
+
+ def set(self, attributes: Dict[Any, Any], key: Any, value: Any) -> None:
+ log.debug("set '%s' on path '%s' to '%s'", key, self, value)
+ attributes[(key, self.natural_path)] = value
+
+ def setdefault(
+ self, attributes: Dict[Any, Any], key: Any, value: Any
+ ) -> None:
+ log.debug("setdefault '%s' on path '%s' to '%s'", key, self, value)
+ attributes.setdefault((key, self.natural_path), value)
+
+ def get(
+ self, attributes: Dict[Any, Any], key: Any, value: Optional[Any] = None
+ ) -> Any:
+ key = (key, self.natural_path)
+ if key in attributes:
+ return attributes[key]
+ else:
+ return value
+
+ def __len__(self) -> int:
+ return len(self.path)
+
+ def __hash__(self) -> int:
+ return id(self)
+
+ @overload
+ def __getitem__(self, entity: _StrPathToken) -> TokenRegistry: ...
+
+ @overload
+ def __getitem__(self, entity: int) -> _PathElementType: ...
+
+ @overload
+ def __getitem__(self, entity: slice) -> _PathRepresentation: ...
+
+ @overload
+ def __getitem__(
+ self, entity: _InternalEntityType[Any]
+ ) -> AbstractEntityRegistry: ...
+
+ @overload
+ def __getitem__(self, entity: MapperProperty[Any]) -> PropRegistry: ...
+
+ def __getitem__(
+ self,
+ entity: Union[
+ _StrPathToken,
+ int,
+ slice,
+ _InternalEntityType[Any],
+ MapperProperty[Any],
+ ],
+ ) -> Union[
+ TokenRegistry,
+ _PathElementType,
+ _PathRepresentation,
+ PropRegistry,
+ AbstractEntityRegistry,
+ ]:
+ raise NotImplementedError()
+
+ # TODO: what are we using this for?
+ @property
+ def length(self) -> int:
+ return len(self.path)
+
+ def pairs(
+ self,
+ ) -> Iterator[
+ Tuple[_InternalEntityType[Any], Union[str, MapperProperty[Any]]]
+ ]:
+ odd_path = cast(_OddPathRepresentation, self.path)
+ even_path = cast(_EvenPathRepresentation, odd_path)
+ for i in range(0, len(odd_path), 2):
+ yield odd_path[i], even_path[i + 1]
+
+ def contains_mapper(self, mapper: Mapper[Any]) -> bool:
+ _m_path = cast(_OddPathRepresentation, self.path)
+ for path_mapper in [_m_path[i] for i in range(0, len(_m_path), 2)]:
+ if path_mapper.mapper.isa(mapper):
+ return True
+ else:
+ return False
+
+ def contains(self, attributes: Dict[Any, Any], key: Any) -> bool:
+ return (key, self.path) in attributes
+
+ def __reduce__(self) -> Any:
+ return _unreduce_path, (self.serialize(),)
+
+ @classmethod
+ def _serialize_path(cls, path: _PathRepresentation) -> _SerializedPath:
+ _m_path = cast(_OddPathRepresentation, path)
+ _p_path = cast(_EvenPathRepresentation, path)
+
+ return list(
+ zip(
+ tuple(
+ m.class_ if (m.is_mapper or m.is_aliased_class) else str(m)
+ for m in [_m_path[i] for i in range(0, len(_m_path), 2)]
+ ),
+ tuple(
+ p.key if insp_is_mapper_property(p) else str(p)
+ for p in [_p_path[i] for i in range(1, len(_p_path), 2)]
+ )
+ + (None,),
+ )
+ )
+
+ @classmethod
+ def _deserialize_path(cls, path: _SerializedPath) -> _PathRepresentation:
+ def _deserialize_mapper_token(mcls: Any) -> Any:
+ return (
+ # note: we likely dont want configure=True here however
+ # this is maintained at the moment for backwards compatibility
+ orm_base._inspect_mapped_class(mcls, configure=True)
+ if mcls not in PathToken._intern
+ else PathToken._intern[mcls]
+ )
+
+ def _deserialize_key_token(mcls: Any, key: Any) -> Any:
+ if key is None:
+ return None
+ elif key in PathToken._intern:
+ return PathToken._intern[key]
+ else:
+ mp = orm_base._inspect_mapped_class(mcls, configure=True)
+ assert mp is not None
+ return mp.attrs[key]
+
+ p = tuple(
+ chain(
+ *[
+ (
+ _deserialize_mapper_token(mcls),
+ _deserialize_key_token(mcls, key),
+ )
+ for mcls, key in path
+ ]
+ )
+ )
+ if p and p[-1] is None:
+ p = p[0:-1]
+ return p
+
+ def serialize(self) -> _SerializedPath:
+ path = self.path
+ return self._serialize_path(path)
+
+ @classmethod
+ def deserialize(cls, path: _SerializedPath) -> PathRegistry:
+ assert path is not None
+ p = cls._deserialize_path(path)
+ return cls.coerce(p)
+
+ @overload
+ @classmethod
+ def per_mapper(cls, mapper: Mapper[Any]) -> CachingEntityRegistry: ...
+
+ @overload
+ @classmethod
+ def per_mapper(cls, mapper: AliasedInsp[Any]) -> SlotsEntityRegistry: ...
+
+ @classmethod
+ def per_mapper(
+ cls, mapper: _InternalEntityType[Any]
+ ) -> AbstractEntityRegistry:
+ if mapper.is_mapper:
+ return CachingEntityRegistry(cls.root, mapper)
+ else:
+ return SlotsEntityRegistry(cls.root, mapper)
+
+ @classmethod
+ def coerce(cls, raw: _PathRepresentation) -> PathRegistry:
+ def _red(prev: PathRegistry, next_: _PathElementType) -> PathRegistry:
+ return prev[next_]
+
+ # can't quite get mypy to appreciate this one :)
+ return reduce(_red, raw, cls.root) # type: ignore
+
+ def __add__(self, other: PathRegistry) -> PathRegistry:
+ def _red(prev: PathRegistry, next_: _PathElementType) -> PathRegistry:
+ return prev[next_]
+
+ return reduce(_red, other.path, self)
+
+ def __str__(self) -> str:
+ return f"ORM Path[{' -> '.join(str(elem) for elem in self.path)}]"
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self.path!r})"
+
+
+class CreatesToken(PathRegistry):
+ __slots__ = ()
+
+ is_aliased_class: bool
+ is_root: bool
+
+ def token(self, token: _StrPathToken) -> TokenRegistry:
+ if token.endswith(f":{_WILDCARD_TOKEN}"):
+ return TokenRegistry(self, token)
+ elif token.endswith(f":{_DEFAULT_TOKEN}"):
+ return TokenRegistry(self.root, token)
+ else:
+ raise exc.ArgumentError(f"invalid token: {token}")
+
+
+class RootRegistry(CreatesToken):
+ """Root registry, defers to mappers so that
+ paths are maintained per-root-mapper.
+
+ """
+
+ __slots__ = ()
+
+ inherit_cache = True
+
+ path = natural_path = ()
+ has_entity = False
+ is_aliased_class = False
+ is_root = True
+ is_unnatural = False
+
+ def _getitem(
+ self, entity: Any
+ ) -> Union[TokenRegistry, AbstractEntityRegistry]:
+ if entity in PathToken._intern:
+ if TYPE_CHECKING:
+ assert isinstance(entity, _StrPathToken)
+ return TokenRegistry(self, PathToken._intern[entity])
+ else:
+ try:
+ return entity._path_registry # type: ignore
+ except AttributeError:
+ raise IndexError(
+ f"invalid argument for RootRegistry.__getitem__: {entity}"
+ )
+
+ def _truncate_recursive(self) -> RootRegistry:
+ return self
+
+ if not TYPE_CHECKING:
+ __getitem__ = _getitem
+
+
+PathRegistry.root = RootRegistry()
+
+
+class PathToken(orm_base.InspectionAttr, HasCacheKey, str):
+ """cacheable string token"""
+
+ _intern: Dict[str, PathToken] = {}
+
+ def _gen_cache_key(
+ self, anon_map: anon_map, bindparams: List[BindParameter[Any]]
+ ) -> Tuple[Any, ...]:
+ return (str(self),)
+
+ @property
+ def _path_for_compare(self) -> Optional[_PathRepresentation]:
+ return None
+
+ @classmethod
+ def intern(cls, strvalue: str) -> PathToken:
+ if strvalue in cls._intern:
+ return cls._intern[strvalue]
+ else:
+ cls._intern[strvalue] = result = PathToken(strvalue)
+ return result
+
+
+class TokenRegistry(PathRegistry):
+ __slots__ = ("token", "parent", "path", "natural_path")
+
+ inherit_cache = True
+
+ token: _StrPathToken
+ parent: CreatesToken
+
+ def __init__(self, parent: CreatesToken, token: _StrPathToken):
+ token = PathToken.intern(token)
+
+ self.token = token
+ self.parent = parent
+ self.path = parent.path + (token,)
+ self.natural_path = parent.natural_path + (token,)
+
+ has_entity = False
+
+ is_token = True
+
+ def generate_for_superclasses(self) -> Iterator[PathRegistry]:
+ # NOTE: this method is no longer used. consider removal
+ parent = self.parent
+ if is_root(parent):
+ yield self
+ return
+
+ if TYPE_CHECKING:
+ assert isinstance(parent, AbstractEntityRegistry)
+ if not parent.is_aliased_class:
+ for mp_ent in parent.mapper.iterate_to_root():
+ yield TokenRegistry(parent.parent[mp_ent], self.token)
+ elif (
+ parent.is_aliased_class
+ and cast(
+ "AliasedInsp[Any]",
+ parent.entity,
+ )._is_with_polymorphic
+ ):
+ yield self
+ for ent in cast(
+ "AliasedInsp[Any]", parent.entity
+ )._with_polymorphic_entities:
+ yield TokenRegistry(parent.parent[ent], self.token)
+ else:
+ yield self
+
+ def _generate_natural_for_superclasses(
+ self,
+ ) -> Iterator[_PathRepresentation]:
+ parent = self.parent
+ if is_root(parent):
+ yield self.natural_path
+ return
+
+ if TYPE_CHECKING:
+ assert isinstance(parent, AbstractEntityRegistry)
+ for mp_ent in parent.mapper.iterate_to_root():
+ yield TokenRegistry(parent.parent[mp_ent], self.token).natural_path
+ if (
+ parent.is_aliased_class
+ and cast(
+ "AliasedInsp[Any]",
+ parent.entity,
+ )._is_with_polymorphic
+ ):
+ yield self.natural_path
+ for ent in cast(
+ "AliasedInsp[Any]", parent.entity
+ )._with_polymorphic_entities:
+ yield (
+ TokenRegistry(parent.parent[ent], self.token).natural_path
+ )
+ else:
+ yield self.natural_path
+
+ def _getitem(self, entity: Any) -> Any:
+ try:
+ return self.path[entity]
+ except TypeError as err:
+ raise IndexError(f"{entity}") from err
+
+ if not TYPE_CHECKING:
+ __getitem__ = _getitem
+
+
+class PropRegistry(PathRegistry):
+ __slots__ = (
+ "prop",
+ "parent",
+ "path",
+ "natural_path",
+ "has_entity",
+ "entity",
+ "mapper",
+ "_wildcard_path_loader_key",
+ "_default_path_loader_key",
+ "_loader_key",
+ "is_unnatural",
+ )
+ inherit_cache = True
+ is_property = True
+
+ prop: MapperProperty[Any]
+ mapper: Optional[Mapper[Any]]
+ entity: Optional[_InternalEntityType[Any]]
+
+ def __init__(
+ self, parent: AbstractEntityRegistry, prop: MapperProperty[Any]
+ ):
+ # restate this path in terms of the
+ # given MapperProperty's parent.
+ insp = cast("_InternalEntityType[Any]", parent[-1])
+ natural_parent: AbstractEntityRegistry = parent
+
+ # inherit "is_unnatural" from the parent
+ self.is_unnatural = parent.parent.is_unnatural or bool(
+ parent.mapper.inherits
+ )
+
+ if not insp.is_aliased_class or insp._use_mapper_path: # type: ignore
+ parent = natural_parent = parent.parent[prop.parent]
+ elif (
+ insp.is_aliased_class
+ and insp.with_polymorphic_mappers
+ and prop.parent in insp.with_polymorphic_mappers
+ ):
+ subclass_entity: _InternalEntityType[Any] = parent[-1]._entity_for_mapper(prop.parent) # type: ignore # noqa: E501
+ parent = parent.parent[subclass_entity]
+
+ # when building a path where with_polymorphic() is in use,
+ # special logic to determine the "natural path" when subclass
+ # entities are used.
+ #
+ # here we are trying to distinguish between a path that starts
+ # on a the with_polymorhpic entity vs. one that starts on a
+ # normal entity that introduces a with_polymorphic() in the
+ # middle using of_type():
+ #
+ # # as in test_polymorphic_rel->
+ # # test_subqueryload_on_subclass_uses_path_correctly
+ # wp = with_polymorphic(RegularEntity, "*")
+ # sess.query(wp).options(someload(wp.SomeSubEntity.foos))
+ #
+ # vs
+ #
+ # # as in test_relationship->JoinedloadWPolyOfTypeContinued
+ # wp = with_polymorphic(SomeFoo, "*")
+ # sess.query(RegularEntity).options(
+ # someload(RegularEntity.foos.of_type(wp))
+ # .someload(wp.SubFoo.bar)
+ # )
+ #
+ # in the former case, the Query as it generates a path that we
+ # want to match will be in terms of the with_polymorphic at the
+ # beginning. in the latter case, Query will generate simple
+ # paths that don't know about this with_polymorphic, so we must
+ # use a separate natural path.
+ #
+ #
+ if parent.parent:
+ natural_parent = parent.parent[subclass_entity.mapper]
+ self.is_unnatural = True
+ else:
+ natural_parent = parent
+ elif (
+ natural_parent.parent
+ and insp.is_aliased_class
+ and prop.parent # this should always be the case here
+ is not insp.mapper
+ and insp.mapper.isa(prop.parent)
+ ):
+ natural_parent = parent.parent[prop.parent]
+
+ self.prop = prop
+ self.parent = parent
+ self.path = parent.path + (prop,)
+ self.natural_path = natural_parent.natural_path + (prop,)
+
+ self.has_entity = prop._links_to_entity
+ if prop._is_relationship:
+ if TYPE_CHECKING:
+ assert isinstance(prop, RelationshipProperty)
+ self.entity = prop.entity
+ self.mapper = prop.mapper
+ else:
+ self.entity = None
+ self.mapper = None
+
+ self._wildcard_path_loader_key = (
+ "loader",
+ parent.natural_path + self.prop._wildcard_token,
+ )
+ self._default_path_loader_key = self.prop._default_path_loader_key
+ self._loader_key = ("loader", self.natural_path)
+
+ def _truncate_recursive(self) -> PropRegistry:
+ earliest = None
+ for i, token in enumerate(reversed(self.path[:-1])):
+ if token is self.prop:
+ earliest = i
+
+ if earliest is None:
+ return self
+ else:
+ return self.coerce(self.path[0 : -(earliest + 1)]) # type: ignore
+
+ @property
+ def entity_path(self) -> AbstractEntityRegistry:
+ assert self.entity is not None
+ return self[self.entity]
+
+ def _getitem(
+ self, entity: Union[int, slice, _InternalEntityType[Any]]
+ ) -> Union[AbstractEntityRegistry, _PathElementType, _PathRepresentation]:
+ if isinstance(entity, (int, slice)):
+ return self.path[entity]
+ else:
+ return SlotsEntityRegistry(self, entity)
+
+ if not TYPE_CHECKING:
+ __getitem__ = _getitem
+
+
+class AbstractEntityRegistry(CreatesToken):
+ __slots__ = (
+ "key",
+ "parent",
+ "is_aliased_class",
+ "path",
+ "entity",
+ "natural_path",
+ )
+
+ has_entity = True
+ is_entity = True
+
+ parent: Union[RootRegistry, PropRegistry]
+ key: _InternalEntityType[Any]
+ entity: _InternalEntityType[Any]
+ is_aliased_class: bool
+
+ def __init__(
+ self,
+ parent: Union[RootRegistry, PropRegistry],
+ entity: _InternalEntityType[Any],
+ ):
+ self.key = entity
+ self.parent = parent
+ self.is_aliased_class = entity.is_aliased_class
+ self.entity = entity
+ self.path = parent.path + (entity,)
+
+ # the "natural path" is the path that we get when Query is traversing
+ # from the lead entities into the various relationships; it corresponds
+ # to the structure of mappers and relationships. when we are given a
+ # path that comes from loader options, as of 1.3 it can have ac-hoc
+ # with_polymorphic() and other AliasedInsp objects inside of it, which
+ # are usually not present in mappings. So here we track both the
+ # "enhanced" path in self.path and the "natural" path that doesn't
+ # include those objects so these two traversals can be matched up.
+
+ # the test here for "(self.is_aliased_class or parent.is_unnatural)"
+ # are to avoid the more expensive conditional logic that follows if we
+ # know we don't have to do it. This conditional can just as well be
+ # "if parent.path:", it just is more function calls.
+ #
+ # This is basically the only place that the "is_unnatural" flag
+ # actually changes behavior.
+ if parent.path and (self.is_aliased_class or parent.is_unnatural):
+ # this is an infrequent code path used only for loader strategies
+ # that also make use of of_type().
+ if entity.mapper.isa(parent.natural_path[-1].mapper): # type: ignore # noqa: E501
+ self.natural_path = parent.natural_path + (entity.mapper,)
+ else:
+ self.natural_path = parent.natural_path + (
+ parent.natural_path[-1].entity, # type: ignore
+ )
+ # it seems to make sense that since these paths get mixed up
+ # with statements that are cached or not, we should make
+ # sure the natural path is cacheable across different occurrences
+ # of equivalent AliasedClass objects. however, so far this
+ # does not seem to be needed for whatever reason.
+ # elif not parent.path and self.is_aliased_class:
+ # self.natural_path = (self.entity._generate_cache_key()[0], )
+ else:
+ self.natural_path = self.path
+
+ def _truncate_recursive(self) -> AbstractEntityRegistry:
+ return self.parent._truncate_recursive()[self.entity]
+
+ @property
+ def root_entity(self) -> _InternalEntityType[Any]:
+ return self.odd_element(0)
+
+ @property
+ def entity_path(self) -> PathRegistry:
+ return self
+
+ @property
+ def mapper(self) -> Mapper[Any]:
+ return self.entity.mapper
+
+ def __bool__(self) -> bool:
+ return True
+
+ def _getitem(
+ self, entity: Any
+ ) -> Union[_PathElementType, _PathRepresentation, PathRegistry]:
+ if isinstance(entity, (int, slice)):
+ return self.path[entity]
+ elif entity in PathToken._intern:
+ return TokenRegistry(self, PathToken._intern[entity])
+ else:
+ return PropRegistry(self, entity)
+
+ if not TYPE_CHECKING:
+ __getitem__ = _getitem
+
+
+class SlotsEntityRegistry(AbstractEntityRegistry):
+ # for aliased class, return lightweight, no-cycles created
+ # version
+ inherit_cache = True
+
+
+class _ERDict(Dict[Any, Any]):
+ def __init__(self, registry: CachingEntityRegistry):
+ self.registry = registry
+
+ def __missing__(self, key: Any) -> PropRegistry:
+ self[key] = item = PropRegistry(self.registry, key)
+
+ return item
+
+
+class CachingEntityRegistry(AbstractEntityRegistry):
+ # for long lived mapper, return dict based caching
+ # version that creates reference cycles
+
+ __slots__ = ("_cache",)
+
+ inherit_cache = True
+
+ def __init__(
+ self,
+ parent: Union[RootRegistry, PropRegistry],
+ entity: _InternalEntityType[Any],
+ ):
+ super().__init__(parent, entity)
+ self._cache = _ERDict(self)
+
+ def pop(self, key: Any, default: Any) -> Any:
+ return self._cache.pop(key, default)
+
+ def _getitem(self, entity: Any) -> Any:
+ if isinstance(entity, (int, slice)):
+ return self.path[entity]
+ elif isinstance(entity, PathToken):
+ return TokenRegistry(self, entity)
+ else:
+ return self._cache[entity]
+
+ if not TYPE_CHECKING:
+ __getitem__ = _getitem
+
+
+if TYPE_CHECKING:
+
+ def path_is_entity(
+ path: PathRegistry,
+ ) -> TypeGuard[AbstractEntityRegistry]: ...
+
+ def path_is_property(path: PathRegistry) -> TypeGuard[PropRegistry]: ...
+
+else:
+ path_is_entity = operator.attrgetter("is_entity")
+ path_is_property = operator.attrgetter("is_property")
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/persistence.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/persistence.py
new file mode 100644
index 0000000..369fc59
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/persistence.py
@@ -0,0 +1,1782 @@
+# orm/persistence.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: ignore-errors
+
+
+"""private module containing functions used to emit INSERT, UPDATE
+and DELETE statements on behalf of a :class:`_orm.Mapper` and its descending
+mappers.
+
+The functions here are called only by the unit of work functions
+in unitofwork.py.
+
+"""
+from __future__ import annotations
+
+from itertools import chain
+from itertools import groupby
+from itertools import zip_longest
+import operator
+
+from . import attributes
+from . import exc as orm_exc
+from . import loading
+from . import sync
+from .base import state_str
+from .. import exc as sa_exc
+from .. import future
+from .. import sql
+from .. import util
+from ..engine import cursor as _cursor
+from ..sql import operators
+from ..sql.elements import BooleanClauseList
+from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
+
+
+def save_obj(base_mapper, states, uowtransaction, single=False):
+ """Issue ``INSERT`` and/or ``UPDATE`` statements for a list
+ of objects.
+
+ This is called within the context of a UOWTransaction during a
+ flush operation, given a list of states to be flushed. The
+ base mapper in an inheritance hierarchy handles the inserts/
+ updates for all descendant mappers.
+
+ """
+
+ # if batch=false, call _save_obj separately for each object
+ if not single and not base_mapper.batch:
+ for state in _sort_states(base_mapper, states):
+ save_obj(base_mapper, [state], uowtransaction, single=True)
+ return
+
+ states_to_update = []
+ states_to_insert = []
+
+ for (
+ state,
+ dict_,
+ mapper,
+ connection,
+ has_identity,
+ row_switch,
+ update_version_id,
+ ) in _organize_states_for_save(base_mapper, states, uowtransaction):
+ if has_identity or row_switch:
+ states_to_update.append(
+ (state, dict_, mapper, connection, update_version_id)
+ )
+ else:
+ states_to_insert.append((state, dict_, mapper, connection))
+
+ for table, mapper in base_mapper._sorted_tables.items():
+ if table not in mapper._pks_by_table:
+ continue
+ insert = _collect_insert_commands(table, states_to_insert)
+
+ update = _collect_update_commands(
+ uowtransaction, table, states_to_update
+ )
+
+ _emit_update_statements(
+ base_mapper,
+ uowtransaction,
+ mapper,
+ table,
+ update,
+ )
+
+ _emit_insert_statements(
+ base_mapper,
+ uowtransaction,
+ mapper,
+ table,
+ insert,
+ )
+
+ _finalize_insert_update_commands(
+ base_mapper,
+ uowtransaction,
+ chain(
+ (
+ (state, state_dict, mapper, connection, False)
+ for (state, state_dict, mapper, connection) in states_to_insert
+ ),
+ (
+ (state, state_dict, mapper, connection, True)
+ for (
+ state,
+ state_dict,
+ mapper,
+ connection,
+ update_version_id,
+ ) in states_to_update
+ ),
+ ),
+ )
+
+
+def post_update(base_mapper, states, uowtransaction, post_update_cols):
+ """Issue UPDATE statements on behalf of a relationship() which
+ specifies post_update.
+
+ """
+
+ states_to_update = list(
+ _organize_states_for_post_update(base_mapper, states, uowtransaction)
+ )
+
+ for table, mapper in base_mapper._sorted_tables.items():
+ if table not in mapper._pks_by_table:
+ continue
+
+ update = (
+ (
+ state,
+ state_dict,
+ sub_mapper,
+ connection,
+ (
+ mapper._get_committed_state_attr_by_column(
+ state, state_dict, mapper.version_id_col
+ )
+ if mapper.version_id_col is not None
+ else None
+ ),
+ )
+ for state, state_dict, sub_mapper, connection in states_to_update
+ if table in sub_mapper._pks_by_table
+ )
+
+ update = _collect_post_update_commands(
+ base_mapper, uowtransaction, table, update, post_update_cols
+ )
+
+ _emit_post_update_statements(
+ base_mapper,
+ uowtransaction,
+ mapper,
+ table,
+ update,
+ )
+
+
+def delete_obj(base_mapper, states, uowtransaction):
+ """Issue ``DELETE`` statements for a list of objects.
+
+ This is called within the context of a UOWTransaction during a
+ flush operation.
+
+ """
+
+ states_to_delete = list(
+ _organize_states_for_delete(base_mapper, states, uowtransaction)
+ )
+
+ table_to_mapper = base_mapper._sorted_tables
+
+ for table in reversed(list(table_to_mapper.keys())):
+ mapper = table_to_mapper[table]
+ if table not in mapper._pks_by_table:
+ continue
+ elif mapper.inherits and mapper.passive_deletes:
+ continue
+
+ delete = _collect_delete_commands(
+ base_mapper, uowtransaction, table, states_to_delete
+ )
+
+ _emit_delete_statements(
+ base_mapper,
+ uowtransaction,
+ mapper,
+ table,
+ delete,
+ )
+
+ for (
+ state,
+ state_dict,
+ mapper,
+ connection,
+ update_version_id,
+ ) in states_to_delete:
+ mapper.dispatch.after_delete(mapper, connection, state)
+
+
+def _organize_states_for_save(base_mapper, states, uowtransaction):
+ """Make an initial pass across a set of states for INSERT or
+ UPDATE.
+
+ This includes splitting out into distinct lists for
+ each, calling before_insert/before_update, obtaining
+ key information for each state including its dictionary,
+ mapper, the connection to use for the execution per state,
+ and the identity flag.
+
+ """
+
+ for state, dict_, mapper, connection in _connections_for_states(
+ base_mapper, uowtransaction, states
+ ):
+ has_identity = bool(state.key)
+
+ instance_key = state.key or mapper._identity_key_from_state(state)
+
+ row_switch = update_version_id = None
+
+ # call before_XXX extensions
+ if not has_identity:
+ mapper.dispatch.before_insert(mapper, connection, state)
+ else:
+ mapper.dispatch.before_update(mapper, connection, state)
+
+ if mapper._validate_polymorphic_identity:
+ mapper._validate_polymorphic_identity(mapper, state, dict_)
+
+ # detect if we have a "pending" instance (i.e. has
+ # no instance_key attached to it), and another instance
+ # with the same identity key already exists as persistent.
+ # convert to an UPDATE if so.
+ if (
+ not has_identity
+ and instance_key in uowtransaction.session.identity_map
+ ):
+ instance = uowtransaction.session.identity_map[instance_key]
+ existing = attributes.instance_state(instance)
+
+ if not uowtransaction.was_already_deleted(existing):
+ if not uowtransaction.is_deleted(existing):
+ util.warn(
+ "New instance %s with identity key %s conflicts "
+ "with persistent instance %s"
+ % (state_str(state), instance_key, state_str(existing))
+ )
+ else:
+ base_mapper._log_debug(
+ "detected row switch for identity %s. "
+ "will update %s, remove %s from "
+ "transaction",
+ instance_key,
+ state_str(state),
+ state_str(existing),
+ )
+
+ # remove the "delete" flag from the existing element
+ uowtransaction.remove_state_actions(existing)
+ row_switch = existing
+
+ if (has_identity or row_switch) and mapper.version_id_col is not None:
+ update_version_id = mapper._get_committed_state_attr_by_column(
+ row_switch if row_switch else state,
+ row_switch.dict if row_switch else dict_,
+ mapper.version_id_col,
+ )
+
+ yield (
+ state,
+ dict_,
+ mapper,
+ connection,
+ has_identity,
+ row_switch,
+ update_version_id,
+ )
+
+
+def _organize_states_for_post_update(base_mapper, states, uowtransaction):
+ """Make an initial pass across a set of states for UPDATE
+ corresponding to post_update.
+
+ This includes obtaining key information for each state
+ including its dictionary, mapper, the connection to use for
+ the execution per state.
+
+ """
+ return _connections_for_states(base_mapper, uowtransaction, states)
+
+
+def _organize_states_for_delete(base_mapper, states, uowtransaction):
+ """Make an initial pass across a set of states for DELETE.
+
+ This includes calling out before_delete and obtaining
+ key information for each state including its dictionary,
+ mapper, the connection to use for the execution per state.
+
+ """
+ for state, dict_, mapper, connection in _connections_for_states(
+ base_mapper, uowtransaction, states
+ ):
+ mapper.dispatch.before_delete(mapper, connection, state)
+
+ if mapper.version_id_col is not None:
+ update_version_id = mapper._get_committed_state_attr_by_column(
+ state, dict_, mapper.version_id_col
+ )
+ else:
+ update_version_id = None
+
+ yield (state, dict_, mapper, connection, update_version_id)
+
+
+def _collect_insert_commands(
+ table,
+ states_to_insert,
+ *,
+ bulk=False,
+ return_defaults=False,
+ render_nulls=False,
+ include_bulk_keys=(),
+):
+ """Identify sets of values to use in INSERT statements for a
+ list of states.
+
+ """
+ for state, state_dict, mapper, connection in states_to_insert:
+ if table not in mapper._pks_by_table:
+ continue
+
+ params = {}
+ value_params = {}
+
+ propkey_to_col = mapper._propkey_to_col[table]
+
+ eval_none = mapper._insert_cols_evaluating_none[table]
+
+ for propkey in set(propkey_to_col).intersection(state_dict):
+ value = state_dict[propkey]
+ col = propkey_to_col[propkey]
+ if value is None and col not in eval_none and not render_nulls:
+ continue
+ elif not bulk and (
+ hasattr(value, "__clause_element__")
+ or isinstance(value, sql.ClauseElement)
+ ):
+ value_params[col] = (
+ value.__clause_element__()
+ if hasattr(value, "__clause_element__")
+ else value
+ )
+ else:
+ params[col.key] = value
+
+ if not bulk:
+ # for all the columns that have no default and we don't have
+ # a value and where "None" is not a special value, add
+ # explicit None to the INSERT. This is a legacy behavior
+ # which might be worth removing, as it should not be necessary
+ # and also produces confusion, given that "missing" and None
+ # now have distinct meanings
+ for colkey in (
+ mapper._insert_cols_as_none[table]
+ .difference(params)
+ .difference([c.key for c in value_params])
+ ):
+ params[colkey] = None
+
+ if not bulk or return_defaults:
+ # params are in terms of Column key objects, so
+ # compare to pk_keys_by_table
+ has_all_pks = mapper._pk_keys_by_table[table].issubset(params)
+
+ if mapper.base_mapper._prefer_eager_defaults(
+ connection.dialect, table
+ ):
+ has_all_defaults = mapper._server_default_col_keys[
+ table
+ ].issubset(params)
+ else:
+ has_all_defaults = True
+ else:
+ has_all_defaults = has_all_pks = True
+
+ if (
+ mapper.version_id_generator is not False
+ and mapper.version_id_col is not None
+ and mapper.version_id_col in mapper._cols_by_table[table]
+ ):
+ params[mapper.version_id_col.key] = mapper.version_id_generator(
+ None
+ )
+
+ if bulk:
+ if mapper._set_polymorphic_identity:
+ params.setdefault(
+ mapper._polymorphic_attr_key, mapper.polymorphic_identity
+ )
+
+ if include_bulk_keys:
+ params.update((k, state_dict[k]) for k in include_bulk_keys)
+
+ yield (
+ state,
+ state_dict,
+ params,
+ mapper,
+ connection,
+ value_params,
+ has_all_pks,
+ has_all_defaults,
+ )
+
+
+def _collect_update_commands(
+ uowtransaction,
+ table,
+ states_to_update,
+ *,
+ bulk=False,
+ use_orm_update_stmt=None,
+ include_bulk_keys=(),
+):
+ """Identify sets of values to use in UPDATE statements for a
+ list of states.
+
+ This function works intricately with the history system
+ to determine exactly what values should be updated
+ as well as how the row should be matched within an UPDATE
+ statement. Includes some tricky scenarios where the primary
+ key of an object might have been changed.
+
+ """
+
+ for (
+ state,
+ state_dict,
+ mapper,
+ connection,
+ update_version_id,
+ ) in states_to_update:
+ if table not in mapper._pks_by_table:
+ continue
+
+ pks = mapper._pks_by_table[table]
+
+ if use_orm_update_stmt is not None:
+ # TODO: ordered values, etc
+ value_params = use_orm_update_stmt._values
+ else:
+ value_params = {}
+
+ propkey_to_col = mapper._propkey_to_col[table]
+
+ if bulk:
+ # keys here are mapped attribute keys, so
+ # look at mapper attribute keys for pk
+ params = {
+ propkey_to_col[propkey].key: state_dict[propkey]
+ for propkey in set(propkey_to_col)
+ .intersection(state_dict)
+ .difference(mapper._pk_attr_keys_by_table[table])
+ }
+ has_all_defaults = True
+ else:
+ params = {}
+ for propkey in set(propkey_to_col).intersection(
+ state.committed_state
+ ):
+ value = state_dict[propkey]
+ col = propkey_to_col[propkey]
+
+ if hasattr(value, "__clause_element__") or isinstance(
+ value, sql.ClauseElement
+ ):
+ value_params[col] = (
+ value.__clause_element__()
+ if hasattr(value, "__clause_element__")
+ else value
+ )
+ # guard against values that generate non-__nonzero__
+ # objects for __eq__()
+ elif (
+ state.manager[propkey].impl.is_equal(
+ value, state.committed_state[propkey]
+ )
+ is not True
+ ):
+ params[col.key] = value
+
+ if mapper.base_mapper.eager_defaults is True:
+ has_all_defaults = (
+ mapper._server_onupdate_default_col_keys[table]
+ ).issubset(params)
+ else:
+ has_all_defaults = True
+
+ if (
+ update_version_id is not None
+ and mapper.version_id_col in mapper._cols_by_table[table]
+ ):
+ if not bulk and not (params or value_params):
+ # HACK: check for history in other tables, in case the
+ # history is only in a different table than the one
+ # where the version_id_col is. This logic was lost
+ # from 0.9 -> 1.0.0 and restored in 1.0.6.
+ for prop in mapper._columntoproperty.values():
+ history = state.manager[prop.key].impl.get_history(
+ state, state_dict, attributes.PASSIVE_NO_INITIALIZE
+ )
+ if history.added:
+ break
+ else:
+ # no net change, break
+ continue
+
+ col = mapper.version_id_col
+ no_params = not params and not value_params
+ params[col._label] = update_version_id
+
+ if (
+ bulk or col.key not in params
+ ) and mapper.version_id_generator is not False:
+ val = mapper.version_id_generator(update_version_id)
+ params[col.key] = val
+ elif mapper.version_id_generator is False and no_params:
+ # no version id generator, no values set on the table,
+ # and version id wasn't manually incremented.
+ # set version id to itself so we get an UPDATE
+ # statement
+ params[col.key] = update_version_id
+
+ elif not (params or value_params):
+ continue
+
+ has_all_pks = True
+ expect_pk_cascaded = False
+ if bulk:
+ # keys here are mapped attribute keys, so
+ # look at mapper attribute keys for pk
+ pk_params = {
+ propkey_to_col[propkey]._label: state_dict.get(propkey)
+ for propkey in set(propkey_to_col).intersection(
+ mapper._pk_attr_keys_by_table[table]
+ )
+ }
+ if util.NONE_SET.intersection(pk_params.values()):
+ raise sa_exc.InvalidRequestError(
+ f"No primary key value supplied for column(s) "
+ f"""{
+ ', '.join(
+ str(c) for c in pks if pk_params[c._label] is None
+ )
+ }; """
+ "per-row ORM Bulk UPDATE by Primary Key requires that "
+ "records contain primary key values",
+ code="bupq",
+ )
+
+ else:
+ pk_params = {}
+ for col in pks:
+ propkey = mapper._columntoproperty[col].key
+
+ history = state.manager[propkey].impl.get_history(
+ state, state_dict, attributes.PASSIVE_OFF
+ )
+
+ if history.added:
+ if (
+ not history.deleted
+ or ("pk_cascaded", state, col)
+ in uowtransaction.attributes
+ ):
+ expect_pk_cascaded = True
+ pk_params[col._label] = history.added[0]
+ params.pop(col.key, None)
+ else:
+ # else, use the old value to locate the row
+ pk_params[col._label] = history.deleted[0]
+ if col in value_params:
+ has_all_pks = False
+ else:
+ pk_params[col._label] = history.unchanged[0]
+ if pk_params[col._label] is None:
+ raise orm_exc.FlushError(
+ "Can't update table %s using NULL for primary "
+ "key value on column %s" % (table, col)
+ )
+
+ if include_bulk_keys:
+ params.update((k, state_dict[k]) for k in include_bulk_keys)
+
+ if params or value_params:
+ params.update(pk_params)
+ yield (
+ state,
+ state_dict,
+ params,
+ mapper,
+ connection,
+ value_params,
+ has_all_defaults,
+ has_all_pks,
+ )
+ elif expect_pk_cascaded:
+ # no UPDATE occurs on this table, but we expect that CASCADE rules
+ # have changed the primary key of the row; propagate this event to
+ # other columns that expect to have been modified. this normally
+ # occurs after the UPDATE is emitted however we invoke it here
+ # explicitly in the absence of our invoking an UPDATE
+ for m, equated_pairs in mapper._table_to_equated[table]:
+ sync.populate(
+ state,
+ m,
+ state,
+ m,
+ equated_pairs,
+ uowtransaction,
+ mapper.passive_updates,
+ )
+
+
+def _collect_post_update_commands(
+ base_mapper, uowtransaction, table, states_to_update, post_update_cols
+):
+ """Identify sets of values to use in UPDATE statements for a
+ list of states within a post_update operation.
+
+ """
+
+ for (
+ state,
+ state_dict,
+ mapper,
+ connection,
+ update_version_id,
+ ) in states_to_update:
+ # assert table in mapper._pks_by_table
+
+ pks = mapper._pks_by_table[table]
+ params = {}
+ hasdata = False
+
+ for col in mapper._cols_by_table[table]:
+ if col in pks:
+ params[col._label] = mapper._get_state_attr_by_column(
+ state, state_dict, col, passive=attributes.PASSIVE_OFF
+ )
+
+ elif col in post_update_cols or col.onupdate is not None:
+ prop = mapper._columntoproperty[col]
+ history = state.manager[prop.key].impl.get_history(
+ state, state_dict, attributes.PASSIVE_NO_INITIALIZE
+ )
+ if history.added:
+ value = history.added[0]
+ params[col.key] = value
+ hasdata = True
+ if hasdata:
+ if (
+ update_version_id is not None
+ and mapper.version_id_col in mapper._cols_by_table[table]
+ ):
+ col = mapper.version_id_col
+ params[col._label] = update_version_id
+
+ if (
+ bool(state.key)
+ and col.key not in params
+ and mapper.version_id_generator is not False
+ ):
+ val = mapper.version_id_generator(update_version_id)
+ params[col.key] = val
+ yield state, state_dict, mapper, connection, params
+
+
+def _collect_delete_commands(
+ base_mapper, uowtransaction, table, states_to_delete
+):
+ """Identify values to use in DELETE statements for a list of
+ states to be deleted."""
+
+ for (
+ state,
+ state_dict,
+ mapper,
+ connection,
+ update_version_id,
+ ) in states_to_delete:
+ if table not in mapper._pks_by_table:
+ continue
+
+ params = {}
+ for col in mapper._pks_by_table[table]:
+ params[col.key] = value = (
+ mapper._get_committed_state_attr_by_column(
+ state, state_dict, col
+ )
+ )
+ if value is None:
+ raise orm_exc.FlushError(
+ "Can't delete from table %s "
+ "using NULL for primary "
+ "key value on column %s" % (table, col)
+ )
+
+ if (
+ update_version_id is not None
+ and mapper.version_id_col in mapper._cols_by_table[table]
+ ):
+ params[mapper.version_id_col.key] = update_version_id
+ yield params, connection
+
+
+def _emit_update_statements(
+ base_mapper,
+ uowtransaction,
+ mapper,
+ table,
+ update,
+ *,
+ bookkeeping=True,
+ use_orm_update_stmt=None,
+ enable_check_rowcount=True,
+):
+ """Emit UPDATE statements corresponding to value lists collected
+ by _collect_update_commands()."""
+
+ needs_version_id = (
+ mapper.version_id_col is not None
+ and mapper.version_id_col in mapper._cols_by_table[table]
+ )
+
+ execution_options = {"compiled_cache": base_mapper._compiled_cache}
+
+ def update_stmt(existing_stmt=None):
+ clauses = BooleanClauseList._construct_raw(operators.and_)
+
+ for col in mapper._pks_by_table[table]:
+ clauses._append_inplace(
+ col == sql.bindparam(col._label, type_=col.type)
+ )
+
+ if needs_version_id:
+ clauses._append_inplace(
+ mapper.version_id_col
+ == sql.bindparam(
+ mapper.version_id_col._label,
+ type_=mapper.version_id_col.type,
+ )
+ )
+
+ if existing_stmt is not None:
+ stmt = existing_stmt.where(clauses)
+ else:
+ stmt = table.update().where(clauses)
+ return stmt
+
+ if use_orm_update_stmt is not None:
+ cached_stmt = update_stmt(use_orm_update_stmt)
+
+ else:
+ cached_stmt = base_mapper._memo(("update", table), update_stmt)
+
+ for (
+ (connection, paramkeys, hasvalue, has_all_defaults, has_all_pks),
+ records,
+ ) in groupby(
+ update,
+ lambda rec: (
+ rec[4], # connection
+ set(rec[2]), # set of parameter keys
+ bool(rec[5]), # whether or not we have "value" parameters
+ rec[6], # has_all_defaults
+ rec[7], # has all pks
+ ),
+ ):
+ rows = 0
+ records = list(records)
+
+ statement = cached_stmt
+
+ if use_orm_update_stmt is not None:
+ statement = statement._annotate(
+ {
+ "_emit_update_table": table,
+ "_emit_update_mapper": mapper,
+ }
+ )
+
+ return_defaults = False
+
+ if not has_all_pks:
+ statement = statement.return_defaults(*mapper._pks_by_table[table])
+ return_defaults = True
+
+ if (
+ bookkeeping
+ and not has_all_defaults
+ and mapper.base_mapper.eager_defaults is True
+ # change as of #8889 - if RETURNING is not going to be used anyway,
+ # (applies to MySQL, MariaDB which lack UPDATE RETURNING) ensure
+ # we can do an executemany UPDATE which is more efficient
+ and table.implicit_returning
+ and connection.dialect.update_returning
+ ):
+ statement = statement.return_defaults(
+ *mapper._server_onupdate_default_cols[table]
+ )
+ return_defaults = True
+
+ if mapper._version_id_has_server_side_value:
+ statement = statement.return_defaults(mapper.version_id_col)
+ return_defaults = True
+
+ assert_singlerow = connection.dialect.supports_sane_rowcount
+
+ assert_multirow = (
+ assert_singlerow
+ and connection.dialect.supports_sane_multi_rowcount
+ )
+
+ # change as of #8889 - if RETURNING is not going to be used anyway,
+ # (applies to MySQL, MariaDB which lack UPDATE RETURNING) ensure
+ # we can do an executemany UPDATE which is more efficient
+ allow_executemany = not return_defaults and not needs_version_id
+
+ if hasvalue:
+ for (
+ state,
+ state_dict,
+ params,
+ mapper,
+ connection,
+ value_params,
+ has_all_defaults,
+ has_all_pks,
+ ) in records:
+ c = connection.execute(
+ statement.values(value_params),
+ params,
+ execution_options=execution_options,
+ )
+ if bookkeeping:
+ _postfetch(
+ mapper,
+ uowtransaction,
+ table,
+ state,
+ state_dict,
+ c,
+ c.context.compiled_parameters[0],
+ value_params,
+ True,
+ c.returned_defaults,
+ )
+ rows += c.rowcount
+ check_rowcount = enable_check_rowcount and assert_singlerow
+ else:
+ if not allow_executemany:
+ check_rowcount = enable_check_rowcount and assert_singlerow
+ for (
+ state,
+ state_dict,
+ params,
+ mapper,
+ connection,
+ value_params,
+ has_all_defaults,
+ has_all_pks,
+ ) in records:
+ c = connection.execute(
+ statement, params, execution_options=execution_options
+ )
+
+ # TODO: why with bookkeeping=False?
+ if bookkeeping:
+ _postfetch(
+ mapper,
+ uowtransaction,
+ table,
+ state,
+ state_dict,
+ c,
+ c.context.compiled_parameters[0],
+ value_params,
+ True,
+ c.returned_defaults,
+ )
+ rows += c.rowcount
+ else:
+ multiparams = [rec[2] for rec in records]
+
+ check_rowcount = enable_check_rowcount and (
+ assert_multirow
+ or (assert_singlerow and len(multiparams) == 1)
+ )
+
+ c = connection.execute(
+ statement, multiparams, execution_options=execution_options
+ )
+
+ rows += c.rowcount
+
+ for (
+ state,
+ state_dict,
+ params,
+ mapper,
+ connection,
+ value_params,
+ has_all_defaults,
+ has_all_pks,
+ ) in records:
+ if bookkeeping:
+ _postfetch(
+ mapper,
+ uowtransaction,
+ table,
+ state,
+ state_dict,
+ c,
+ c.context.compiled_parameters[0],
+ value_params,
+ True,
+ (
+ c.returned_defaults
+ if not c.context.executemany
+ else None
+ ),
+ )
+
+ if check_rowcount:
+ if rows != len(records):
+ raise orm_exc.StaleDataError(
+ "UPDATE statement on table '%s' expected to "
+ "update %d row(s); %d were matched."
+ % (table.description, len(records), rows)
+ )
+
+ elif needs_version_id:
+ util.warn(
+ "Dialect %s does not support updated rowcount "
+ "- versioning cannot be verified."
+ % c.dialect.dialect_description
+ )
+
+
+def _emit_insert_statements(
+ base_mapper,
+ uowtransaction,
+ mapper,
+ table,
+ insert,
+ *,
+ bookkeeping=True,
+ use_orm_insert_stmt=None,
+ execution_options=None,
+):
+ """Emit INSERT statements corresponding to value lists collected
+ by _collect_insert_commands()."""
+
+ if use_orm_insert_stmt is not None:
+ cached_stmt = use_orm_insert_stmt
+ exec_opt = util.EMPTY_DICT
+
+ # if a user query with RETURNING was passed, we definitely need
+ # to use RETURNING.
+ returning_is_required_anyway = bool(use_orm_insert_stmt._returning)
+ deterministic_results_reqd = (
+ returning_is_required_anyway
+ and use_orm_insert_stmt._sort_by_parameter_order
+ ) or bookkeeping
+ else:
+ returning_is_required_anyway = False
+ deterministic_results_reqd = bookkeeping
+ cached_stmt = base_mapper._memo(("insert", table), table.insert)
+ exec_opt = {"compiled_cache": base_mapper._compiled_cache}
+
+ if execution_options:
+ execution_options = util.EMPTY_DICT.merge_with(
+ exec_opt, execution_options
+ )
+ else:
+ execution_options = exec_opt
+
+ return_result = None
+
+ for (
+ (connection, _, hasvalue, has_all_pks, has_all_defaults),
+ records,
+ ) in groupby(
+ insert,
+ lambda rec: (
+ rec[4], # connection
+ set(rec[2]), # parameter keys
+ bool(rec[5]), # whether we have "value" parameters
+ rec[6],
+ rec[7],
+ ),
+ ):
+ statement = cached_stmt
+
+ if use_orm_insert_stmt is not None:
+ statement = statement._annotate(
+ {
+ "_emit_insert_table": table,
+ "_emit_insert_mapper": mapper,
+ }
+ )
+
+ if (
+ (
+ not bookkeeping
+ or (
+ has_all_defaults
+ or not base_mapper._prefer_eager_defaults(
+ connection.dialect, table
+ )
+ or not table.implicit_returning
+ or not connection.dialect.insert_returning
+ )
+ )
+ and not returning_is_required_anyway
+ and has_all_pks
+ and not hasvalue
+ ):
+ # the "we don't need newly generated values back" section.
+ # here we have all the PKs, all the defaults or we don't want
+ # to fetch them, or the dialect doesn't support RETURNING at all
+ # so we have to post-fetch / use lastrowid anyway.
+ records = list(records)
+ multiparams = [rec[2] for rec in records]
+
+ result = connection.execute(
+ statement, multiparams, execution_options=execution_options
+ )
+ if bookkeeping:
+ for (
+ (
+ state,
+ state_dict,
+ params,
+ mapper_rec,
+ conn,
+ value_params,
+ has_all_pks,
+ has_all_defaults,
+ ),
+ last_inserted_params,
+ ) in zip(records, result.context.compiled_parameters):
+ if state:
+ _postfetch(
+ mapper_rec,
+ uowtransaction,
+ table,
+ state,
+ state_dict,
+ result,
+ last_inserted_params,
+ value_params,
+ False,
+ (
+ result.returned_defaults
+ if not result.context.executemany
+ else None
+ ),
+ )
+ else:
+ _postfetch_bulk_save(mapper_rec, state_dict, table)
+
+ else:
+ # here, we need defaults and/or pk values back or we otherwise
+ # know that we are using RETURNING in any case
+
+ records = list(records)
+
+ if returning_is_required_anyway or (
+ table.implicit_returning and not hasvalue and len(records) > 1
+ ):
+ if (
+ deterministic_results_reqd
+ and connection.dialect.insert_executemany_returning_sort_by_parameter_order # noqa: E501
+ ) or (
+ not deterministic_results_reqd
+ and connection.dialect.insert_executemany_returning
+ ):
+ do_executemany = True
+ elif returning_is_required_anyway:
+ if deterministic_results_reqd:
+ dt = " with RETURNING and sort by parameter order"
+ else:
+ dt = " with RETURNING"
+ raise sa_exc.InvalidRequestError(
+ f"Can't use explicit RETURNING for bulk INSERT "
+ f"operation with "
+ f"{connection.dialect.dialect_description} backend; "
+ f"executemany{dt} is not enabled for this dialect."
+ )
+ else:
+ do_executemany = False
+ else:
+ do_executemany = False
+
+ if use_orm_insert_stmt is None:
+ if (
+ not has_all_defaults
+ and base_mapper._prefer_eager_defaults(
+ connection.dialect, table
+ )
+ ):
+ statement = statement.return_defaults(
+ *mapper._server_default_cols[table],
+ sort_by_parameter_order=bookkeeping,
+ )
+
+ if mapper.version_id_col is not None:
+ statement = statement.return_defaults(
+ mapper.version_id_col,
+ sort_by_parameter_order=bookkeeping,
+ )
+ elif do_executemany:
+ statement = statement.return_defaults(
+ *table.primary_key, sort_by_parameter_order=bookkeeping
+ )
+
+ if do_executemany:
+ multiparams = [rec[2] for rec in records]
+
+ result = connection.execute(
+ statement, multiparams, execution_options=execution_options
+ )
+
+ if use_orm_insert_stmt is not None:
+ if return_result is None:
+ return_result = result
+ else:
+ return_result = return_result.splice_vertically(result)
+
+ if bookkeeping:
+ for (
+ (
+ state,
+ state_dict,
+ params,
+ mapper_rec,
+ conn,
+ value_params,
+ has_all_pks,
+ has_all_defaults,
+ ),
+ last_inserted_params,
+ inserted_primary_key,
+ returned_defaults,
+ ) in zip_longest(
+ records,
+ result.context.compiled_parameters,
+ result.inserted_primary_key_rows,
+ result.returned_defaults_rows or (),
+ ):
+ if inserted_primary_key is None:
+ # this is a real problem and means that we didn't
+ # get back as many PK rows. we can't continue
+ # since this indicates PK rows were missing, which
+ # means we likely mis-populated records starting
+ # at that point with incorrectly matched PK
+ # values.
+ raise orm_exc.FlushError(
+ "Multi-row INSERT statement for %s did not "
+ "produce "
+ "the correct number of INSERTed rows for "
+ "RETURNING. Ensure there are no triggers or "
+ "special driver issues preventing INSERT from "
+ "functioning properly." % mapper_rec
+ )
+
+ for pk, col in zip(
+ inserted_primary_key,
+ mapper._pks_by_table[table],
+ ):
+ prop = mapper_rec._columntoproperty[col]
+ if state_dict.get(prop.key) is None:
+ state_dict[prop.key] = pk
+
+ if state:
+ _postfetch(
+ mapper_rec,
+ uowtransaction,
+ table,
+ state,
+ state_dict,
+ result,
+ last_inserted_params,
+ value_params,
+ False,
+ returned_defaults,
+ )
+ else:
+ _postfetch_bulk_save(mapper_rec, state_dict, table)
+ else:
+ assert not returning_is_required_anyway
+
+ for (
+ state,
+ state_dict,
+ params,
+ mapper_rec,
+ connection,
+ value_params,
+ has_all_pks,
+ has_all_defaults,
+ ) in records:
+ if value_params:
+ result = connection.execute(
+ statement.values(value_params),
+ params,
+ execution_options=execution_options,
+ )
+ else:
+ result = connection.execute(
+ statement,
+ params,
+ execution_options=execution_options,
+ )
+
+ primary_key = result.inserted_primary_key
+ if primary_key is None:
+ raise orm_exc.FlushError(
+ "Single-row INSERT statement for %s "
+ "did not produce a "
+ "new primary key result "
+ "being invoked. Ensure there are no triggers or "
+ "special driver issues preventing INSERT from "
+ "functioning properly." % (mapper_rec,)
+ )
+ for pk, col in zip(
+ primary_key, mapper._pks_by_table[table]
+ ):
+ prop = mapper_rec._columntoproperty[col]
+ if (
+ col in value_params
+ or state_dict.get(prop.key) is None
+ ):
+ state_dict[prop.key] = pk
+ if bookkeeping:
+ if state:
+ _postfetch(
+ mapper_rec,
+ uowtransaction,
+ table,
+ state,
+ state_dict,
+ result,
+ result.context.compiled_parameters[0],
+ value_params,
+ False,
+ (
+ result.returned_defaults
+ if not result.context.executemany
+ else None
+ ),
+ )
+ else:
+ _postfetch_bulk_save(mapper_rec, state_dict, table)
+
+ if use_orm_insert_stmt is not None:
+ if return_result is None:
+ return _cursor.null_dml_result()
+ else:
+ return return_result
+
+
+def _emit_post_update_statements(
+ base_mapper, uowtransaction, mapper, table, update
+):
+ """Emit UPDATE statements corresponding to value lists collected
+ by _collect_post_update_commands()."""
+
+ execution_options = {"compiled_cache": base_mapper._compiled_cache}
+
+ needs_version_id = (
+ mapper.version_id_col is not None
+ and mapper.version_id_col in mapper._cols_by_table[table]
+ )
+
+ def update_stmt():
+ clauses = BooleanClauseList._construct_raw(operators.and_)
+
+ for col in mapper._pks_by_table[table]:
+ clauses._append_inplace(
+ col == sql.bindparam(col._label, type_=col.type)
+ )
+
+ if needs_version_id:
+ clauses._append_inplace(
+ mapper.version_id_col
+ == sql.bindparam(
+ mapper.version_id_col._label,
+ type_=mapper.version_id_col.type,
+ )
+ )
+
+ stmt = table.update().where(clauses)
+
+ return stmt
+
+ statement = base_mapper._memo(("post_update", table), update_stmt)
+
+ if mapper._version_id_has_server_side_value:
+ statement = statement.return_defaults(mapper.version_id_col)
+
+ # execute each UPDATE in the order according to the original
+ # list of states to guarantee row access order, but
+ # also group them into common (connection, cols) sets
+ # to support executemany().
+ for key, records in groupby(
+ update,
+ lambda rec: (rec[3], set(rec[4])), # connection # parameter keys
+ ):
+ rows = 0
+
+ records = list(records)
+ connection = key[0]
+
+ assert_singlerow = connection.dialect.supports_sane_rowcount
+ assert_multirow = (
+ assert_singlerow
+ and connection.dialect.supports_sane_multi_rowcount
+ )
+ allow_executemany = not needs_version_id or assert_multirow
+
+ if not allow_executemany:
+ check_rowcount = assert_singlerow
+ for state, state_dict, mapper_rec, connection, params in records:
+ c = connection.execute(
+ statement, params, execution_options=execution_options
+ )
+
+ _postfetch_post_update(
+ mapper_rec,
+ uowtransaction,
+ table,
+ state,
+ state_dict,
+ c,
+ c.context.compiled_parameters[0],
+ )
+ rows += c.rowcount
+ else:
+ multiparams = [
+ params
+ for state, state_dict, mapper_rec, conn, params in records
+ ]
+
+ check_rowcount = assert_multirow or (
+ assert_singlerow and len(multiparams) == 1
+ )
+
+ c = connection.execute(
+ statement, multiparams, execution_options=execution_options
+ )
+
+ rows += c.rowcount
+ for state, state_dict, mapper_rec, connection, params in records:
+ _postfetch_post_update(
+ mapper_rec,
+ uowtransaction,
+ table,
+ state,
+ state_dict,
+ c,
+ c.context.compiled_parameters[0],
+ )
+
+ if check_rowcount:
+ if rows != len(records):
+ raise orm_exc.StaleDataError(
+ "UPDATE statement on table '%s' expected to "
+ "update %d row(s); %d were matched."
+ % (table.description, len(records), rows)
+ )
+
+ elif needs_version_id:
+ util.warn(
+ "Dialect %s does not support updated rowcount "
+ "- versioning cannot be verified."
+ % c.dialect.dialect_description
+ )
+
+
+def _emit_delete_statements(
+ base_mapper, uowtransaction, mapper, table, delete
+):
+ """Emit DELETE statements corresponding to value lists collected
+ by _collect_delete_commands()."""
+
+ need_version_id = (
+ mapper.version_id_col is not None
+ and mapper.version_id_col in mapper._cols_by_table[table]
+ )
+
+ def delete_stmt():
+ clauses = BooleanClauseList._construct_raw(operators.and_)
+
+ for col in mapper._pks_by_table[table]:
+ clauses._append_inplace(
+ col == sql.bindparam(col.key, type_=col.type)
+ )
+
+ if need_version_id:
+ clauses._append_inplace(
+ mapper.version_id_col
+ == sql.bindparam(
+ mapper.version_id_col.key, type_=mapper.version_id_col.type
+ )
+ )
+
+ return table.delete().where(clauses)
+
+ statement = base_mapper._memo(("delete", table), delete_stmt)
+ for connection, recs in groupby(delete, lambda rec: rec[1]): # connection
+ del_objects = [params for params, connection in recs]
+
+ execution_options = {"compiled_cache": base_mapper._compiled_cache}
+ expected = len(del_objects)
+ rows_matched = -1
+ only_warn = False
+
+ if (
+ need_version_id
+ and not connection.dialect.supports_sane_multi_rowcount
+ ):
+ if connection.dialect.supports_sane_rowcount:
+ rows_matched = 0
+ # execute deletes individually so that versioned
+ # rows can be verified
+ for params in del_objects:
+ c = connection.execute(
+ statement, params, execution_options=execution_options
+ )
+ rows_matched += c.rowcount
+ else:
+ util.warn(
+ "Dialect %s does not support deleted rowcount "
+ "- versioning cannot be verified."
+ % connection.dialect.dialect_description
+ )
+ connection.execute(
+ statement, del_objects, execution_options=execution_options
+ )
+ else:
+ c = connection.execute(
+ statement, del_objects, execution_options=execution_options
+ )
+
+ if not need_version_id:
+ only_warn = True
+
+ rows_matched = c.rowcount
+
+ if (
+ base_mapper.confirm_deleted_rows
+ and rows_matched > -1
+ and expected != rows_matched
+ and (
+ connection.dialect.supports_sane_multi_rowcount
+ or len(del_objects) == 1
+ )
+ ):
+ # TODO: why does this "only warn" if versioning is turned off,
+ # whereas the UPDATE raises?
+ if only_warn:
+ util.warn(
+ "DELETE statement on table '%s' expected to "
+ "delete %d row(s); %d were matched. Please set "
+ "confirm_deleted_rows=False within the mapper "
+ "configuration to prevent this warning."
+ % (table.description, expected, rows_matched)
+ )
+ else:
+ raise orm_exc.StaleDataError(
+ "DELETE statement on table '%s' expected to "
+ "delete %d row(s); %d were matched. Please set "
+ "confirm_deleted_rows=False within the mapper "
+ "configuration to prevent this warning."
+ % (table.description, expected, rows_matched)
+ )
+
+
+def _finalize_insert_update_commands(base_mapper, uowtransaction, states):
+ """finalize state on states that have been inserted or updated,
+ including calling after_insert/after_update events.
+
+ """
+ for state, state_dict, mapper, connection, has_identity in states:
+ if mapper._readonly_props:
+ readonly = state.unmodified_intersection(
+ [
+ p.key
+ for p in mapper._readonly_props
+ if (
+ p.expire_on_flush
+ and (not p.deferred or p.key in state.dict)
+ )
+ or (
+ not p.expire_on_flush
+ and not p.deferred
+ and p.key not in state.dict
+ )
+ ]
+ )
+ if readonly:
+ state._expire_attributes(state.dict, readonly)
+
+ # if eager_defaults option is enabled, load
+ # all expired cols. Else if we have a version_id_col, make sure
+ # it isn't expired.
+ toload_now = []
+
+ # this is specifically to emit a second SELECT for eager_defaults,
+ # so only if it's set to True, not "auto"
+ if base_mapper.eager_defaults is True:
+ toload_now.extend(
+ state._unloaded_non_object.intersection(
+ mapper._server_default_plus_onupdate_propkeys
+ )
+ )
+
+ if (
+ mapper.version_id_col is not None
+ and mapper.version_id_generator is False
+ ):
+ if mapper._version_id_prop.key in state.unloaded:
+ toload_now.extend([mapper._version_id_prop.key])
+
+ if toload_now:
+ state.key = base_mapper._identity_key_from_state(state)
+ stmt = future.select(mapper).set_label_style(
+ LABEL_STYLE_TABLENAME_PLUS_COL
+ )
+ loading.load_on_ident(
+ uowtransaction.session,
+ stmt,
+ state.key,
+ refresh_state=state,
+ only_load_props=toload_now,
+ )
+
+ # call after_XXX extensions
+ if not has_identity:
+ mapper.dispatch.after_insert(mapper, connection, state)
+ else:
+ mapper.dispatch.after_update(mapper, connection, state)
+
+ if (
+ mapper.version_id_generator is False
+ and mapper.version_id_col is not None
+ ):
+ if state_dict[mapper._version_id_prop.key] is None:
+ raise orm_exc.FlushError(
+ "Instance does not contain a non-NULL version value"
+ )
+
+
+def _postfetch_post_update(
+ mapper, uowtransaction, table, state, dict_, result, params
+):
+ needs_version_id = (
+ mapper.version_id_col is not None
+ and mapper.version_id_col in mapper._cols_by_table[table]
+ )
+
+ if not uowtransaction.is_deleted(state):
+ # post updating after a regular INSERT or UPDATE, do a full postfetch
+ prefetch_cols = result.context.compiled.prefetch
+ postfetch_cols = result.context.compiled.postfetch
+ elif needs_version_id:
+ # post updating before a DELETE with a version_id_col, need to
+ # postfetch just version_id_col
+ prefetch_cols = postfetch_cols = ()
+ else:
+ # post updating before a DELETE without a version_id_col,
+ # don't need to postfetch
+ return
+
+ if needs_version_id:
+ prefetch_cols = list(prefetch_cols) + [mapper.version_id_col]
+
+ refresh_flush = bool(mapper.class_manager.dispatch.refresh_flush)
+ if refresh_flush:
+ load_evt_attrs = []
+
+ for c in prefetch_cols:
+ if c.key in params and c in mapper._columntoproperty:
+ dict_[mapper._columntoproperty[c].key] = params[c.key]
+ if refresh_flush:
+ load_evt_attrs.append(mapper._columntoproperty[c].key)
+
+ if refresh_flush and load_evt_attrs:
+ mapper.class_manager.dispatch.refresh_flush(
+ state, uowtransaction, load_evt_attrs
+ )
+
+ if postfetch_cols:
+ state._expire_attributes(
+ state.dict,
+ [
+ mapper._columntoproperty[c].key
+ for c in postfetch_cols
+ if c in mapper._columntoproperty
+ ],
+ )
+
+
+def _postfetch(
+ mapper,
+ uowtransaction,
+ table,
+ state,
+ dict_,
+ result,
+ params,
+ value_params,
+ isupdate,
+ returned_defaults,
+):
+ """Expire attributes in need of newly persisted database state,
+ after an INSERT or UPDATE statement has proceeded for that
+ state."""
+
+ prefetch_cols = result.context.compiled.prefetch
+ postfetch_cols = result.context.compiled.postfetch
+ returning_cols = result.context.compiled.effective_returning
+
+ if (
+ mapper.version_id_col is not None
+ and mapper.version_id_col in mapper._cols_by_table[table]
+ ):
+ prefetch_cols = list(prefetch_cols) + [mapper.version_id_col]
+
+ refresh_flush = bool(mapper.class_manager.dispatch.refresh_flush)
+ if refresh_flush:
+ load_evt_attrs = []
+
+ if returning_cols:
+ row = returned_defaults
+ if row is not None:
+ for row_value, col in zip(row, returning_cols):
+ # pk cols returned from insert are handled
+ # distinctly, don't step on the values here
+ if col.primary_key and result.context.isinsert:
+ continue
+
+ # note that columns can be in the "return defaults" that are
+ # not mapped to this mapper, typically because they are
+ # "excluded", which can be specified directly or also occurs
+ # when using declarative w/ single table inheritance
+ prop = mapper._columntoproperty.get(col)
+ if prop:
+ dict_[prop.key] = row_value
+ if refresh_flush:
+ load_evt_attrs.append(prop.key)
+
+ for c in prefetch_cols:
+ if c.key in params and c in mapper._columntoproperty:
+ pkey = mapper._columntoproperty[c].key
+
+ # set prefetched value in dict and also pop from committed_state,
+ # since this is new database state that replaces whatever might
+ # have previously been fetched (see #10800). this is essentially a
+ # shorthand version of set_committed_value(), which could also be
+ # used here directly (with more overhead)
+ dict_[pkey] = params[c.key]
+ state.committed_state.pop(pkey, None)
+
+ if refresh_flush:
+ load_evt_attrs.append(pkey)
+
+ if refresh_flush and load_evt_attrs:
+ mapper.class_manager.dispatch.refresh_flush(
+ state, uowtransaction, load_evt_attrs
+ )
+
+ if isupdate and value_params:
+ # explicitly suit the use case specified by
+ # [ticket:3801], PK SQL expressions for UPDATE on non-RETURNING
+ # database which are set to themselves in order to do a version bump.
+ postfetch_cols.extend(
+ [
+ col
+ for col in value_params
+ if col.primary_key and col not in returning_cols
+ ]
+ )
+
+ if postfetch_cols:
+ state._expire_attributes(
+ state.dict,
+ [
+ mapper._columntoproperty[c].key
+ for c in postfetch_cols
+ if c in mapper._columntoproperty
+ ],
+ )
+
+ # synchronize newly inserted ids from one table to the next
+ # TODO: this still goes a little too often. would be nice to
+ # have definitive list of "columns that changed" here
+ for m, equated_pairs in mapper._table_to_equated[table]:
+ sync.populate(
+ state,
+ m,
+ state,
+ m,
+ equated_pairs,
+ uowtransaction,
+ mapper.passive_updates,
+ )
+
+
+def _postfetch_bulk_save(mapper, dict_, table):
+ for m, equated_pairs in mapper._table_to_equated[table]:
+ sync.bulk_populate_inherit_keys(dict_, m, equated_pairs)
+
+
+def _connections_for_states(base_mapper, uowtransaction, states):
+ """Return an iterator of (state, state.dict, mapper, connection).
+
+ The states are sorted according to _sort_states, then paired
+ with the connection they should be using for the given
+ unit of work transaction.
+
+ """
+ # if session has a connection callable,
+ # organize individual states with the connection
+ # to use for update
+ if uowtransaction.session.connection_callable:
+ connection_callable = uowtransaction.session.connection_callable
+ else:
+ connection = uowtransaction.transaction.connection(base_mapper)
+ connection_callable = None
+
+ for state in _sort_states(base_mapper, states):
+ if connection_callable:
+ connection = connection_callable(base_mapper, state.obj())
+
+ mapper = state.manager.mapper
+
+ yield state, state.dict, mapper, connection
+
+
+def _sort_states(mapper, states):
+ pending = set(states)
+ persistent = {s for s in pending if s.key is not None}
+ pending.difference_update(persistent)
+
+ try:
+ persistent_sorted = sorted(
+ persistent, key=mapper._persistent_sortkey_fn
+ )
+ except TypeError as err:
+ raise sa_exc.InvalidRequestError(
+ "Could not sort objects by primary key; primary key "
+ "values must be sortable in Python (was: %s)" % err
+ ) from err
+ return (
+ sorted(pending, key=operator.attrgetter("insert_order"))
+ + persistent_sorted
+ )
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/properties.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/properties.py
new file mode 100644
index 0000000..adee44a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/properties.py
@@ -0,0 +1,886 @@
+# orm/properties.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""MapperProperty implementations.
+
+This is a private module which defines the behavior of individual ORM-
+mapped attributes.
+
+"""
+
+from __future__ import annotations
+
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import attributes
+from . import strategy_options
+from .base import _DeclarativeMapped
+from .base import class_mapper
+from .descriptor_props import CompositeProperty
+from .descriptor_props import ConcreteInheritedProperty
+from .descriptor_props import SynonymProperty
+from .interfaces import _AttributeOptions
+from .interfaces import _DEFAULT_ATTRIBUTE_OPTIONS
+from .interfaces import _IntrospectsAnnotations
+from .interfaces import _MapsColumns
+from .interfaces import MapperProperty
+from .interfaces import PropComparator
+from .interfaces import StrategizedProperty
+from .relationships import RelationshipProperty
+from .util import de_stringify_annotation
+from .util import de_stringify_union_elements
+from .. import exc as sa_exc
+from .. import ForeignKey
+from .. import log
+from .. import util
+from ..sql import coercions
+from ..sql import roles
+from ..sql.base import _NoArg
+from ..sql.schema import Column
+from ..sql.schema import SchemaConst
+from ..sql.type_api import TypeEngine
+from ..util.typing import de_optionalize_union_types
+from ..util.typing import is_fwd_ref
+from ..util.typing import is_optional_union
+from ..util.typing import is_pep593
+from ..util.typing import is_pep695
+from ..util.typing import is_union
+from ..util.typing import Self
+from ..util.typing import typing_get_args
+
+if TYPE_CHECKING:
+ from ._typing import _IdentityKeyType
+ from ._typing import _InstanceDict
+ from ._typing import _ORMColumnExprArgument
+ from ._typing import _RegistryType
+ from .base import Mapped
+ from .decl_base import _ClassScanMapperConfig
+ from .mapper import Mapper
+ from .session import Session
+ from .state import _InstallLoaderCallableProto
+ from .state import InstanceState
+ from ..sql._typing import _InfoType
+ from ..sql.elements import ColumnElement
+ from ..sql.elements import NamedColumn
+ from ..sql.operators import OperatorType
+ from ..util.typing import _AnnotationScanType
+ from ..util.typing import RODescriptorReference
+
+_T = TypeVar("_T", bound=Any)
+_PT = TypeVar("_PT", bound=Any)
+_NC = TypeVar("_NC", bound="NamedColumn[Any]")
+
+__all__ = [
+ "ColumnProperty",
+ "CompositeProperty",
+ "ConcreteInheritedProperty",
+ "RelationshipProperty",
+ "SynonymProperty",
+]
+
+
+@log.class_logger
+class ColumnProperty(
+ _MapsColumns[_T],
+ StrategizedProperty[_T],
+ _IntrospectsAnnotations,
+ log.Identified,
+):
+ """Describes an object attribute that corresponds to a table column
+ or other column expression.
+
+ Public constructor is the :func:`_orm.column_property` function.
+
+ """
+
+ strategy_wildcard_key = strategy_options._COLUMN_TOKEN
+ inherit_cache = True
+ """:meta private:"""
+
+ _links_to_entity = False
+
+ columns: List[NamedColumn[Any]]
+
+ _is_polymorphic_discriminator: bool
+
+ _mapped_by_synonym: Optional[str]
+
+ comparator_factory: Type[PropComparator[_T]]
+
+ __slots__ = (
+ "columns",
+ "group",
+ "deferred",
+ "instrument",
+ "comparator_factory",
+ "active_history",
+ "expire_on_flush",
+ "_creation_order",
+ "_is_polymorphic_discriminator",
+ "_mapped_by_synonym",
+ "_deferred_column_loader",
+ "_raise_column_loader",
+ "_renders_in_subqueries",
+ "raiseload",
+ )
+
+ def __init__(
+ self,
+ column: _ORMColumnExprArgument[_T],
+ *additional_columns: _ORMColumnExprArgument[Any],
+ attribute_options: Optional[_AttributeOptions] = None,
+ group: Optional[str] = None,
+ deferred: bool = False,
+ raiseload: bool = False,
+ comparator_factory: Optional[Type[PropComparator[_T]]] = None,
+ active_history: bool = False,
+ expire_on_flush: bool = True,
+ info: Optional[_InfoType] = None,
+ doc: Optional[str] = None,
+ _instrument: bool = True,
+ _assume_readonly_dc_attributes: bool = False,
+ ):
+ super().__init__(
+ attribute_options=attribute_options,
+ _assume_readonly_dc_attributes=_assume_readonly_dc_attributes,
+ )
+ columns = (column,) + additional_columns
+ self.columns = [
+ coercions.expect(roles.LabeledColumnExprRole, c) for c in columns
+ ]
+ self.group = group
+ self.deferred = deferred
+ self.raiseload = raiseload
+ self.instrument = _instrument
+ self.comparator_factory = (
+ comparator_factory
+ if comparator_factory is not None
+ else self.__class__.Comparator
+ )
+ self.active_history = active_history
+ self.expire_on_flush = expire_on_flush
+
+ if info is not None:
+ self.info.update(info)
+
+ if doc is not None:
+ self.doc = doc
+ else:
+ for col in reversed(self.columns):
+ doc = getattr(col, "doc", None)
+ if doc is not None:
+ self.doc = doc
+ break
+ else:
+ self.doc = None
+
+ util.set_creation_order(self)
+
+ self.strategy_key = (
+ ("deferred", self.deferred),
+ ("instrument", self.instrument),
+ )
+ if self.raiseload:
+ self.strategy_key += (("raiseload", True),)
+
+ def declarative_scan(
+ self,
+ decl_scan: _ClassScanMapperConfig,
+ registry: _RegistryType,
+ cls: Type[Any],
+ originating_module: Optional[str],
+ key: str,
+ mapped_container: Optional[Type[Mapped[Any]]],
+ annotation: Optional[_AnnotationScanType],
+ extracted_mapped_annotation: Optional[_AnnotationScanType],
+ is_dataclass_field: bool,
+ ) -> None:
+ column = self.columns[0]
+ if column.key is None:
+ column.key = key
+ if column.name is None:
+ column.name = key
+
+ @property
+ def mapper_property_to_assign(self) -> Optional[MapperProperty[_T]]:
+ return self
+
+ @property
+ def columns_to_assign(self) -> List[Tuple[Column[Any], int]]:
+ # mypy doesn't care about the isinstance here
+ return [
+ (c, 0) # type: ignore
+ for c in self.columns
+ if isinstance(c, Column) and c.table is None
+ ]
+
+ def _memoized_attr__renders_in_subqueries(self) -> bool:
+ if ("query_expression", True) in self.strategy_key:
+ return self.strategy._have_default_expression # type: ignore
+
+ return ("deferred", True) not in self.strategy_key or (
+ self not in self.parent._readonly_props # type: ignore
+ )
+
+ @util.preload_module("sqlalchemy.orm.state", "sqlalchemy.orm.strategies")
+ def _memoized_attr__deferred_column_loader(
+ self,
+ ) -> _InstallLoaderCallableProto[Any]:
+ state = util.preloaded.orm_state
+ strategies = util.preloaded.orm_strategies
+ return state.InstanceState._instance_level_callable_processor(
+ self.parent.class_manager,
+ strategies.LoadDeferredColumns(self.key),
+ self.key,
+ )
+
+ @util.preload_module("sqlalchemy.orm.state", "sqlalchemy.orm.strategies")
+ def _memoized_attr__raise_column_loader(
+ self,
+ ) -> _InstallLoaderCallableProto[Any]:
+ state = util.preloaded.orm_state
+ strategies = util.preloaded.orm_strategies
+ return state.InstanceState._instance_level_callable_processor(
+ self.parent.class_manager,
+ strategies.LoadDeferredColumns(self.key, True),
+ self.key,
+ )
+
+ def __clause_element__(self) -> roles.ColumnsClauseRole:
+ """Allow the ColumnProperty to work in expression before it is turned
+ into an instrumented attribute.
+ """
+
+ return self.expression
+
+ @property
+ def expression(self) -> roles.ColumnsClauseRole:
+ """Return the primary column or expression for this ColumnProperty.
+
+ E.g.::
+
+
+ class File(Base):
+ # ...
+
+ name = Column(String(64))
+ extension = Column(String(8))
+ filename = column_property(name + '.' + extension)
+ path = column_property('C:/' + filename.expression)
+
+ .. seealso::
+
+ :ref:`mapper_column_property_sql_expressions_composed`
+
+ """
+ return self.columns[0]
+
+ def instrument_class(self, mapper: Mapper[Any]) -> None:
+ if not self.instrument:
+ return
+
+ attributes.register_descriptor(
+ mapper.class_,
+ self.key,
+ comparator=self.comparator_factory(self, mapper),
+ parententity=mapper,
+ doc=self.doc,
+ )
+
+ def do_init(self) -> None:
+ super().do_init()
+
+ if len(self.columns) > 1 and set(self.parent.primary_key).issuperset(
+ self.columns
+ ):
+ util.warn(
+ (
+ "On mapper %s, primary key column '%s' is being combined "
+ "with distinct primary key column '%s' in attribute '%s'. "
+ "Use explicit properties to give each column its own "
+ "mapped attribute name."
+ )
+ % (self.parent, self.columns[1], self.columns[0], self.key)
+ )
+
+ def copy(self) -> ColumnProperty[_T]:
+ return ColumnProperty(
+ *self.columns,
+ deferred=self.deferred,
+ group=self.group,
+ active_history=self.active_history,
+ )
+
+ def merge(
+ self,
+ session: Session,
+ source_state: InstanceState[Any],
+ source_dict: _InstanceDict,
+ dest_state: InstanceState[Any],
+ dest_dict: _InstanceDict,
+ load: bool,
+ _recursive: Dict[Any, object],
+ _resolve_conflict_map: Dict[_IdentityKeyType[Any], object],
+ ) -> None:
+ if not self.instrument:
+ return
+ elif self.key in source_dict:
+ value = source_dict[self.key]
+
+ if not load:
+ dest_dict[self.key] = value
+ else:
+ impl = dest_state.get_impl(self.key)
+ impl.set(dest_state, dest_dict, value, None)
+ elif dest_state.has_identity and self.key not in dest_dict:
+ dest_state._expire_attributes(
+ dest_dict, [self.key], no_loader=True
+ )
+
+ class Comparator(util.MemoizedSlots, PropComparator[_PT]):
+ """Produce boolean, comparison, and other operators for
+ :class:`.ColumnProperty` attributes.
+
+ See the documentation for :class:`.PropComparator` for a brief
+ overview.
+
+ .. seealso::
+
+ :class:`.PropComparator`
+
+ :class:`.ColumnOperators`
+
+ :ref:`types_operators`
+
+ :attr:`.TypeEngine.comparator_factory`
+
+ """
+
+ if not TYPE_CHECKING:
+ # prevent pylance from being clever about slots
+ __slots__ = "__clause_element__", "info", "expressions"
+
+ prop: RODescriptorReference[ColumnProperty[_PT]]
+
+ expressions: Sequence[NamedColumn[Any]]
+ """The full sequence of columns referenced by this
+ attribute, adjusted for any aliasing in progress.
+
+ .. versionadded:: 1.3.17
+
+ .. seealso::
+
+ :ref:`maptojoin` - usage example
+ """
+
+ def _orm_annotate_column(self, column: _NC) -> _NC:
+ """annotate and possibly adapt a column to be returned
+ as the mapped-attribute exposed version of the column.
+
+ The column in this context needs to act as much like the
+ column in an ORM mapped context as possible, so includes
+ annotations to give hints to various ORM functions as to
+ the source entity of this column. It also adapts it
+ to the mapper's with_polymorphic selectable if one is
+ present.
+
+ """
+
+ pe = self._parententity
+ annotations: Dict[str, Any] = {
+ "entity_namespace": pe,
+ "parententity": pe,
+ "parentmapper": pe,
+ "proxy_key": self.prop.key,
+ }
+
+ col = column
+
+ # for a mapper with polymorphic_on and an adapter, return
+ # the column against the polymorphic selectable.
+ # see also orm.util._orm_downgrade_polymorphic_columns
+ # for the reverse operation.
+ if self._parentmapper._polymorphic_adapter:
+ mapper_local_col = col
+ col = self._parentmapper._polymorphic_adapter.traverse(col)
+
+ # this is a clue to the ORM Query etc. that this column
+ # was adapted to the mapper's polymorphic_adapter. the
+ # ORM uses this hint to know which column its adapting.
+ annotations["adapt_column"] = mapper_local_col
+
+ return col._annotate(annotations)._set_propagate_attrs(
+ {"compile_state_plugin": "orm", "plugin_subject": pe}
+ )
+
+ if TYPE_CHECKING:
+
+ def __clause_element__(self) -> NamedColumn[_PT]: ...
+
+ def _memoized_method___clause_element__(
+ self,
+ ) -> NamedColumn[_PT]:
+ if self.adapter:
+ return self.adapter(self.prop.columns[0], self.prop.key)
+ else:
+ return self._orm_annotate_column(self.prop.columns[0])
+
+ def _memoized_attr_info(self) -> _InfoType:
+ """The .info dictionary for this attribute."""
+
+ ce = self.__clause_element__()
+ try:
+ return ce.info # type: ignore
+ except AttributeError:
+ return self.prop.info
+
+ def _memoized_attr_expressions(self) -> Sequence[NamedColumn[Any]]:
+ """The full sequence of columns referenced by this
+ attribute, adjusted for any aliasing in progress.
+
+ .. versionadded:: 1.3.17
+
+ """
+ if self.adapter:
+ return [
+ self.adapter(col, self.prop.key)
+ for col in self.prop.columns
+ ]
+ else:
+ return [
+ self._orm_annotate_column(col) for col in self.prop.columns
+ ]
+
+ def _fallback_getattr(self, key: str) -> Any:
+ """proxy attribute access down to the mapped column.
+
+ this allows user-defined comparison methods to be accessed.
+ """
+ return getattr(self.__clause_element__(), key)
+
+ def operate(
+ self, op: OperatorType, *other: Any, **kwargs: Any
+ ) -> ColumnElement[Any]:
+ return op(self.__clause_element__(), *other, **kwargs) # type: ignore[no-any-return] # noqa: E501
+
+ def reverse_operate(
+ self, op: OperatorType, other: Any, **kwargs: Any
+ ) -> ColumnElement[Any]:
+ col = self.__clause_element__()
+ return op(col._bind_param(op, other), col, **kwargs) # type: ignore[no-any-return] # noqa: E501
+
+ def __str__(self) -> str:
+ if not self.parent or not self.key:
+ return object.__repr__(self)
+ return str(self.parent.class_.__name__) + "." + self.key
+
+
+class MappedSQLExpression(ColumnProperty[_T], _DeclarativeMapped[_T]):
+ """Declarative front-end for the :class:`.ColumnProperty` class.
+
+ Public constructor is the :func:`_orm.column_property` function.
+
+ .. versionchanged:: 2.0 Added :class:`_orm.MappedSQLExpression` as
+ a Declarative compatible subclass for :class:`_orm.ColumnProperty`.
+
+ .. seealso::
+
+ :class:`.MappedColumn`
+
+ """
+
+ inherit_cache = True
+ """:meta private:"""
+
+
+class MappedColumn(
+ _IntrospectsAnnotations,
+ _MapsColumns[_T],
+ _DeclarativeMapped[_T],
+):
+ """Maps a single :class:`_schema.Column` on a class.
+
+ :class:`_orm.MappedColumn` is a specialization of the
+ :class:`_orm.ColumnProperty` class and is oriented towards declarative
+ configuration.
+
+ To construct :class:`_orm.MappedColumn` objects, use the
+ :func:`_orm.mapped_column` constructor function.
+
+ .. versionadded:: 2.0
+
+
+ """
+
+ __slots__ = (
+ "column",
+ "_creation_order",
+ "_sort_order",
+ "foreign_keys",
+ "_has_nullable",
+ "_has_insert_default",
+ "deferred",
+ "deferred_group",
+ "deferred_raiseload",
+ "active_history",
+ "_attribute_options",
+ "_has_dataclass_arguments",
+ "_use_existing_column",
+ )
+
+ deferred: Union[_NoArg, bool]
+ deferred_raiseload: bool
+ deferred_group: Optional[str]
+
+ column: Column[_T]
+ foreign_keys: Optional[Set[ForeignKey]]
+ _attribute_options: _AttributeOptions
+
+ def __init__(self, *arg: Any, **kw: Any):
+ self._attribute_options = attr_opts = kw.pop(
+ "attribute_options", _DEFAULT_ATTRIBUTE_OPTIONS
+ )
+
+ self._use_existing_column = kw.pop("use_existing_column", False)
+
+ self._has_dataclass_arguments = (
+ attr_opts is not None
+ and attr_opts != _DEFAULT_ATTRIBUTE_OPTIONS
+ and any(
+ attr_opts[i] is not _NoArg.NO_ARG
+ for i, attr in enumerate(attr_opts._fields)
+ if attr != "dataclasses_default"
+ )
+ )
+
+ insert_default = kw.pop("insert_default", _NoArg.NO_ARG)
+ self._has_insert_default = insert_default is not _NoArg.NO_ARG
+
+ if self._has_insert_default:
+ kw["default"] = insert_default
+ elif attr_opts.dataclasses_default is not _NoArg.NO_ARG:
+ kw["default"] = attr_opts.dataclasses_default
+
+ self.deferred_group = kw.pop("deferred_group", None)
+ self.deferred_raiseload = kw.pop("deferred_raiseload", None)
+ self.deferred = kw.pop("deferred", _NoArg.NO_ARG)
+ self.active_history = kw.pop("active_history", False)
+
+ self._sort_order = kw.pop("sort_order", _NoArg.NO_ARG)
+ self.column = cast("Column[_T]", Column(*arg, **kw))
+ self.foreign_keys = self.column.foreign_keys
+ self._has_nullable = "nullable" in kw and kw.get("nullable") not in (
+ None,
+ SchemaConst.NULL_UNSPECIFIED,
+ )
+ util.set_creation_order(self)
+
+ def _copy(self, **kw: Any) -> Self:
+ new = self.__class__.__new__(self.__class__)
+ new.column = self.column._copy(**kw)
+ new.deferred = self.deferred
+ new.deferred_group = self.deferred_group
+ new.deferred_raiseload = self.deferred_raiseload
+ new.foreign_keys = new.column.foreign_keys
+ new.active_history = self.active_history
+ new._has_nullable = self._has_nullable
+ new._attribute_options = self._attribute_options
+ new._has_insert_default = self._has_insert_default
+ new._has_dataclass_arguments = self._has_dataclass_arguments
+ new._use_existing_column = self._use_existing_column
+ new._sort_order = self._sort_order
+ util.set_creation_order(new)
+ return new
+
+ @property
+ def name(self) -> str:
+ return self.column.name
+
+ @property
+ def mapper_property_to_assign(self) -> Optional[MapperProperty[_T]]:
+ effective_deferred = self.deferred
+ if effective_deferred is _NoArg.NO_ARG:
+ effective_deferred = bool(
+ self.deferred_group or self.deferred_raiseload
+ )
+
+ if effective_deferred or self.active_history:
+ return ColumnProperty(
+ self.column,
+ deferred=effective_deferred,
+ group=self.deferred_group,
+ raiseload=self.deferred_raiseload,
+ attribute_options=self._attribute_options,
+ active_history=self.active_history,
+ )
+ else:
+ return None
+
+ @property
+ def columns_to_assign(self) -> List[Tuple[Column[Any], int]]:
+ return [
+ (
+ self.column,
+ (
+ self._sort_order
+ if self._sort_order is not _NoArg.NO_ARG
+ else 0
+ ),
+ )
+ ]
+
+ def __clause_element__(self) -> Column[_T]:
+ return self.column
+
+ def operate(
+ self, op: OperatorType, *other: Any, **kwargs: Any
+ ) -> ColumnElement[Any]:
+ return op(self.__clause_element__(), *other, **kwargs) # type: ignore[no-any-return] # noqa: E501
+
+ def reverse_operate(
+ self, op: OperatorType, other: Any, **kwargs: Any
+ ) -> ColumnElement[Any]:
+ col = self.__clause_element__()
+ return op(col._bind_param(op, other), col, **kwargs) # type: ignore[no-any-return] # noqa: E501
+
+ def found_in_pep593_annotated(self) -> Any:
+ # return a blank mapped_column(). This mapped_column()'s
+ # Column will be merged into it in _init_column_for_annotation().
+ return MappedColumn()
+
+ def declarative_scan(
+ self,
+ decl_scan: _ClassScanMapperConfig,
+ registry: _RegistryType,
+ cls: Type[Any],
+ originating_module: Optional[str],
+ key: str,
+ mapped_container: Optional[Type[Mapped[Any]]],
+ annotation: Optional[_AnnotationScanType],
+ extracted_mapped_annotation: Optional[_AnnotationScanType],
+ is_dataclass_field: bool,
+ ) -> None:
+ column = self.column
+
+ if (
+ self._use_existing_column
+ and decl_scan.inherits
+ and decl_scan.single
+ ):
+ if decl_scan.is_deferred:
+ raise sa_exc.ArgumentError(
+ "Can't use use_existing_column with deferred mappers"
+ )
+ supercls_mapper = class_mapper(decl_scan.inherits, False)
+
+ colname = column.name if column.name is not None else key
+ column = self.column = supercls_mapper.local_table.c.get( # type: ignore # noqa: E501
+ colname, column
+ )
+
+ if column.key is None:
+ column.key = key
+ if column.name is None:
+ column.name = key
+
+ sqltype = column.type
+
+ if extracted_mapped_annotation is None:
+ if sqltype._isnull and not self.column.foreign_keys:
+ self._raise_for_required(key, cls)
+ else:
+ return
+
+ self._init_column_for_annotation(
+ cls,
+ registry,
+ extracted_mapped_annotation,
+ originating_module,
+ )
+
+ @util.preload_module("sqlalchemy.orm.decl_base")
+ def declarative_scan_for_composite(
+ self,
+ registry: _RegistryType,
+ cls: Type[Any],
+ originating_module: Optional[str],
+ key: str,
+ param_name: str,
+ param_annotation: _AnnotationScanType,
+ ) -> None:
+ decl_base = util.preloaded.orm_decl_base
+ decl_base._undefer_column_name(param_name, self.column)
+ self._init_column_for_annotation(
+ cls, registry, param_annotation, originating_module
+ )
+
+ def _init_column_for_annotation(
+ self,
+ cls: Type[Any],
+ registry: _RegistryType,
+ argument: _AnnotationScanType,
+ originating_module: Optional[str],
+ ) -> None:
+ sqltype = self.column.type
+
+ if isinstance(argument, str) or is_fwd_ref(
+ argument, check_generic=True
+ ):
+ assert originating_module is not None
+ argument = de_stringify_annotation(
+ cls, argument, originating_module, include_generic=True
+ )
+
+ if is_union(argument):
+ assert originating_module is not None
+ argument = de_stringify_union_elements(
+ cls, argument, originating_module
+ )
+
+ nullable = is_optional_union(argument)
+
+ if not self._has_nullable:
+ self.column.nullable = nullable
+
+ our_type = de_optionalize_union_types(argument)
+
+ use_args_from = None
+
+ our_original_type = our_type
+
+ if is_pep695(our_type):
+ our_type = our_type.__value__
+
+ if is_pep593(our_type):
+ our_type_is_pep593 = True
+
+ pep_593_components = typing_get_args(our_type)
+ raw_pep_593_type = pep_593_components[0]
+ if is_optional_union(raw_pep_593_type):
+ raw_pep_593_type = de_optionalize_union_types(raw_pep_593_type)
+
+ nullable = True
+ if not self._has_nullable:
+ self.column.nullable = nullable
+ for elem in pep_593_components[1:]:
+ if isinstance(elem, MappedColumn):
+ use_args_from = elem
+ break
+ else:
+ our_type_is_pep593 = False
+ raw_pep_593_type = None
+
+ if use_args_from is not None:
+ if (
+ not self._has_insert_default
+ and use_args_from.column.default is not None
+ ):
+ self.column.default = None
+
+ use_args_from.column._merge(self.column)
+ sqltype = self.column.type
+
+ if (
+ use_args_from.deferred is not _NoArg.NO_ARG
+ and self.deferred is _NoArg.NO_ARG
+ ):
+ self.deferred = use_args_from.deferred
+
+ if (
+ use_args_from.deferred_group is not None
+ and self.deferred_group is None
+ ):
+ self.deferred_group = use_args_from.deferred_group
+
+ if (
+ use_args_from.deferred_raiseload is not None
+ and self.deferred_raiseload is None
+ ):
+ self.deferred_raiseload = use_args_from.deferred_raiseload
+
+ if (
+ use_args_from._use_existing_column
+ and not self._use_existing_column
+ ):
+ self._use_existing_column = True
+
+ if use_args_from.active_history:
+ self.active_history = use_args_from.active_history
+
+ if (
+ use_args_from._sort_order is not None
+ and self._sort_order is _NoArg.NO_ARG
+ ):
+ self._sort_order = use_args_from._sort_order
+
+ if (
+ use_args_from.column.key is not None
+ or use_args_from.column.name is not None
+ ):
+ util.warn_deprecated(
+ "Can't use the 'key' or 'name' arguments in "
+ "Annotated with mapped_column(); this will be ignored",
+ "2.0.22",
+ )
+
+ if use_args_from._has_dataclass_arguments:
+ for idx, arg in enumerate(
+ use_args_from._attribute_options._fields
+ ):
+ if (
+ use_args_from._attribute_options[idx]
+ is not _NoArg.NO_ARG
+ ):
+ arg = arg.replace("dataclasses_", "")
+ util.warn_deprecated(
+ f"Argument '{arg}' is a dataclass argument and "
+ "cannot be specified within a mapped_column() "
+ "bundled inside of an Annotated object",
+ "2.0.22",
+ )
+
+ if sqltype._isnull and not self.column.foreign_keys:
+ new_sqltype = None
+
+ if our_type_is_pep593:
+ checks = [our_original_type, raw_pep_593_type]
+ else:
+ checks = [our_original_type]
+
+ for check_type in checks:
+ new_sqltype = registry._resolve_type(check_type)
+ if new_sqltype is not None:
+ break
+ else:
+ if isinstance(our_type, TypeEngine) or (
+ isinstance(our_type, type)
+ and issubclass(our_type, TypeEngine)
+ ):
+ raise sa_exc.ArgumentError(
+ f"The type provided inside the {self.column.key!r} "
+ "attribute Mapped annotation is the SQLAlchemy type "
+ f"{our_type}. Expected a Python type instead"
+ )
+ else:
+ raise sa_exc.ArgumentError(
+ "Could not locate SQLAlchemy Core type for Python "
+ f"type {our_type} inside the {self.column.key!r} "
+ "attribute Mapped annotation"
+ )
+
+ self.column._set_type(new_sqltype)
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py
new file mode 100644
index 0000000..1dfc9cb
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py
@@ -0,0 +1,3394 @@
+# orm/query.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""The Query class and support.
+
+Defines the :class:`_query.Query` class, the central
+construct used by the ORM to construct database queries.
+
+The :class:`_query.Query` class should not be confused with the
+:class:`_expression.Select` class, which defines database
+SELECT operations at the SQL (non-ORM) level. ``Query`` differs from
+``Select`` in that it returns ORM-mapped objects and interacts with an
+ORM session, whereas the ``Select`` construct interacts directly with the
+database to return iterable result sets.
+
+"""
+from __future__ import annotations
+
+import collections.abc as collections_abc
+import operator
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from . import attributes
+from . import interfaces
+from . import loading
+from . import util as orm_util
+from ._typing import _O
+from .base import _assertions
+from .context import _column_descriptions
+from .context import _determine_last_joined_entity
+from .context import _legacy_filter_by_entity_zero
+from .context import FromStatement
+from .context import ORMCompileState
+from .context import QueryContext
+from .interfaces import ORMColumnDescription
+from .interfaces import ORMColumnsClauseRole
+from .util import AliasedClass
+from .util import object_mapper
+from .util import with_parent
+from .. import exc as sa_exc
+from .. import inspect
+from .. import inspection
+from .. import log
+from .. import sql
+from .. import util
+from ..engine import Result
+from ..engine import Row
+from ..event import dispatcher
+from ..event import EventTarget
+from ..sql import coercions
+from ..sql import expression
+from ..sql import roles
+from ..sql import Select
+from ..sql import util as sql_util
+from ..sql import visitors
+from ..sql._typing import _FromClauseArgument
+from ..sql._typing import _TP
+from ..sql.annotation import SupportsCloneAnnotations
+from ..sql.base import _entity_namespace_key
+from ..sql.base import _generative
+from ..sql.base import _NoArg
+from ..sql.base import Executable
+from ..sql.base import Generative
+from ..sql.elements import BooleanClauseList
+from ..sql.expression import Exists
+from ..sql.selectable import _MemoizedSelectEntities
+from ..sql.selectable import _SelectFromElements
+from ..sql.selectable import ForUpdateArg
+from ..sql.selectable import HasHints
+from ..sql.selectable import HasPrefixes
+from ..sql.selectable import HasSuffixes
+from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
+from ..sql.selectable import SelectLabelStyle
+from ..util.typing import Literal
+from ..util.typing import Self
+
+
+if TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _ExternalEntityType
+ from ._typing import _InternalEntityType
+ from ._typing import SynchronizeSessionArgument
+ from .mapper import Mapper
+ from .path_registry import PathRegistry
+ from .session import _PKIdentityArgument
+ from .session import Session
+ from .state import InstanceState
+ from ..engine.cursor import CursorResult
+ from ..engine.interfaces import _ImmutableExecuteOptions
+ from ..engine.interfaces import CompiledCacheType
+ from ..engine.interfaces import IsolationLevel
+ from ..engine.interfaces import SchemaTranslateMapType
+ from ..engine.result import FrozenResult
+ from ..engine.result import ScalarResult
+ from ..sql._typing import _ColumnExpressionArgument
+ from ..sql._typing import _ColumnExpressionOrStrLabelArgument
+ from ..sql._typing import _ColumnsClauseArgument
+ from ..sql._typing import _DMLColumnArgument
+ from ..sql._typing import _JoinTargetArgument
+ from ..sql._typing import _LimitOffsetType
+ from ..sql._typing import _MAYBE_ENTITY
+ from ..sql._typing import _no_kw
+ from ..sql._typing import _NOT_ENTITY
+ from ..sql._typing import _OnClauseArgument
+ from ..sql._typing import _PropagateAttrsType
+ from ..sql._typing import _T0
+ from ..sql._typing import _T1
+ from ..sql._typing import _T2
+ from ..sql._typing import _T3
+ from ..sql._typing import _T4
+ from ..sql._typing import _T5
+ from ..sql._typing import _T6
+ from ..sql._typing import _T7
+ from ..sql._typing import _TypedColumnClauseArgument as _TCCA
+ from ..sql.base import CacheableOptions
+ from ..sql.base import ExecutableOption
+ from ..sql.elements import ColumnElement
+ from ..sql.elements import Label
+ from ..sql.selectable import _ForUpdateOfArgument
+ from ..sql.selectable import _JoinTargetElement
+ from ..sql.selectable import _SetupJoinsElement
+ from ..sql.selectable import Alias
+ from ..sql.selectable import CTE
+ from ..sql.selectable import ExecutableReturnsRows
+ from ..sql.selectable import FromClause
+ from ..sql.selectable import ScalarSelect
+ from ..sql.selectable import Subquery
+
+
+__all__ = ["Query", "QueryContext"]
+
+_T = TypeVar("_T", bound=Any)
+
+
+@inspection._self_inspects
+@log.class_logger
+class Query(
+ _SelectFromElements,
+ SupportsCloneAnnotations,
+ HasPrefixes,
+ HasSuffixes,
+ HasHints,
+ EventTarget,
+ log.Identified,
+ Generative,
+ Executable,
+ Generic[_T],
+):
+ """ORM-level SQL construction object.
+
+ .. legacy:: The ORM :class:`.Query` object is a legacy construct
+ as of SQLAlchemy 2.0. See the notes at the top of
+ :ref:`query_api_toplevel` for an overview, including links to migration
+ documentation.
+
+ :class:`_query.Query` objects are normally initially generated using the
+ :meth:`~.Session.query` method of :class:`.Session`, and in
+ less common cases by instantiating the :class:`_query.Query` directly and
+ associating with a :class:`.Session` using the
+ :meth:`_query.Query.with_session`
+ method.
+
+ """
+
+ # elements that are in Core and can be cached in the same way
+ _where_criteria: Tuple[ColumnElement[Any], ...] = ()
+ _having_criteria: Tuple[ColumnElement[Any], ...] = ()
+
+ _order_by_clauses: Tuple[ColumnElement[Any], ...] = ()
+ _group_by_clauses: Tuple[ColumnElement[Any], ...] = ()
+ _limit_clause: Optional[ColumnElement[Any]] = None
+ _offset_clause: Optional[ColumnElement[Any]] = None
+
+ _distinct: bool = False
+ _distinct_on: Tuple[ColumnElement[Any], ...] = ()
+
+ _for_update_arg: Optional[ForUpdateArg] = None
+ _correlate: Tuple[FromClause, ...] = ()
+ _auto_correlate: bool = True
+ _from_obj: Tuple[FromClause, ...] = ()
+ _setup_joins: Tuple[_SetupJoinsElement, ...] = ()
+
+ _label_style: SelectLabelStyle = SelectLabelStyle.LABEL_STYLE_LEGACY_ORM
+
+ _memoized_select_entities = ()
+
+ _compile_options: Union[Type[CacheableOptions], CacheableOptions] = (
+ ORMCompileState.default_compile_options
+ )
+
+ _with_options: Tuple[ExecutableOption, ...]
+ load_options = QueryContext.default_load_options + {
+ "_legacy_uniquing": True
+ }
+
+ _params: util.immutabledict[str, Any] = util.EMPTY_DICT
+
+ # local Query builder state, not needed for
+ # compilation or execution
+ _enable_assertions = True
+
+ _statement: Optional[ExecutableReturnsRows] = None
+
+ session: Session
+
+ dispatch: dispatcher[Query[_T]]
+
+ # mirrors that of ClauseElement, used to propagate the "orm"
+ # plugin as well as the "subject" of the plugin, e.g. the mapper
+ # we are querying against.
+ @util.memoized_property
+ def _propagate_attrs(self) -> _PropagateAttrsType:
+ return util.EMPTY_DICT
+
+ def __init__(
+ self,
+ entities: Union[
+ _ColumnsClauseArgument[Any], Sequence[_ColumnsClauseArgument[Any]]
+ ],
+ session: Optional[Session] = None,
+ ):
+ """Construct a :class:`_query.Query` directly.
+
+ E.g.::
+
+ q = Query([User, Address], session=some_session)
+
+ The above is equivalent to::
+
+ q = some_session.query(User, Address)
+
+ :param entities: a sequence of entities and/or SQL expressions.
+
+ :param session: a :class:`.Session` with which the
+ :class:`_query.Query`
+ will be associated. Optional; a :class:`_query.Query`
+ can be associated
+ with a :class:`.Session` generatively via the
+ :meth:`_query.Query.with_session` method as well.
+
+ .. seealso::
+
+ :meth:`.Session.query`
+
+ :meth:`_query.Query.with_session`
+
+ """
+
+ # session is usually present. There's one case in subqueryloader
+ # where it stores a Query without a Session and also there are tests
+ # for the query(Entity).with_session(session) API which is likely in
+ # some old recipes, however these are legacy as select() can now be
+ # used.
+ self.session = session # type: ignore
+ self._set_entities(entities)
+
+ def _set_propagate_attrs(self, values: Mapping[str, Any]) -> Self:
+ self._propagate_attrs = util.immutabledict(values)
+ return self
+
+ def _set_entities(
+ self,
+ entities: Union[
+ _ColumnsClauseArgument[Any], Iterable[_ColumnsClauseArgument[Any]]
+ ],
+ ) -> None:
+ self._raw_columns = [
+ coercions.expect(
+ roles.ColumnsClauseRole,
+ ent,
+ apply_propagate_attrs=self,
+ post_inspect=True,
+ )
+ for ent in util.to_list(entities)
+ ]
+
+ def tuples(self: Query[_O]) -> Query[Tuple[_O]]:
+ """return a tuple-typed form of this :class:`.Query`.
+
+ This method invokes the :meth:`.Query.only_return_tuples`
+ method with a value of ``True``, which by itself ensures that this
+ :class:`.Query` will always return :class:`.Row` objects, even
+ if the query is made against a single entity. It then also
+ at the typing level will return a "typed" query, if possible,
+ that will type result rows as ``Tuple`` objects with typed
+ elements.
+
+ This method can be compared to the :meth:`.Result.tuples` method,
+ which returns "self", but from a typing perspective returns an object
+ that will yield typed ``Tuple`` objects for results. Typing
+ takes effect only if this :class:`.Query` object is a typed
+ query object already.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :meth:`.Result.tuples` - v2 equivalent method.
+
+ """
+ return self.only_return_tuples(True) # type: ignore
+
+ def _entity_from_pre_ent_zero(self) -> Optional[_InternalEntityType[Any]]:
+ if not self._raw_columns:
+ return None
+
+ ent = self._raw_columns[0]
+
+ if "parententity" in ent._annotations:
+ return ent._annotations["parententity"] # type: ignore
+ elif "bundle" in ent._annotations:
+ return ent._annotations["bundle"] # type: ignore
+ else:
+ # label, other SQL expression
+ for element in visitors.iterate(ent):
+ if "parententity" in element._annotations:
+ return element._annotations["parententity"] # type: ignore # noqa: E501
+ else:
+ return None
+
+ def _only_full_mapper_zero(self, methname: str) -> Mapper[Any]:
+ if (
+ len(self._raw_columns) != 1
+ or "parententity" not in self._raw_columns[0]._annotations
+ or not self._raw_columns[0].is_selectable
+ ):
+ raise sa_exc.InvalidRequestError(
+ "%s() can only be used against "
+ "a single mapped class." % methname
+ )
+
+ return self._raw_columns[0]._annotations["parententity"] # type: ignore # noqa: E501
+
+ def _set_select_from(
+ self, obj: Iterable[_FromClauseArgument], set_base_alias: bool
+ ) -> None:
+ fa = [
+ coercions.expect(
+ roles.StrictFromClauseRole,
+ elem,
+ allow_select=True,
+ apply_propagate_attrs=self,
+ )
+ for elem in obj
+ ]
+
+ self._compile_options += {"_set_base_alias": set_base_alias}
+ self._from_obj = tuple(fa)
+
+ @_generative
+ def _set_lazyload_from(self, state: InstanceState[Any]) -> Self:
+ self.load_options += {"_lazy_loaded_from": state}
+ return self
+
+ def _get_condition(self) -> None:
+ """used by legacy BakedQuery"""
+ self._no_criterion_condition("get", order_by=False, distinct=False)
+
+ def _get_existing_condition(self) -> None:
+ self._no_criterion_assertion("get", order_by=False, distinct=False)
+
+ def _no_criterion_assertion(
+ self, meth: str, order_by: bool = True, distinct: bool = True
+ ) -> None:
+ if not self._enable_assertions:
+ return
+ if (
+ self._where_criteria
+ or self._statement is not None
+ or self._from_obj
+ or self._setup_joins
+ or self._limit_clause is not None
+ or self._offset_clause is not None
+ or self._group_by_clauses
+ or (order_by and self._order_by_clauses)
+ or (distinct and self._distinct)
+ ):
+ raise sa_exc.InvalidRequestError(
+ "Query.%s() being called on a "
+ "Query with existing criterion. " % meth
+ )
+
+ def _no_criterion_condition(
+ self, meth: str, order_by: bool = True, distinct: bool = True
+ ) -> None:
+ self._no_criterion_assertion(meth, order_by, distinct)
+
+ self._from_obj = self._setup_joins = ()
+ if self._statement is not None:
+ self._compile_options += {"_statement": None}
+ self._where_criteria = ()
+ self._distinct = False
+
+ self._order_by_clauses = self._group_by_clauses = ()
+
+ def _no_clauseelement_condition(self, meth: str) -> None:
+ if not self._enable_assertions:
+ return
+ if self._order_by_clauses:
+ raise sa_exc.InvalidRequestError(
+ "Query.%s() being called on a "
+ "Query with existing criterion. " % meth
+ )
+ self._no_criterion_condition(meth)
+
+ def _no_statement_condition(self, meth: str) -> None:
+ if not self._enable_assertions:
+ return
+ if self._statement is not None:
+ raise sa_exc.InvalidRequestError(
+ (
+ "Query.%s() being called on a Query with an existing full "
+ "statement - can't apply criterion."
+ )
+ % meth
+ )
+
+ def _no_limit_offset(self, meth: str) -> None:
+ if not self._enable_assertions:
+ return
+ if self._limit_clause is not None or self._offset_clause is not None:
+ raise sa_exc.InvalidRequestError(
+ "Query.%s() being called on a Query which already has LIMIT "
+ "or OFFSET applied. Call %s() before limit() or offset() "
+ "are applied." % (meth, meth)
+ )
+
+ @property
+ def _has_row_limiting_clause(self) -> bool:
+ return (
+ self._limit_clause is not None or self._offset_clause is not None
+ )
+
+ def _get_options(
+ self,
+ populate_existing: Optional[bool] = None,
+ version_check: Optional[bool] = None,
+ only_load_props: Optional[Sequence[str]] = None,
+ refresh_state: Optional[InstanceState[Any]] = None,
+ identity_token: Optional[Any] = None,
+ ) -> Self:
+ load_options: Dict[str, Any] = {}
+ compile_options: Dict[str, Any] = {}
+
+ if version_check:
+ load_options["_version_check"] = version_check
+ if populate_existing:
+ load_options["_populate_existing"] = populate_existing
+ if refresh_state:
+ load_options["_refresh_state"] = refresh_state
+ compile_options["_for_refresh_state"] = True
+ if only_load_props:
+ compile_options["_only_load_props"] = frozenset(only_load_props)
+ if identity_token:
+ load_options["_identity_token"] = identity_token
+
+ if load_options:
+ self.load_options += load_options
+ if compile_options:
+ self._compile_options += compile_options
+
+ return self
+
+ def _clone(self, **kw: Any) -> Self:
+ return self._generate()
+
+ def _get_select_statement_only(self) -> Select[_T]:
+ if self._statement is not None:
+ raise sa_exc.InvalidRequestError(
+ "Can't call this method on a Query that uses from_statement()"
+ )
+ return cast("Select[_T]", self.statement)
+
+ @property
+ def statement(self) -> Union[Select[_T], FromStatement[_T]]:
+ """The full SELECT statement represented by this Query.
+
+ The statement by default will not have disambiguating labels
+ applied to the construct unless with_labels(True) is called
+ first.
+
+ """
+
+ # .statement can return the direct future.Select() construct here, as
+ # long as we are not using subsequent adaption features that
+ # are made against raw entities, e.g. from_self(), with_polymorphic(),
+ # select_entity_from(). If these features are being used, then
+ # the Select() we return will not have the correct .selected_columns
+ # collection and will not embed in subsequent queries correctly.
+ # We could find a way to make this collection "correct", however
+ # this would not be too different from doing the full compile as
+ # we are doing in any case, the Select() would still not have the
+ # proper state for other attributes like whereclause, order_by,
+ # and these features are all deprecated in any case.
+ #
+ # for these reasons, Query is not a Select, it remains an ORM
+ # object for which __clause_element__() must be called in order for
+ # it to provide a real expression object.
+ #
+ # from there, it starts to look much like Query itself won't be
+ # passed into the execute process and won't generate its own cache
+ # key; this will all occur in terms of the ORM-enabled Select.
+ if not self._compile_options._set_base_alias:
+ # if we don't have legacy top level aliasing features in use
+ # then convert to a future select() directly
+ stmt = self._statement_20(for_statement=True)
+ else:
+ stmt = self._compile_state(for_statement=True).statement
+
+ if self._params:
+ stmt = stmt.params(self._params)
+
+ return stmt
+
+ def _final_statement(self, legacy_query_style: bool = True) -> Select[Any]:
+ """Return the 'final' SELECT statement for this :class:`.Query`.
+
+ This is used by the testing suite only and is fairly inefficient.
+
+ This is the Core-only select() that will be rendered by a complete
+ compilation of this query, and is what .statement used to return
+ in 1.3.
+
+
+ """
+
+ q = self._clone()
+
+ return q._compile_state(
+ use_legacy_query_style=legacy_query_style
+ ).statement # type: ignore
+
+ def _statement_20(
+ self, for_statement: bool = False, use_legacy_query_style: bool = True
+ ) -> Union[Select[_T], FromStatement[_T]]:
+ # TODO: this event needs to be deprecated, as it currently applies
+ # only to ORM query and occurs at this spot that is now more
+ # or less an artificial spot
+ if self.dispatch.before_compile:
+ for fn in self.dispatch.before_compile:
+ new_query = fn(self)
+ if new_query is not None and new_query is not self:
+ self = new_query
+ if not fn._bake_ok: # type: ignore
+ self._compile_options += {"_bake_ok": False}
+
+ compile_options = self._compile_options
+ compile_options += {
+ "_for_statement": for_statement,
+ "_use_legacy_query_style": use_legacy_query_style,
+ }
+
+ stmt: Union[Select[_T], FromStatement[_T]]
+
+ if self._statement is not None:
+ stmt = FromStatement(self._raw_columns, self._statement)
+ stmt.__dict__.update(
+ _with_options=self._with_options,
+ _with_context_options=self._with_context_options,
+ _compile_options=compile_options,
+ _execution_options=self._execution_options,
+ _propagate_attrs=self._propagate_attrs,
+ )
+ else:
+ # Query / select() internal attributes are 99% cross-compatible
+ stmt = Select._create_raw_select(**self.__dict__)
+ stmt.__dict__.update(
+ _label_style=self._label_style,
+ _compile_options=compile_options,
+ _propagate_attrs=self._propagate_attrs,
+ )
+ stmt.__dict__.pop("session", None)
+
+ # ensure the ORM context is used to compile the statement, even
+ # if it has no ORM entities. This is so ORM-only things like
+ # _legacy_joins are picked up that wouldn't be picked up by the
+ # Core statement context
+ if "compile_state_plugin" not in stmt._propagate_attrs:
+ stmt._propagate_attrs = stmt._propagate_attrs.union(
+ {"compile_state_plugin": "orm", "plugin_subject": None}
+ )
+
+ return stmt
+
+ def subquery(
+ self,
+ name: Optional[str] = None,
+ with_labels: bool = False,
+ reduce_columns: bool = False,
+ ) -> Subquery:
+ """Return the full SELECT statement represented by
+ this :class:`_query.Query`, embedded within an
+ :class:`_expression.Alias`.
+
+ Eager JOIN generation within the query is disabled.
+
+ .. seealso::
+
+ :meth:`_sql.Select.subquery` - v2 comparable method.
+
+ :param name: string name to be assigned as the alias;
+ this is passed through to :meth:`_expression.FromClause.alias`.
+ If ``None``, a name will be deterministically generated
+ at compile time.
+
+ :param with_labels: if True, :meth:`.with_labels` will be called
+ on the :class:`_query.Query` first to apply table-qualified labels
+ to all columns.
+
+ :param reduce_columns: if True,
+ :meth:`_expression.Select.reduce_columns` will
+ be called on the resulting :func:`_expression.select` construct,
+ to remove same-named columns where one also refers to the other
+ via foreign key or WHERE clause equivalence.
+
+ """
+ q = self.enable_eagerloads(False)
+ if with_labels:
+ q = q.set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL)
+
+ stmt = q._get_select_statement_only()
+
+ if TYPE_CHECKING:
+ assert isinstance(stmt, Select)
+
+ if reduce_columns:
+ stmt = stmt.reduce_columns()
+ return stmt.subquery(name=name)
+
+ def cte(
+ self,
+ name: Optional[str] = None,
+ recursive: bool = False,
+ nesting: bool = False,
+ ) -> CTE:
+ r"""Return the full SELECT statement represented by this
+ :class:`_query.Query` represented as a common table expression (CTE).
+
+ Parameters and usage are the same as those of the
+ :meth:`_expression.SelectBase.cte` method; see that method for
+ further details.
+
+ Here is the `PostgreSQL WITH
+ RECURSIVE example
+ <https://www.postgresql.org/docs/current/static/queries-with.html>`_.
+ Note that, in this example, the ``included_parts`` cte and the
+ ``incl_alias`` alias of it are Core selectables, which
+ means the columns are accessed via the ``.c.`` attribute. The
+ ``parts_alias`` object is an :func:`_orm.aliased` instance of the
+ ``Part`` entity, so column-mapped attributes are available
+ directly::
+
+ from sqlalchemy.orm import aliased
+
+ class Part(Base):
+ __tablename__ = 'part'
+ part = Column(String, primary_key=True)
+ sub_part = Column(String, primary_key=True)
+ quantity = Column(Integer)
+
+ included_parts = session.query(
+ Part.sub_part,
+ Part.part,
+ Part.quantity).\
+ filter(Part.part=="our part").\
+ cte(name="included_parts", recursive=True)
+
+ incl_alias = aliased(included_parts, name="pr")
+ parts_alias = aliased(Part, name="p")
+ included_parts = included_parts.union_all(
+ session.query(
+ parts_alias.sub_part,
+ parts_alias.part,
+ parts_alias.quantity).\
+ filter(parts_alias.part==incl_alias.c.sub_part)
+ )
+
+ q = session.query(
+ included_parts.c.sub_part,
+ func.sum(included_parts.c.quantity).
+ label('total_quantity')
+ ).\
+ group_by(included_parts.c.sub_part)
+
+ .. seealso::
+
+ :meth:`_sql.Select.cte` - v2 equivalent method.
+
+ """
+ return (
+ self.enable_eagerloads(False)
+ ._get_select_statement_only()
+ .cte(name=name, recursive=recursive, nesting=nesting)
+ )
+
+ def label(self, name: Optional[str]) -> Label[Any]:
+ """Return the full SELECT statement represented by this
+ :class:`_query.Query`, converted
+ to a scalar subquery with a label of the given name.
+
+ .. seealso::
+
+ :meth:`_sql.Select.label` - v2 comparable method.
+
+ """
+
+ return (
+ self.enable_eagerloads(False)
+ ._get_select_statement_only()
+ .label(name)
+ )
+
+ @overload
+ def as_scalar(
+ self: Query[Tuple[_MAYBE_ENTITY]],
+ ) -> ScalarSelect[_MAYBE_ENTITY]: ...
+
+ @overload
+ def as_scalar(
+ self: Query[Tuple[_NOT_ENTITY]],
+ ) -> ScalarSelect[_NOT_ENTITY]: ...
+
+ @overload
+ def as_scalar(self) -> ScalarSelect[Any]: ...
+
+ @util.deprecated(
+ "1.4",
+ "The :meth:`_query.Query.as_scalar` method is deprecated and will be "
+ "removed in a future release. Please refer to "
+ ":meth:`_query.Query.scalar_subquery`.",
+ )
+ def as_scalar(self) -> ScalarSelect[Any]:
+ """Return the full SELECT statement represented by this
+ :class:`_query.Query`, converted to a scalar subquery.
+
+ """
+ return self.scalar_subquery()
+
+ @overload
+ def scalar_subquery(
+ self: Query[Tuple[_MAYBE_ENTITY]],
+ ) -> ScalarSelect[Any]: ...
+
+ @overload
+ def scalar_subquery(
+ self: Query[Tuple[_NOT_ENTITY]],
+ ) -> ScalarSelect[_NOT_ENTITY]: ...
+
+ @overload
+ def scalar_subquery(self) -> ScalarSelect[Any]: ...
+
+ def scalar_subquery(self) -> ScalarSelect[Any]:
+ """Return the full SELECT statement represented by this
+ :class:`_query.Query`, converted to a scalar subquery.
+
+ Analogous to
+ :meth:`sqlalchemy.sql.expression.SelectBase.scalar_subquery`.
+
+ .. versionchanged:: 1.4 The :meth:`_query.Query.scalar_subquery`
+ method replaces the :meth:`_query.Query.as_scalar` method.
+
+ .. seealso::
+
+ :meth:`_sql.Select.scalar_subquery` - v2 comparable method.
+
+ """
+
+ return (
+ self.enable_eagerloads(False)
+ ._get_select_statement_only()
+ .scalar_subquery()
+ )
+
+ @property
+ def selectable(self) -> Union[Select[_T], FromStatement[_T]]:
+ """Return the :class:`_expression.Select` object emitted by this
+ :class:`_query.Query`.
+
+ Used for :func:`_sa.inspect` compatibility, this is equivalent to::
+
+ query.enable_eagerloads(False).with_labels().statement
+
+ """
+ return self.__clause_element__()
+
+ def __clause_element__(self) -> Union[Select[_T], FromStatement[_T]]:
+ return (
+ self._with_compile_options(
+ _enable_eagerloads=False, _render_for_subquery=True
+ )
+ .set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL)
+ .statement
+ )
+
+ @overload
+ def only_return_tuples(
+ self: Query[_O], value: Literal[True]
+ ) -> RowReturningQuery[Tuple[_O]]: ...
+
+ @overload
+ def only_return_tuples(
+ self: Query[_O], value: Literal[False]
+ ) -> Query[_O]: ...
+
+ @_generative
+ def only_return_tuples(self, value: bool) -> Query[Any]:
+ """When set to True, the query results will always be a
+ :class:`.Row` object.
+
+ This can change a query that normally returns a single entity
+ as a scalar to return a :class:`.Row` result in all cases.
+
+ .. seealso::
+
+ :meth:`.Query.tuples` - returns tuples, but also at the typing
+ level will type results as ``Tuple``.
+
+ :meth:`_query.Query.is_single_entity`
+
+ :meth:`_engine.Result.tuples` - v2 comparable method.
+
+ """
+ self.load_options += dict(_only_return_tuples=value)
+ return self
+
+ @property
+ def is_single_entity(self) -> bool:
+ """Indicates if this :class:`_query.Query`
+ returns tuples or single entities.
+
+ Returns True if this query returns a single entity for each instance
+ in its result list, and False if this query returns a tuple of entities
+ for each result.
+
+ .. versionadded:: 1.3.11
+
+ .. seealso::
+
+ :meth:`_query.Query.only_return_tuples`
+
+ """
+ return (
+ not self.load_options._only_return_tuples
+ and len(self._raw_columns) == 1
+ and "parententity" in self._raw_columns[0]._annotations
+ and isinstance(
+ self._raw_columns[0]._annotations["parententity"],
+ ORMColumnsClauseRole,
+ )
+ )
+
+ @_generative
+ def enable_eagerloads(self, value: bool) -> Self:
+ """Control whether or not eager joins and subqueries are
+ rendered.
+
+ When set to False, the returned Query will not render
+ eager joins regardless of :func:`~sqlalchemy.orm.joinedload`,
+ :func:`~sqlalchemy.orm.subqueryload` options
+ or mapper-level ``lazy='joined'``/``lazy='subquery'``
+ configurations.
+
+ This is used primarily when nesting the Query's
+ statement into a subquery or other
+ selectable, or when using :meth:`_query.Query.yield_per`.
+
+ """
+ self._compile_options += {"_enable_eagerloads": value}
+ return self
+
+ @_generative
+ def _with_compile_options(self, **opt: Any) -> Self:
+ self._compile_options += opt
+ return self
+
+ @util.became_legacy_20(
+ ":meth:`_orm.Query.with_labels` and :meth:`_orm.Query.apply_labels`",
+ alternative="Use set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL) "
+ "instead.",
+ )
+ def with_labels(self) -> Self:
+ return self.set_label_style(
+ SelectLabelStyle.LABEL_STYLE_TABLENAME_PLUS_COL
+ )
+
+ apply_labels = with_labels
+
+ @property
+ def get_label_style(self) -> SelectLabelStyle:
+ """
+ Retrieve the current label style.
+
+ .. versionadded:: 1.4
+
+ .. seealso::
+
+ :meth:`_sql.Select.get_label_style` - v2 equivalent method.
+
+ """
+ return self._label_style
+
+ def set_label_style(self, style: SelectLabelStyle) -> Self:
+ """Apply column labels to the return value of Query.statement.
+
+ Indicates that this Query's `statement` accessor should return
+ a SELECT statement that applies labels to all columns in the
+ form <tablename>_<columnname>; this is commonly used to
+ disambiguate columns from multiple tables which have the same
+ name.
+
+ When the `Query` actually issues SQL to load rows, it always
+ uses column labeling.
+
+ .. note:: The :meth:`_query.Query.set_label_style` method *only* applies
+ the output of :attr:`_query.Query.statement`, and *not* to any of
+ the result-row invoking systems of :class:`_query.Query` itself,
+ e.g.
+ :meth:`_query.Query.first`, :meth:`_query.Query.all`, etc.
+ To execute
+ a query using :meth:`_query.Query.set_label_style`, invoke the
+ :attr:`_query.Query.statement` using :meth:`.Session.execute`::
+
+ result = session.execute(
+ query
+ .set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL)
+ .statement
+ )
+
+ .. versionadded:: 1.4
+
+
+ .. seealso::
+
+ :meth:`_sql.Select.set_label_style` - v2 equivalent method.
+
+ """ # noqa
+ if self._label_style is not style:
+ self = self._generate()
+ self._label_style = style
+ return self
+
+ @_generative
+ def enable_assertions(self, value: bool) -> Self:
+ """Control whether assertions are generated.
+
+ When set to False, the returned Query will
+ not assert its state before certain operations,
+ including that LIMIT/OFFSET has not been applied
+ when filter() is called, no criterion exists
+ when get() is called, and no "from_statement()"
+ exists when filter()/order_by()/group_by() etc.
+ is called. This more permissive mode is used by
+ custom Query subclasses to specify criterion or
+ other modifiers outside of the usual usage patterns.
+
+ Care should be taken to ensure that the usage
+ pattern is even possible. A statement applied
+ by from_statement() will override any criterion
+ set by filter() or order_by(), for example.
+
+ """
+ self._enable_assertions = value
+ return self
+
+ @property
+ def whereclause(self) -> Optional[ColumnElement[bool]]:
+ """A readonly attribute which returns the current WHERE criterion for
+ this Query.
+
+ This returned value is a SQL expression construct, or ``None`` if no
+ criterion has been established.
+
+ .. seealso::
+
+ :attr:`_sql.Select.whereclause` - v2 equivalent property.
+
+ """
+ return BooleanClauseList._construct_for_whereclause(
+ self._where_criteria
+ )
+
+ @_generative
+ def _with_current_path(self, path: PathRegistry) -> Self:
+ """indicate that this query applies to objects loaded
+ within a certain path.
+
+ Used by deferred loaders (see strategies.py) which transfer
+ query options from an originating query to a newly generated
+ query intended for the deferred load.
+
+ """
+ self._compile_options += {"_current_path": path}
+ return self
+
+ @_generative
+ def yield_per(self, count: int) -> Self:
+ r"""Yield only ``count`` rows at a time.
+
+ The purpose of this method is when fetching very large result sets
+ (> 10K rows), to batch results in sub-collections and yield them
+ out partially, so that the Python interpreter doesn't need to declare
+ very large areas of memory which is both time consuming and leads
+ to excessive memory use. The performance from fetching hundreds of
+ thousands of rows can often double when a suitable yield-per setting
+ (e.g. approximately 1000) is used, even with DBAPIs that buffer
+ rows (which are most).
+
+ As of SQLAlchemy 1.4, the :meth:`_orm.Query.yield_per` method is
+ equivalent to using the ``yield_per`` execution option at the ORM
+ level. See the section :ref:`orm_queryguide_yield_per` for further
+ background on this option.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_yield_per`
+
+ """
+ self.load_options += {"_yield_per": count}
+ return self
+
+ @util.became_legacy_20(
+ ":meth:`_orm.Query.get`",
+ alternative="The method is now available as :meth:`_orm.Session.get`",
+ )
+ def get(self, ident: _PKIdentityArgument) -> Optional[Any]:
+ """Return an instance based on the given primary key identifier,
+ or ``None`` if not found.
+
+ E.g.::
+
+ my_user = session.query(User).get(5)
+
+ some_object = session.query(VersionedFoo).get((5, 10))
+
+ some_object = session.query(VersionedFoo).get(
+ {"id": 5, "version_id": 10})
+
+ :meth:`_query.Query.get` is special in that it provides direct
+ access to the identity map of the owning :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:`_query.Query.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.
+
+ :meth:`_query.Query.get` is only used to return a single
+ mapped instance, not multiple instances or
+ individual column constructs, and strictly
+ on a single primary key value. The originating
+ :class:`_query.Query` must be constructed in this way,
+ i.e. against a single mapped entity,
+ with no additional filtering criterion. Loading
+ options via :meth:`_query.Query.options` may be applied
+ however, and will be used if the object is not
+ yet locally present.
+
+ :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 = query.get(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 = query.get((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 = query.get({"id": 5, "version_id": 10})
+
+ .. versionadded:: 1.3 the :meth:`_query.Query.get`
+ method now optionally
+ accepts a dictionary of attribute names to values in order to
+ indicate a primary key identifier.
+
+
+ :return: The object instance, or ``None``.
+
+ """
+ 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: _PKIdentityArgument,
+ db_load_fn: Callable[..., Any],
+ identity_token: Optional[Any] = None,
+ ) -> Optional[Any]:
+ mapper = self._only_full_mapper_zero("get")
+ return self.session._get_impl(
+ mapper,
+ primary_key_identity,
+ 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
+ def lazy_loaded_from(self) -> Optional[InstanceState[Any]]:
+ """An :class:`.InstanceState` that is using this :class:`_query.Query`
+ for a lazy load operation.
+
+ .. deprecated:: 1.4 This attribute should be viewed via the
+ :attr:`.ORMExecuteState.lazy_loaded_from` attribute, within
+ the context of the :meth:`.SessionEvents.do_orm_execute`
+ event.
+
+ .. seealso::
+
+ :attr:`.ORMExecuteState.lazy_loaded_from`
+
+ """
+ return self.load_options._lazy_loaded_from # type: ignore
+
+ @property
+ def _current_path(self) -> PathRegistry:
+ return self._compile_options._current_path # type: ignore
+
+ @_generative
+ def correlate(
+ self,
+ *fromclauses: Union[Literal[None, False], _FromClauseArgument],
+ ) -> Self:
+ """Return a :class:`.Query` construct which will correlate the given
+ FROM clauses to that of an enclosing :class:`.Query` or
+ :func:`~.expression.select`.
+
+ The method here accepts mapped classes, :func:`.aliased` constructs,
+ and :class:`_orm.Mapper` constructs as arguments, which are resolved
+ into expression constructs, in addition to appropriate expression
+ constructs.
+
+ The correlation arguments are ultimately passed to
+ :meth:`_expression.Select.correlate`
+ after coercion to expression constructs.
+
+ The correlation arguments take effect in such cases
+ as when :meth:`_query.Query.from_self` is used, or when
+ a subquery as returned by :meth:`_query.Query.subquery` is
+ embedded in another :func:`_expression.select` construct.
+
+ .. seealso::
+
+ :meth:`_sql.Select.correlate` - v2 equivalent method.
+
+ """
+
+ self._auto_correlate = False
+ if fromclauses and fromclauses[0] in {None, False}:
+ self._correlate = ()
+ else:
+ self._correlate = self._correlate + tuple(
+ coercions.expect(roles.FromClauseRole, f) for f in fromclauses
+ )
+ return self
+
+ @_generative
+ def autoflush(self, setting: bool) -> Self:
+ """Return a Query with a specific 'autoflush' setting.
+
+ As of SQLAlchemy 1.4, the :meth:`_orm.Query.autoflush` method
+ is equivalent to using the ``autoflush`` execution option at the
+ ORM level. See the section :ref:`orm_queryguide_autoflush` for
+ further background on this option.
+
+ """
+ self.load_options += {"_autoflush": setting}
+ return self
+
+ @_generative
+ def populate_existing(self) -> Self:
+ """Return a :class:`_query.Query`
+ that will expire and refresh all instances
+ as they are loaded, or reused from the current :class:`.Session`.
+
+ As of SQLAlchemy 1.4, the :meth:`_orm.Query.populate_existing` method
+ is equivalent to using the ``populate_existing`` execution option at
+ the ORM level. See the section :ref:`orm_queryguide_populate_existing`
+ for further background on this option.
+
+ """
+ self.load_options += {"_populate_existing": True}
+ return self
+
+ @_generative
+ def _with_invoke_all_eagers(self, value: bool) -> Self:
+ """Set the 'invoke all eagers' flag which causes joined- and
+ subquery loaders to traverse into already-loaded related objects
+ and collections.
+
+ Default is that of :attr:`_query.Query._invoke_all_eagers`.
+
+ """
+ self.load_options += {"_invoke_all_eagers": value}
+ return self
+
+ @util.became_legacy_20(
+ ":meth:`_orm.Query.with_parent`",
+ alternative="Use the :func:`_orm.with_parent` standalone construct.",
+ )
+ @util.preload_module("sqlalchemy.orm.relationships")
+ def with_parent(
+ self,
+ instance: object,
+ property: Optional[ # noqa: A002
+ attributes.QueryableAttribute[Any]
+ ] = None,
+ from_entity: Optional[_ExternalEntityType[Any]] = None,
+ ) -> Self:
+ """Add filtering criterion that relates the given instance
+ to a child object or collection, using its attribute state
+ as well as an established :func:`_orm.relationship()`
+ configuration.
+
+ The method uses the :func:`.with_parent` function to generate
+ the clause, the result of which is passed to
+ :meth:`_query.Query.filter`.
+
+ Parameters are the same as :func:`.with_parent`, with the exception
+ that the given property can be None, in which case a search is
+ performed against this :class:`_query.Query` object's target mapper.
+
+ :param instance:
+ An instance which has some :func:`_orm.relationship`.
+
+ :param property:
+ Class bound attribute which indicates
+ what relationship from the instance should be used to reconcile the
+ parent/child relationship.
+
+ :param from_entity:
+ Entity in which to consider as the left side. This defaults to the
+ "zero" entity of the :class:`_query.Query` itself.
+
+ """
+ relationships = util.preloaded.orm_relationships
+
+ if from_entity:
+ entity_zero = inspect(from_entity)
+ else:
+ entity_zero = _legacy_filter_by_entity_zero(self)
+ if property is None:
+ # TODO: deprecate, property has to be supplied
+ mapper = object_mapper(instance)
+
+ for prop in mapper.iterate_properties:
+ if (
+ isinstance(prop, relationships.RelationshipProperty)
+ and prop.mapper is entity_zero.mapper # type: ignore
+ ):
+ property = prop # type: ignore # noqa: A001
+ break
+ else:
+ raise sa_exc.InvalidRequestError(
+ "Could not locate a property which relates instances "
+ "of class '%s' to instances of class '%s'"
+ % (
+ entity_zero.mapper.class_.__name__, # type: ignore
+ instance.__class__.__name__,
+ )
+ )
+
+ return self.filter(
+ with_parent(
+ instance,
+ property, # type: ignore
+ entity_zero.entity, # type: ignore
+ )
+ )
+
+ @_generative
+ def add_entity(
+ self,
+ entity: _EntityType[Any],
+ alias: Optional[Union[Alias, Subquery]] = None,
+ ) -> Query[Any]:
+ """add a mapped entity to the list of result columns
+ to be returned.
+
+ .. seealso::
+
+ :meth:`_sql.Select.add_columns` - v2 comparable method.
+ """
+
+ if alias is not None:
+ # TODO: deprecate
+ entity = AliasedClass(entity, alias)
+
+ self._raw_columns = list(self._raw_columns)
+
+ self._raw_columns.append(
+ coercions.expect(
+ roles.ColumnsClauseRole, entity, apply_propagate_attrs=self
+ )
+ )
+ return self
+
+ @_generative
+ def with_session(self, session: Session) -> Self:
+ """Return a :class:`_query.Query` that will use the given
+ :class:`.Session`.
+
+ While the :class:`_query.Query`
+ object is normally instantiated using the
+ :meth:`.Session.query` method, it is legal to build the
+ :class:`_query.Query`
+ directly without necessarily using a :class:`.Session`. Such a
+ :class:`_query.Query` object, or any :class:`_query.Query`
+ already associated
+ with a different :class:`.Session`, can produce a new
+ :class:`_query.Query`
+ object associated with a target session using this method::
+
+ from sqlalchemy.orm import Query
+
+ query = Query([MyClass]).filter(MyClass.id == 5)
+
+ result = query.with_session(my_session).one()
+
+ """
+
+ self.session = session
+ return self
+
+ def _legacy_from_self(
+ self, *entities: _ColumnsClauseArgument[Any]
+ ) -> Self:
+ # used for query.count() as well as for the same
+ # function in BakedQuery, as well as some old tests in test_baked.py.
+
+ fromclause = (
+ self.set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL)
+ .correlate(None)
+ .subquery()
+ ._anonymous_fromclause()
+ )
+
+ q = self._from_selectable(fromclause)
+
+ if entities:
+ q._set_entities(entities)
+ return q
+
+ @_generative
+ def _set_enable_single_crit(self, val: bool) -> Self:
+ self._compile_options += {"_enable_single_crit": val}
+ return self
+
+ @_generative
+ def _from_selectable(
+ self, fromclause: FromClause, set_entity_from: bool = True
+ ) -> Self:
+ for attr in (
+ "_where_criteria",
+ "_order_by_clauses",
+ "_group_by_clauses",
+ "_limit_clause",
+ "_offset_clause",
+ "_last_joined_entity",
+ "_setup_joins",
+ "_memoized_select_entities",
+ "_distinct",
+ "_distinct_on",
+ "_having_criteria",
+ "_prefixes",
+ "_suffixes",
+ ):
+ self.__dict__.pop(attr, None)
+ self._set_select_from([fromclause], set_entity_from)
+ self._compile_options += {
+ "_enable_single_crit": False,
+ }
+
+ return self
+
+ @util.deprecated(
+ "1.4",
+ ":meth:`_query.Query.values` "
+ "is deprecated and will be removed in a "
+ "future release. Please use :meth:`_query.Query.with_entities`",
+ )
+ def values(self, *columns: _ColumnsClauseArgument[Any]) -> Iterable[Any]:
+ """Return an iterator yielding result tuples corresponding
+ to the given list of columns
+
+ """
+ return self._values_no_warn(*columns)
+
+ _values = values
+
+ def _values_no_warn(
+ self, *columns: _ColumnsClauseArgument[Any]
+ ) -> Iterable[Any]:
+ if not columns:
+ return iter(())
+ q = self._clone().enable_eagerloads(False)
+ q._set_entities(columns)
+ if not q.load_options._yield_per:
+ q.load_options += {"_yield_per": 10}
+ return iter(q)
+
+ @util.deprecated(
+ "1.4",
+ ":meth:`_query.Query.value` "
+ "is deprecated and will be removed in a "
+ "future release. Please use :meth:`_query.Query.with_entities` "
+ "in combination with :meth:`_query.Query.scalar`",
+ )
+ def value(self, column: _ColumnExpressionArgument[Any]) -> Any:
+ """Return a scalar result corresponding to the given
+ column expression.
+
+ """
+ try:
+ return next(self._values_no_warn(column))[0] # type: ignore
+ except StopIteration:
+ return None
+
+ @overload
+ def with_entities(self, _entity: _EntityType[_O]) -> Query[_O]: ...
+
+ @overload
+ def with_entities(
+ self,
+ _colexpr: roles.TypedColumnsClauseRole[_T],
+ ) -> RowReturningQuery[Tuple[_T]]: ...
+
+ # START OVERLOADED FUNCTIONS self.with_entities RowReturningQuery 2-8
+
+ # code within this block is **programmatically,
+ # statically generated** by tools/generate_tuple_map_overloads.py
+
+ @overload
+ def with_entities(
+ self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1]
+ ) -> RowReturningQuery[Tuple[_T0, _T1]]: ...
+
+ @overload
+ def with_entities(
+ self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2]
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2]]: ...
+
+ @overload
+ def with_entities(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3]]: ...
+
+ @overload
+ def with_entities(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4]]: ...
+
+ @overload
+ def with_entities(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ __ent5: _TCCA[_T5],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: ...
+
+ @overload
+ def with_entities(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ __ent5: _TCCA[_T5],
+ __ent6: _TCCA[_T6],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: ...
+
+ @overload
+ def with_entities(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ __ent5: _TCCA[_T5],
+ __ent6: _TCCA[_T6],
+ __ent7: _TCCA[_T7],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: ...
+
+ # END OVERLOADED FUNCTIONS self.with_entities
+
+ @overload
+ def with_entities(
+ self, *entities: _ColumnsClauseArgument[Any]
+ ) -> Query[Any]: ...
+
+ @_generative
+ def with_entities(
+ self, *entities: _ColumnsClauseArgument[Any], **__kw: Any
+ ) -> Query[Any]:
+ r"""Return a new :class:`_query.Query`
+ replacing the SELECT list with the
+ given entities.
+
+ e.g.::
+
+ # Users, filtered on some arbitrary criterion
+ # and then ordered by related email address
+ q = session.query(User).\
+ join(User.address).\
+ filter(User.name.like('%ed%')).\
+ order_by(Address.email)
+
+ # given *only* User.id==5, Address.email, and 'q', what
+ # would the *next* User in the result be ?
+ subq = q.with_entities(Address.email).\
+ order_by(None).\
+ filter(User.id==5).\
+ subquery()
+ q = q.join((subq, subq.c.email < Address.email)).\
+ limit(1)
+
+ .. seealso::
+
+ :meth:`_sql.Select.with_only_columns` - v2 comparable method.
+ """
+ if __kw:
+ raise _no_kw()
+
+ # Query has all the same fields as Select for this operation
+ # this could in theory be based on a protocol but not sure if it's
+ # worth it
+ _MemoizedSelectEntities._generate_for_statement(self) # type: ignore
+ self._set_entities(entities)
+ return self
+
+ @_generative
+ def add_columns(
+ self, *column: _ColumnExpressionArgument[Any]
+ ) -> Query[Any]:
+ """Add one or more column expressions to the list
+ of result columns to be returned.
+
+ .. seealso::
+
+ :meth:`_sql.Select.add_columns` - v2 comparable method.
+ """
+
+ self._raw_columns = list(self._raw_columns)
+
+ self._raw_columns.extend(
+ coercions.expect(
+ roles.ColumnsClauseRole,
+ c,
+ apply_propagate_attrs=self,
+ post_inspect=True,
+ )
+ for c in column
+ )
+ return self
+
+ @util.deprecated(
+ "1.4",
+ ":meth:`_query.Query.add_column` "
+ "is deprecated and will be removed in a "
+ "future release. Please use :meth:`_query.Query.add_columns`",
+ )
+ def add_column(self, column: _ColumnExpressionArgument[Any]) -> Query[Any]:
+ """Add a column expression to the list of result columns to be
+ returned.
+
+ """
+ return self.add_columns(column)
+
+ @_generative
+ def options(self, *args: ExecutableOption) -> Self:
+ """Return a new :class:`_query.Query` object,
+ applying the given list of
+ mapper options.
+
+ Most supplied options regard changing how column- and
+ relationship-mapped attributes are loaded.
+
+ .. seealso::
+
+ :ref:`loading_columns`
+
+ :ref:`relationship_loader_options`
+
+ """
+
+ opts = tuple(util.flatten_iterator(args))
+ if self._compile_options._current_path:
+ # opting for lower method overhead for the checks
+ for opt in opts:
+ if not opt._is_core and opt._is_legacy_option: # type: ignore
+ opt.process_query_conditionally(self) # type: ignore
+ else:
+ for opt in opts:
+ if not opt._is_core and opt._is_legacy_option: # type: ignore
+ opt.process_query(self) # type: ignore
+
+ self._with_options += opts
+ return self
+
+ def with_transformation(
+ self, fn: Callable[[Query[Any]], Query[Any]]
+ ) -> Query[Any]:
+ """Return a new :class:`_query.Query` object transformed by
+ the given function.
+
+ E.g.::
+
+ def filter_something(criterion):
+ def transform(q):
+ return q.filter(criterion)
+ return transform
+
+ q = q.with_transformation(filter_something(x==5))
+
+ This allows ad-hoc recipes to be created for :class:`_query.Query`
+ objects.
+
+ """
+ return fn(self)
+
+ def get_execution_options(self) -> _ImmutableExecuteOptions:
+ """Get the non-SQL options which will take effect during execution.
+
+ .. versionadded:: 1.3
+
+ .. seealso::
+
+ :meth:`_query.Query.execution_options`
+
+ :meth:`_sql.Select.get_execution_options` - v2 comparable method.
+
+ """
+ return self._execution_options
+
+ @overload
+ def execution_options(
+ self,
+ *,
+ compiled_cache: Optional[CompiledCacheType] = ...,
+ logging_token: str = ...,
+ isolation_level: IsolationLevel = ...,
+ no_parameters: bool = False,
+ stream_results: bool = False,
+ max_row_buffer: int = ...,
+ yield_per: int = ...,
+ insertmanyvalues_page_size: int = ...,
+ schema_translate_map: Optional[SchemaTranslateMapType] = ...,
+ populate_existing: bool = False,
+ autoflush: bool = False,
+ preserve_rowcount: bool = False,
+ **opt: Any,
+ ) -> Self: ...
+
+ @overload
+ def execution_options(self, **opt: Any) -> Self: ...
+
+ @_generative
+ def execution_options(self, **kwargs: Any) -> Self:
+ """Set non-SQL options which take effect during execution.
+
+ Options allowed here include all of those accepted by
+ :meth:`_engine.Connection.execution_options`, as well as a series
+ of ORM specific options:
+
+ ``populate_existing=True`` - equivalent to using
+ :meth:`_orm.Query.populate_existing`
+
+ ``autoflush=True|False`` - equivalent to using
+ :meth:`_orm.Query.autoflush`
+
+ ``yield_per=<value>`` - equivalent to using
+ :meth:`_orm.Query.yield_per`
+
+ Note that the ``stream_results`` execution option is enabled
+ automatically if the :meth:`~sqlalchemy.orm.query.Query.yield_per()`
+ method or execution option is used.
+
+ .. versionadded:: 1.4 - added ORM options to
+ :meth:`_orm.Query.execution_options`
+
+ The execution options may also be specified on a per execution basis
+ when using :term:`2.0 style` queries via the
+ :paramref:`_orm.Session.execution_options` parameter.
+
+ .. warning:: The
+ :paramref:`_engine.Connection.execution_options.stream_results`
+ parameter should not be used at the level of individual ORM
+ statement executions, as the :class:`_orm.Session` will not track
+ objects from different schema translate maps within a single
+ session. For multiple schema translate maps within the scope of a
+ single :class:`_orm.Session`, see :ref:`examples_sharding`.
+
+
+ .. seealso::
+
+ :ref:`engine_stream_results`
+
+ :meth:`_query.Query.get_execution_options`
+
+ :meth:`_sql.Select.execution_options` - v2 equivalent method.
+
+ """
+ self._execution_options = self._execution_options.union(kwargs)
+ return self
+
+ @_generative
+ def with_for_update(
+ self,
+ *,
+ nowait: bool = False,
+ read: bool = False,
+ of: Optional[_ForUpdateOfArgument] = None,
+ skip_locked: bool = False,
+ key_share: bool = False,
+ ) -> Self:
+ """return a new :class:`_query.Query`
+ with the specified options for the
+ ``FOR UPDATE`` clause.
+
+ The behavior of this method is identical to that of
+ :meth:`_expression.GenerativeSelect.with_for_update`.
+ When called with no arguments,
+ the resulting ``SELECT`` statement will have a ``FOR UPDATE`` clause
+ appended. When additional arguments are specified, backend-specific
+ options such as ``FOR UPDATE NOWAIT`` or ``LOCK IN SHARE MODE``
+ can take effect.
+
+ E.g.::
+
+ q = sess.query(User).populate_existing().with_for_update(nowait=True, of=User)
+
+ The above query on a PostgreSQL backend will render like::
+
+ SELECT users.id AS users_id FROM users FOR UPDATE OF users NOWAIT
+
+ .. warning::
+
+ Using ``with_for_update`` in the context of eager loading
+ relationships is not officially supported or recommended by
+ SQLAlchemy and may not work with certain queries on various
+ database backends. When ``with_for_update`` is successfully used
+ with a query that involves :func:`_orm.joinedload`, SQLAlchemy will
+ attempt to emit SQL that locks all involved tables.
+
+ .. note:: It is generally a good idea to combine the use of the
+ :meth:`_orm.Query.populate_existing` method when using the
+ :meth:`_orm.Query.with_for_update` method. The purpose of
+ :meth:`_orm.Query.populate_existing` is to force all the data read
+ from the SELECT to be populated into the ORM objects returned,
+ even if these objects are already in the :term:`identity map`.
+
+ .. seealso::
+
+ :meth:`_expression.GenerativeSelect.with_for_update`
+ - Core level method with
+ full argument and behavioral description.
+
+ :meth:`_orm.Query.populate_existing` - overwrites attributes of
+ objects already loaded in the identity map.
+
+ """ # noqa: E501
+
+ self._for_update_arg = ForUpdateArg(
+ read=read,
+ nowait=nowait,
+ of=of,
+ skip_locked=skip_locked,
+ key_share=key_share,
+ )
+ return self
+
+ @_generative
+ def params(
+ self, __params: Optional[Dict[str, Any]] = None, **kw: Any
+ ) -> Self:
+ r"""Add values for bind parameters which may have been
+ specified in filter().
+
+ Parameters may be specified using \**kwargs, or optionally a single
+ dictionary as the first positional argument. The reason for both is
+ that \**kwargs is convenient, however some parameter dictionaries
+ contain unicode keys in which case \**kwargs cannot be used.
+
+ """
+ if __params:
+ kw.update(__params)
+ self._params = self._params.union(kw)
+ return self
+
+ def where(self, *criterion: _ColumnExpressionArgument[bool]) -> Self:
+ """A synonym for :meth:`.Query.filter`.
+
+ .. versionadded:: 1.4
+
+ .. seealso::
+
+ :meth:`_sql.Select.where` - v2 equivalent method.
+
+ """
+ return self.filter(*criterion)
+
+ @_generative
+ @_assertions(_no_statement_condition, _no_limit_offset)
+ def filter(self, *criterion: _ColumnExpressionArgument[bool]) -> Self:
+ r"""Apply the given filtering criterion to a copy
+ of this :class:`_query.Query`, using SQL expressions.
+
+ e.g.::
+
+ session.query(MyClass).filter(MyClass.name == 'some name')
+
+ Multiple criteria may be specified as comma separated; the effect
+ is that they will be joined together using the :func:`.and_`
+ function::
+
+ session.query(MyClass).\
+ filter(MyClass.name == 'some name', MyClass.id > 5)
+
+ The criterion is any SQL expression object applicable to the
+ WHERE clause of a select. String expressions are coerced
+ into SQL expression constructs via the :func:`_expression.text`
+ construct.
+
+ .. seealso::
+
+ :meth:`_query.Query.filter_by` - filter on keyword expressions.
+
+ :meth:`_sql.Select.where` - v2 equivalent method.
+
+ """
+ for crit in list(criterion):
+ crit = coercions.expect(
+ roles.WhereHavingRole, crit, apply_propagate_attrs=self
+ )
+
+ self._where_criteria += (crit,)
+ return self
+
+ @util.memoized_property
+ def _last_joined_entity(
+ self,
+ ) -> Optional[Union[_InternalEntityType[Any], _JoinTargetElement]]:
+ if self._setup_joins:
+ return _determine_last_joined_entity(
+ self._setup_joins,
+ )
+ else:
+ return None
+
+ def _filter_by_zero(self) -> Any:
+ """for the filter_by() method, return the target entity for which
+ we will attempt to derive an expression from based on string name.
+
+ """
+
+ if self._setup_joins:
+ _last_joined_entity = self._last_joined_entity
+ if _last_joined_entity is not None:
+ return _last_joined_entity
+
+ # discussion related to #7239
+ # special check determines if we should try to derive attributes
+ # for filter_by() from the "from object", i.e., if the user
+ # called query.select_from(some selectable).filter_by(some_attr=value).
+ # We don't want to do that in the case that methods like
+ # from_self(), select_entity_from(), or a set op like union() were
+ # called; while these methods also place a
+ # selectable in the _from_obj collection, they also set up
+ # the _set_base_alias boolean which turns on the whole "adapt the
+ # entity to this selectable" thing, meaning the query still continues
+ # to construct itself in terms of the lead entity that was passed
+ # to query(), e.g. query(User).from_self() is still in terms of User,
+ # and not the subquery that from_self() created. This feature of
+ # "implicitly adapt all occurrences of entity X to some arbitrary
+ # subquery" is the main thing I am trying to do away with in 2.0 as
+ # users should now used aliased() for that, but I can't entirely get
+ # rid of it due to query.union() and other set ops relying upon it.
+ #
+ # compare this to the base Select()._filter_by_zero() which can
+ # just return self._from_obj[0] if present, because there is no
+ # "_set_base_alias" feature.
+ #
+ # IOW, this conditional essentially detects if
+ # "select_from(some_selectable)" has been called, as opposed to
+ # "select_entity_from()", "from_self()"
+ # or "union() / some_set_op()".
+ if self._from_obj and not self._compile_options._set_base_alias:
+ return self._from_obj[0]
+
+ return self._raw_columns[0]
+
+ def filter_by(self, **kwargs: Any) -> Self:
+ r"""Apply the given filtering criterion to a copy
+ of this :class:`_query.Query`, using keyword expressions.
+
+ e.g.::
+
+ session.query(MyClass).filter_by(name = 'some name')
+
+ Multiple criteria may be specified as comma separated; the effect
+ is that they will be joined together using the :func:`.and_`
+ function::
+
+ session.query(MyClass).\
+ filter_by(name = 'some name', id = 5)
+
+ The keyword expressions are extracted from the primary
+ entity of the query, or the last entity that was the
+ target of a call to :meth:`_query.Query.join`.
+
+ .. seealso::
+
+ :meth:`_query.Query.filter` - filter on SQL expressions.
+
+ :meth:`_sql.Select.filter_by` - v2 comparable method.
+
+ """
+ from_entity = self._filter_by_zero()
+
+ clauses = [
+ _entity_namespace_key(from_entity, key) == value
+ for key, value in kwargs.items()
+ ]
+ return self.filter(*clauses)
+
+ @_generative
+ def order_by(
+ self,
+ __first: Union[
+ Literal[None, False, _NoArg.NO_ARG],
+ _ColumnExpressionOrStrLabelArgument[Any],
+ ] = _NoArg.NO_ARG,
+ *clauses: _ColumnExpressionOrStrLabelArgument[Any],
+ ) -> Self:
+ """Apply one or more ORDER BY criteria to the query and return
+ the newly resulting :class:`_query.Query`.
+
+ e.g.::
+
+ q = session.query(Entity).order_by(Entity.id, Entity.name)
+
+ Calling this method multiple times is equivalent to calling it once
+ with all the clauses concatenated. All existing ORDER BY criteria may
+ be cancelled by passing ``None`` by itself. New ORDER BY criteria may
+ then be added by invoking :meth:`_orm.Query.order_by` again, e.g.::
+
+ # will erase all ORDER BY and ORDER BY new_col alone
+ q = q.order_by(None).order_by(new_col)
+
+ .. seealso::
+
+ These sections describe ORDER BY in terms of :term:`2.0 style`
+ invocation but apply to :class:`_orm.Query` as well:
+
+ :ref:`tutorial_order_by` - in the :ref:`unified_tutorial`
+
+ :ref:`tutorial_order_by_label` - in the :ref:`unified_tutorial`
+
+ :meth:`_sql.Select.order_by` - v2 equivalent method.
+
+ """
+
+ for assertion in (self._no_statement_condition, self._no_limit_offset):
+ assertion("order_by")
+
+ if not clauses and (__first is None or __first is False):
+ self._order_by_clauses = ()
+ elif __first is not _NoArg.NO_ARG:
+ criterion = tuple(
+ coercions.expect(roles.OrderByRole, clause)
+ for clause in (__first,) + clauses
+ )
+ self._order_by_clauses += criterion
+
+ return self
+
+ @_generative
+ def group_by(
+ self,
+ __first: Union[
+ Literal[None, False, _NoArg.NO_ARG],
+ _ColumnExpressionOrStrLabelArgument[Any],
+ ] = _NoArg.NO_ARG,
+ *clauses: _ColumnExpressionOrStrLabelArgument[Any],
+ ) -> Self:
+ """Apply one or more GROUP BY criterion to the query and return
+ the newly resulting :class:`_query.Query`.
+
+ All existing GROUP BY settings can be suppressed by
+ passing ``None`` - this will suppress any GROUP BY configured
+ on mappers as well.
+
+ .. seealso::
+
+ These sections describe GROUP BY in terms of :term:`2.0 style`
+ invocation but apply to :class:`_orm.Query` as well:
+
+ :ref:`tutorial_group_by_w_aggregates` - in the
+ :ref:`unified_tutorial`
+
+ :ref:`tutorial_order_by_label` - in the :ref:`unified_tutorial`
+
+ :meth:`_sql.Select.group_by` - v2 equivalent method.
+
+ """
+
+ for assertion in (self._no_statement_condition, self._no_limit_offset):
+ assertion("group_by")
+
+ if not clauses and (__first is None or __first is False):
+ self._group_by_clauses = ()
+ elif __first is not _NoArg.NO_ARG:
+ criterion = tuple(
+ coercions.expect(roles.GroupByRole, clause)
+ for clause in (__first,) + clauses
+ )
+ self._group_by_clauses += criterion
+ return self
+
+ @_generative
+ @_assertions(_no_statement_condition, _no_limit_offset)
+ def having(self, *having: _ColumnExpressionArgument[bool]) -> Self:
+ r"""Apply a HAVING criterion to the query and return the
+ newly resulting :class:`_query.Query`.
+
+ :meth:`_query.Query.having` is used in conjunction with
+ :meth:`_query.Query.group_by`.
+
+ HAVING criterion makes it possible to use filters on aggregate
+ functions like COUNT, SUM, AVG, MAX, and MIN, eg.::
+
+ q = session.query(User.id).\
+ join(User.addresses).\
+ group_by(User.id).\
+ having(func.count(Address.id) > 2)
+
+ .. seealso::
+
+ :meth:`_sql.Select.having` - v2 equivalent method.
+
+ """
+
+ for criterion in having:
+ having_criteria = coercions.expect(
+ roles.WhereHavingRole, criterion
+ )
+ self._having_criteria += (having_criteria,)
+ return self
+
+ def _set_op(self, expr_fn: Any, *q: Query[Any]) -> Self:
+ list_of_queries = (self,) + q
+ return self._from_selectable(expr_fn(*(list_of_queries)).subquery())
+
+ def union(self, *q: Query[Any]) -> Self:
+ """Produce a UNION of this Query against one or more queries.
+
+ e.g.::
+
+ q1 = sess.query(SomeClass).filter(SomeClass.foo=='bar')
+ q2 = sess.query(SomeClass).filter(SomeClass.bar=='foo')
+
+ q3 = q1.union(q2)
+
+ The method accepts multiple Query objects so as to control
+ the level of nesting. A series of ``union()`` calls such as::
+
+ x.union(y).union(z).all()
+
+ will nest on each ``union()``, and produces::
+
+ SELECT * FROM (SELECT * FROM (SELECT * FROM X UNION
+ SELECT * FROM y) UNION SELECT * FROM Z)
+
+ Whereas::
+
+ x.union(y, z).all()
+
+ produces::
+
+ SELECT * FROM (SELECT * FROM X UNION SELECT * FROM y UNION
+ SELECT * FROM Z)
+
+ Note that many database backends do not allow ORDER BY to
+ be rendered on a query called within UNION, EXCEPT, etc.
+ To disable all ORDER BY clauses including those configured
+ on mappers, issue ``query.order_by(None)`` - the resulting
+ :class:`_query.Query` object will not render ORDER BY within
+ its SELECT statement.
+
+ .. seealso::
+
+ :meth:`_sql.Select.union` - v2 equivalent method.
+
+ """
+ return self._set_op(expression.union, *q)
+
+ def union_all(self, *q: Query[Any]) -> Self:
+ """Produce a UNION ALL of this Query against one or more queries.
+
+ Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See
+ that method for usage examples.
+
+ .. seealso::
+
+ :meth:`_sql.Select.union_all` - v2 equivalent method.
+
+ """
+ return self._set_op(expression.union_all, *q)
+
+ def intersect(self, *q: Query[Any]) -> Self:
+ """Produce an INTERSECT of this Query against one or more queries.
+
+ Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See
+ that method for usage examples.
+
+ .. seealso::
+
+ :meth:`_sql.Select.intersect` - v2 equivalent method.
+
+ """
+ return self._set_op(expression.intersect, *q)
+
+ def intersect_all(self, *q: Query[Any]) -> Self:
+ """Produce an INTERSECT ALL of this Query against one or more queries.
+
+ Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See
+ that method for usage examples.
+
+ .. seealso::
+
+ :meth:`_sql.Select.intersect_all` - v2 equivalent method.
+
+ """
+ return self._set_op(expression.intersect_all, *q)
+
+ def except_(self, *q: Query[Any]) -> Self:
+ """Produce an EXCEPT of this Query against one or more queries.
+
+ Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See
+ that method for usage examples.
+
+ .. seealso::
+
+ :meth:`_sql.Select.except_` - v2 equivalent method.
+
+ """
+ return self._set_op(expression.except_, *q)
+
+ def except_all(self, *q: Query[Any]) -> Self:
+ """Produce an EXCEPT ALL of this Query against one or more queries.
+
+ Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See
+ that method for usage examples.
+
+ .. seealso::
+
+ :meth:`_sql.Select.except_all` - v2 equivalent method.
+
+ """
+ return self._set_op(expression.except_all, *q)
+
+ @_generative
+ @_assertions(_no_statement_condition, _no_limit_offset)
+ def join(
+ self,
+ target: _JoinTargetArgument,
+ onclause: Optional[_OnClauseArgument] = None,
+ *,
+ isouter: bool = False,
+ full: bool = False,
+ ) -> Self:
+ r"""Create a SQL JOIN against this :class:`_query.Query`
+ object's criterion
+ and apply generatively, returning the newly resulting
+ :class:`_query.Query`.
+
+ **Simple Relationship Joins**
+
+ Consider a mapping between two classes ``User`` and ``Address``,
+ with a relationship ``User.addresses`` representing a collection
+ of ``Address`` objects associated with each ``User``. The most
+ common usage of :meth:`_query.Query.join`
+ is to create a JOIN along this
+ relationship, using the ``User.addresses`` attribute as an indicator
+ for how this should occur::
+
+ q = session.query(User).join(User.addresses)
+
+ Where above, the call to :meth:`_query.Query.join` along
+ ``User.addresses`` will result in SQL approximately equivalent to::
+
+ SELECT user.id, user.name
+ FROM user JOIN address ON user.id = address.user_id
+
+ In the above example we refer to ``User.addresses`` as passed to
+ :meth:`_query.Query.join` as the "on clause", that is, it indicates
+ how the "ON" portion of the JOIN should be constructed.
+
+ To construct a chain of joins, multiple :meth:`_query.Query.join`
+ calls may be used. The relationship-bound attribute implies both
+ the left and right side of the join at once::
+
+ q = session.query(User).\
+ join(User.orders).\
+ join(Order.items).\
+ join(Item.keywords)
+
+ .. note:: as seen in the above example, **the order in which each
+ call to the join() method occurs is important**. Query would not,
+ for example, know how to join correctly if we were to specify
+ ``User``, then ``Item``, then ``Order``, in our chain of joins; in
+ such a case, depending on the arguments passed, it may raise an
+ error that it doesn't know how to join, or it may produce invalid
+ SQL in which case the database will raise an error. In correct
+ practice, the
+ :meth:`_query.Query.join` method is invoked in such a way that lines
+ up with how we would want the JOIN clauses in SQL to be
+ rendered, and each call should represent a clear link from what
+ precedes it.
+
+ **Joins to a Target Entity or Selectable**
+
+ A second form of :meth:`_query.Query.join` allows any mapped entity or
+ core selectable construct as a target. In this usage,
+ :meth:`_query.Query.join` will attempt to create a JOIN along the
+ natural foreign key relationship between two entities::
+
+ q = session.query(User).join(Address)
+
+ In the above calling form, :meth:`_query.Query.join` is called upon to
+ create the "on clause" automatically for us. This calling form will
+ ultimately raise an error if either there are no foreign keys between
+ the two entities, or if there are multiple foreign key linkages between
+ the target entity and the entity or entities already present on the
+ left side such that creating a join requires more information. Note
+ that when indicating a join to a target without any ON clause, ORM
+ configured relationships are not taken into account.
+
+ **Joins to a Target with an ON Clause**
+
+ The third calling form allows both the target entity as well
+ as the ON clause to be passed explicitly. A example that includes
+ a SQL expression as the ON clause is as follows::
+
+ q = session.query(User).join(Address, User.id==Address.user_id)
+
+ The above form may also use a relationship-bound attribute as the
+ ON clause as well::
+
+ q = session.query(User).join(Address, User.addresses)
+
+ The above syntax can be useful for the case where we wish
+ to join to an alias of a particular target entity. If we wanted
+ to join to ``Address`` twice, it could be achieved using two
+ aliases set up using the :func:`~sqlalchemy.orm.aliased` function::
+
+ a1 = aliased(Address)
+ a2 = aliased(Address)
+
+ q = session.query(User).\
+ join(a1, User.addresses).\
+ join(a2, User.addresses).\
+ filter(a1.email_address=='ed@foo.com').\
+ filter(a2.email_address=='ed@bar.com')
+
+ The relationship-bound calling form can also specify a target entity
+ using the :meth:`_orm.PropComparator.of_type` method; a query
+ equivalent to the one above would be::
+
+ a1 = aliased(Address)
+ a2 = aliased(Address)
+
+ q = session.query(User).\
+ join(User.addresses.of_type(a1)).\
+ join(User.addresses.of_type(a2)).\
+ filter(a1.email_address == 'ed@foo.com').\
+ filter(a2.email_address == 'ed@bar.com')
+
+ **Augmenting Built-in ON Clauses**
+
+ As a substitute for providing a full custom ON condition for an
+ existing relationship, the :meth:`_orm.PropComparator.and_` function
+ may be applied to a relationship attribute to augment additional
+ criteria into the ON clause; the additional criteria will be combined
+ with the default criteria using AND::
+
+ q = session.query(User).join(
+ User.addresses.and_(Address.email_address != 'foo@bar.com')
+ )
+
+ .. versionadded:: 1.4
+
+ **Joining to Tables and Subqueries**
+
+
+ The target of a join may also be any table or SELECT statement,
+ which may be related to a target entity or not. Use the
+ appropriate ``.subquery()`` method in order to make a subquery
+ out of a query::
+
+ subq = session.query(Address).\
+ filter(Address.email_address == 'ed@foo.com').\
+ subquery()
+
+
+ q = session.query(User).join(
+ subq, User.id == subq.c.user_id
+ )
+
+ Joining to a subquery in terms of a specific relationship and/or
+ target entity may be achieved by linking the subquery to the
+ entity using :func:`_orm.aliased`::
+
+ subq = session.query(Address).\
+ filter(Address.email_address == 'ed@foo.com').\
+ subquery()
+
+ address_subq = aliased(Address, subq)
+
+ q = session.query(User).join(
+ User.addresses.of_type(address_subq)
+ )
+
+
+ **Controlling what to Join From**
+
+ In cases where the left side of the current state of
+ :class:`_query.Query` is not in line with what we want to join from,
+ the :meth:`_query.Query.select_from` method may be used::
+
+ q = session.query(Address).select_from(User).\
+ join(User.addresses).\
+ filter(User.name == 'ed')
+
+ Which will produce SQL similar to::
+
+ SELECT address.* FROM user
+ JOIN address ON user.id=address.user_id
+ WHERE user.name = :name_1
+
+ .. seealso::
+
+ :meth:`_sql.Select.join` - v2 equivalent method.
+
+ :param \*props: Incoming arguments for :meth:`_query.Query.join`,
+ the props collection in modern use should be considered to be a one
+ or two argument form, either as a single "target" entity or ORM
+ attribute-bound relationship, or as a target entity plus an "on
+ clause" which may be a SQL expression or ORM attribute-bound
+ relationship.
+
+ :param isouter=False: If True, the join used will be a left outer join,
+ just as if the :meth:`_query.Query.outerjoin` method were called.
+
+ :param full=False: render FULL OUTER JOIN; implies ``isouter``.
+
+ """
+
+ join_target = coercions.expect(
+ roles.JoinTargetRole,
+ target,
+ apply_propagate_attrs=self,
+ legacy=True,
+ )
+ if onclause is not None:
+ onclause_element = coercions.expect(
+ roles.OnClauseRole, onclause, legacy=True
+ )
+ else:
+ onclause_element = None
+
+ self._setup_joins += (
+ (
+ join_target,
+ onclause_element,
+ None,
+ {
+ "isouter": isouter,
+ "full": full,
+ },
+ ),
+ )
+
+ self.__dict__.pop("_last_joined_entity", None)
+ return self
+
+ def outerjoin(
+ self,
+ target: _JoinTargetArgument,
+ onclause: Optional[_OnClauseArgument] = None,
+ *,
+ full: bool = False,
+ ) -> Self:
+ """Create a left outer join against this ``Query`` object's criterion
+ and apply generatively, returning the newly resulting ``Query``.
+
+ Usage is the same as the ``join()`` method.
+
+ .. seealso::
+
+ :meth:`_sql.Select.outerjoin` - v2 equivalent method.
+
+ """
+ return self.join(target, onclause=onclause, isouter=True, full=full)
+
+ @_generative
+ @_assertions(_no_statement_condition)
+ def reset_joinpoint(self) -> Self:
+ """Return a new :class:`.Query`, where the "join point" has
+ been reset back to the base FROM entities of the query.
+
+ This method is usually used in conjunction with the
+ ``aliased=True`` feature of the :meth:`~.Query.join`
+ method. See the example in :meth:`~.Query.join` for how
+ this is used.
+
+ """
+ self._last_joined_entity = None
+
+ return self
+
+ @_generative
+ @_assertions(_no_clauseelement_condition)
+ def select_from(self, *from_obj: _FromClauseArgument) -> Self:
+ r"""Set the FROM clause of this :class:`.Query` explicitly.
+
+ :meth:`.Query.select_from` is often used in conjunction with
+ :meth:`.Query.join` in order to control which entity is selected
+ from on the "left" side of the join.
+
+ The entity or selectable object here effectively replaces the
+ "left edge" of any calls to :meth:`~.Query.join`, when no
+ joinpoint is otherwise established - usually, the default "join
+ point" is the leftmost entity in the :class:`~.Query` object's
+ list of entities to be selected.
+
+ A typical example::
+
+ q = session.query(Address).select_from(User).\
+ join(User.addresses).\
+ filter(User.name == 'ed')
+
+ Which produces SQL equivalent to::
+
+ SELECT address.* FROM user
+ JOIN address ON user.id=address.user_id
+ WHERE user.name = :name_1
+
+ :param \*from_obj: collection of one or more entities to apply
+ to the FROM clause. Entities can be mapped classes,
+ :class:`.AliasedClass` objects, :class:`.Mapper` objects
+ as well as core :class:`.FromClause` elements like subqueries.
+
+ .. seealso::
+
+ :meth:`~.Query.join`
+
+ :meth:`.Query.select_entity_from`
+
+ :meth:`_sql.Select.select_from` - v2 equivalent method.
+
+ """
+
+ self._set_select_from(from_obj, False)
+ return self
+
+ def __getitem__(self, item: Any) -> Any:
+ return orm_util._getitem(
+ self,
+ item,
+ )
+
+ @_generative
+ @_assertions(_no_statement_condition)
+ def slice(
+ self,
+ start: int,
+ stop: int,
+ ) -> Self:
+ """Computes the "slice" of the :class:`_query.Query` represented by
+ the given indices and returns the resulting :class:`_query.Query`.
+
+ The start and stop indices behave like the argument to Python's
+ built-in :func:`range` function. This method provides an
+ alternative to using ``LIMIT``/``OFFSET`` to get a slice of the
+ query.
+
+ For example, ::
+
+ session.query(User).order_by(User.id).slice(1, 3)
+
+ renders as
+
+ .. sourcecode:: sql
+
+ SELECT users.id AS users_id,
+ users.name AS users_name
+ FROM users ORDER BY users.id
+ LIMIT ? OFFSET ?
+ (2, 1)
+
+ .. seealso::
+
+ :meth:`_query.Query.limit`
+
+ :meth:`_query.Query.offset`
+
+ :meth:`_sql.Select.slice` - v2 equivalent method.
+
+ """
+
+ self._limit_clause, self._offset_clause = sql_util._make_slice(
+ self._limit_clause, self._offset_clause, start, stop
+ )
+ return self
+
+ @_generative
+ @_assertions(_no_statement_condition)
+ def limit(self, limit: _LimitOffsetType) -> Self:
+ """Apply a ``LIMIT`` to the query and return the newly resulting
+ ``Query``.
+
+ .. seealso::
+
+ :meth:`_sql.Select.limit` - v2 equivalent method.
+
+ """
+ self._limit_clause = sql_util._offset_or_limit_clause(limit)
+ return self
+
+ @_generative
+ @_assertions(_no_statement_condition)
+ def offset(self, offset: _LimitOffsetType) -> Self:
+ """Apply an ``OFFSET`` to the query and return the newly resulting
+ ``Query``.
+
+ .. seealso::
+
+ :meth:`_sql.Select.offset` - v2 equivalent method.
+ """
+ self._offset_clause = sql_util._offset_or_limit_clause(offset)
+ return self
+
+ @_generative
+ @_assertions(_no_statement_condition)
+ def distinct(self, *expr: _ColumnExpressionArgument[Any]) -> Self:
+ r"""Apply a ``DISTINCT`` to the query and return the newly resulting
+ ``Query``.
+
+
+ .. note::
+
+ The ORM-level :meth:`.distinct` call includes logic that will
+ automatically add columns from the ORDER BY of the query to the
+ columns clause of the SELECT statement, to satisfy the common need
+ of the database backend that ORDER BY columns be part of the SELECT
+ list when DISTINCT is used. These columns *are not* added to the
+ list of columns actually fetched by the :class:`_query.Query`,
+ however,
+ so would not affect results. The columns are passed through when
+ using the :attr:`_query.Query.statement` accessor, however.
+
+ .. deprecated:: 2.0 This logic is deprecated and will be removed
+ in SQLAlchemy 2.0. See :ref:`migration_20_query_distinct`
+ for a description of this use case in 2.0.
+
+ .. seealso::
+
+ :meth:`_sql.Select.distinct` - v2 equivalent method.
+
+ :param \*expr: optional column expressions. When present,
+ the PostgreSQL dialect will render a ``DISTINCT ON (<expressions>)``
+ construct.
+
+ .. deprecated:: 1.4 Using \*expr in other dialects is deprecated
+ and will raise :class:`_exc.CompileError` in a future version.
+
+ """
+ if expr:
+ self._distinct = True
+ self._distinct_on = self._distinct_on + tuple(
+ coercions.expect(roles.ByOfRole, e) for e in expr
+ )
+ else:
+ self._distinct = True
+ return self
+
+ def all(self) -> List[_T]:
+ """Return the results represented by this :class:`_query.Query`
+ as a list.
+
+ This results in an execution of the underlying SQL statement.
+
+ .. warning:: The :class:`_query.Query` object,
+ when asked to return either
+ a sequence or iterator that consists of full ORM-mapped entities,
+ will **deduplicate entries based on primary key**. See the FAQ for
+ more details.
+
+ .. seealso::
+
+ :ref:`faq_query_deduplicating`
+
+ .. seealso::
+
+ :meth:`_engine.Result.all` - v2 comparable method.
+
+ :meth:`_engine.Result.scalars` - v2 comparable method.
+ """
+ return self._iter().all() # type: ignore
+
+ @_generative
+ @_assertions(_no_clauseelement_condition)
+ def from_statement(self, statement: ExecutableReturnsRows) -> Self:
+ """Execute the given SELECT statement and return results.
+
+ This method bypasses all internal statement compilation, and the
+ statement is executed without modification.
+
+ The statement is typically either a :func:`_expression.text`
+ or :func:`_expression.select` construct, and should return the set
+ of columns
+ appropriate to the entity class represented by this
+ :class:`_query.Query`.
+
+ .. seealso::
+
+ :meth:`_sql.Select.from_statement` - v2 comparable method.
+
+ """
+ statement = coercions.expect(
+ roles.SelectStatementRole, statement, apply_propagate_attrs=self
+ )
+ self._statement = statement
+ return self
+
+ def first(self) -> Optional[_T]:
+ """Return the first result of this ``Query`` or
+ None if the result doesn't contain any row.
+
+ first() applies a limit of one within the generated SQL, so that
+ only one primary entity row is generated on the server side
+ (note this may consist of multiple result rows if join-loaded
+ collections are present).
+
+ Calling :meth:`_query.Query.first`
+ results in an execution of the underlying
+ query.
+
+ .. seealso::
+
+ :meth:`_query.Query.one`
+
+ :meth:`_query.Query.one_or_none`
+
+ :meth:`_engine.Result.first` - v2 comparable method.
+
+ :meth:`_engine.Result.scalars` - v2 comparable method.
+
+ """
+ # replicates limit(1) behavior
+ if self._statement is not None:
+ return self._iter().first() # type: ignore
+ else:
+ return self.limit(1)._iter().first() # type: ignore
+
+ def one_or_none(self) -> Optional[_T]:
+ """Return at most one result or raise an exception.
+
+ Returns ``None`` if the query selects
+ no rows. Raises ``sqlalchemy.orm.exc.MultipleResultsFound``
+ if multiple object identities are returned, or if multiple
+ rows are returned for a query that returns only scalar values
+ as opposed to full identity-mapped entities.
+
+ Calling :meth:`_query.Query.one_or_none`
+ results in an execution of the
+ underlying query.
+
+ .. seealso::
+
+ :meth:`_query.Query.first`
+
+ :meth:`_query.Query.one`
+
+ :meth:`_engine.Result.one_or_none` - v2 comparable method.
+
+ :meth:`_engine.Result.scalar_one_or_none` - v2 comparable method.
+
+ """
+ return self._iter().one_or_none() # type: ignore
+
+ def one(self) -> _T:
+ """Return exactly one result or raise an exception.
+
+ Raises ``sqlalchemy.orm.exc.NoResultFound`` if the query selects
+ no rows. Raises ``sqlalchemy.orm.exc.MultipleResultsFound``
+ if multiple object identities are returned, or if multiple
+ rows are returned for a query that returns only scalar values
+ as opposed to full identity-mapped entities.
+
+ Calling :meth:`.one` results in an execution of the underlying query.
+
+ .. seealso::
+
+ :meth:`_query.Query.first`
+
+ :meth:`_query.Query.one_or_none`
+
+ :meth:`_engine.Result.one` - v2 comparable method.
+
+ :meth:`_engine.Result.scalar_one` - v2 comparable method.
+
+ """
+ return self._iter().one() # type: ignore
+
+ def scalar(self) -> Any:
+ """Return the first element of the first result or None
+ if no rows present. If multiple rows are returned,
+ raises MultipleResultsFound.
+
+ >>> session.query(Item).scalar()
+ <Item>
+ >>> session.query(Item.id).scalar()
+ 1
+ >>> session.query(Item.id).filter(Item.id < 0).scalar()
+ None
+ >>> session.query(Item.id, Item.name).scalar()
+ 1
+ >>> session.query(func.count(Parent.id)).scalar()
+ 20
+
+ This results in an execution of the underlying query.
+
+ .. seealso::
+
+ :meth:`_engine.Result.scalar` - v2 comparable method.
+
+ """
+ # TODO: not sure why we can't use result.scalar() here
+ try:
+ ret = self.one()
+ if not isinstance(ret, collections_abc.Sequence):
+ return ret
+ return ret[0]
+ except sa_exc.NoResultFound:
+ return None
+
+ def __iter__(self) -> Iterator[_T]:
+ result = self._iter()
+ try:
+ yield from result # type: ignore
+ except GeneratorExit:
+ # issue #8710 - direct iteration is not re-usable after
+ # an iterable block is broken, so close the result
+ result._soft_close()
+ raise
+
+ def _iter(self) -> Union[ScalarResult[_T], Result[_T]]:
+ # new style execution.
+ params = self._params
+
+ statement = self._statement_20()
+ result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
+ statement,
+ params,
+ execution_options={"_sa_orm_load_options": self.load_options},
+ )
+
+ # legacy: automatically set scalars, unique
+ if result._attributes.get("is_single_entity", False):
+ result = cast("Result[_T]", result).scalars()
+
+ if (
+ result._attributes.get("filtered", False)
+ and not self.load_options._yield_per
+ ):
+ result = result.unique()
+
+ return result
+
+ def __str__(self) -> str:
+ statement = self._statement_20()
+
+ try:
+ bind = (
+ self._get_bind_args(statement, self.session.get_bind)
+ if self.session
+ else None
+ )
+ except sa_exc.UnboundExecutionError:
+ bind = None
+
+ return str(statement.compile(bind))
+
+ def _get_bind_args(self, statement: Any, fn: Any, **kw: Any) -> Any:
+ return fn(clause=statement, **kw)
+
+ @property
+ def column_descriptions(self) -> List[ORMColumnDescription]:
+ """Return metadata about the columns which would be
+ returned by this :class:`_query.Query`.
+
+ Format is a list of dictionaries::
+
+ user_alias = aliased(User, name='user2')
+ q = sess.query(User, User.id, user_alias)
+
+ # this expression:
+ q.column_descriptions
+
+ # would return:
+ [
+ {
+ 'name':'User',
+ 'type':User,
+ 'aliased':False,
+ 'expr':User,
+ 'entity': User
+ },
+ {
+ 'name':'id',
+ 'type':Integer(),
+ 'aliased':False,
+ 'expr':User.id,
+ 'entity': User
+ },
+ {
+ 'name':'user2',
+ 'type':User,
+ 'aliased':True,
+ 'expr':user_alias,
+ 'entity': user_alias
+ }
+ ]
+
+ .. seealso::
+
+ This API is available using :term:`2.0 style` queries as well,
+ documented at:
+
+ * :ref:`queryguide_inspection`
+
+ * :attr:`.Select.column_descriptions`
+
+ """
+
+ return _column_descriptions(self, legacy=True)
+
+ @util.deprecated(
+ "2.0",
+ "The :meth:`_orm.Query.instances` method is deprecated and will "
+ "be removed in a future release. "
+ "Use the Select.from_statement() method or aliased() construct in "
+ "conjunction with Session.execute() instead.",
+ )
+ def instances(
+ self,
+ result_proxy: CursorResult[Any],
+ context: Optional[QueryContext] = None,
+ ) -> Any:
+ """Return an ORM result given a :class:`_engine.CursorResult` and
+ :class:`.QueryContext`.
+
+ """
+ if context is None:
+ util.warn_deprecated(
+ "Using the Query.instances() method without a context "
+ "is deprecated and will be disallowed in a future release. "
+ "Please make use of :meth:`_query.Query.from_statement` "
+ "for linking ORM results to arbitrary select constructs.",
+ version="1.4",
+ )
+ compile_state = self._compile_state(for_statement=False)
+
+ context = QueryContext(
+ compile_state,
+ compile_state.statement,
+ self._params,
+ self.session,
+ self.load_options,
+ )
+
+ result = loading.instances(result_proxy, context)
+
+ # legacy: automatically set scalars, unique
+ if result._attributes.get("is_single_entity", False):
+ result = result.scalars() # type: ignore
+
+ if result._attributes.get("filtered", False):
+ result = result.unique()
+
+ # TODO: isn't this supposed to be a list?
+ return result
+
+ @util.became_legacy_20(
+ ":meth:`_orm.Query.merge_result`",
+ alternative="The method is superseded by the "
+ ":func:`_orm.merge_frozen_result` function.",
+ enable_warnings=False, # warnings occur via loading.merge_result
+ )
+ def merge_result(
+ self,
+ iterator: Union[
+ FrozenResult[Any], Iterable[Sequence[Any]], Iterable[object]
+ ],
+ load: bool = True,
+ ) -> Union[FrozenResult[Any], Iterable[Any]]:
+ """Merge a result into this :class:`_query.Query` object's Session.
+
+ Given an iterator returned by a :class:`_query.Query`
+ of the same structure
+ as this one, return an identical iterator of results, with all mapped
+ instances merged into the session using :meth:`.Session.merge`. This
+ is an optimized method which will merge all mapped instances,
+ preserving the structure of the result rows and unmapped columns with
+ less method overhead than that of calling :meth:`.Session.merge`
+ explicitly for each value.
+
+ The structure of the results is determined based on the column list of
+ this :class:`_query.Query` - if these do not correspond,
+ unchecked errors
+ will occur.
+
+ The 'load' argument is the same as that of :meth:`.Session.merge`.
+
+ For an example of how :meth:`_query.Query.merge_result` is used, see
+ the source code for the example :ref:`examples_caching`, where
+ :meth:`_query.Query.merge_result` is used to efficiently restore state
+ from a cache back into a target :class:`.Session`.
+
+ """
+
+ return loading.merge_result(self, iterator, load)
+
+ def exists(self) -> Exists:
+ """A convenience method that turns a query into an EXISTS subquery
+ of the form EXISTS (SELECT 1 FROM ... WHERE ...).
+
+ e.g.::
+
+ q = session.query(User).filter(User.name == 'fred')
+ session.query(q.exists())
+
+ Producing SQL similar to::
+
+ SELECT EXISTS (
+ SELECT 1 FROM users WHERE users.name = :name_1
+ ) AS anon_1
+
+ The EXISTS construct is usually used in the WHERE clause::
+
+ session.query(User.id).filter(q.exists()).scalar()
+
+ Note that some databases such as SQL Server don't allow an
+ EXISTS expression to be present in the columns clause of a
+ SELECT. To select a simple boolean value based on the exists
+ as a WHERE, use :func:`.literal`::
+
+ from sqlalchemy import literal
+
+ session.query(literal(True)).filter(q.exists()).scalar()
+
+ .. seealso::
+
+ :meth:`_sql.Select.exists` - v2 comparable method.
+
+ """
+
+ # .add_columns() for the case that we are a query().select_from(X),
+ # so that ".statement" can be produced (#2995) but also without
+ # omitting the FROM clause from a query(X) (#2818);
+ # .with_only_columns() after we have a core select() so that
+ # we get just "SELECT 1" without any entities.
+
+ inner = (
+ self.enable_eagerloads(False)
+ .add_columns(sql.literal_column("1"))
+ .set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL)
+ ._get_select_statement_only()
+ .with_only_columns(1)
+ )
+
+ ezero = self._entity_from_pre_ent_zero()
+ if ezero is not None:
+ inner = inner.select_from(ezero)
+
+ return sql.exists(inner)
+
+ def count(self) -> int:
+ r"""Return a count of rows this the SQL formed by this :class:`Query`
+ would return.
+
+ This generates the SQL for this Query as follows::
+
+ SELECT count(1) AS count_1 FROM (
+ SELECT <rest of query follows...>
+ ) AS anon_1
+
+ The above SQL returns a single row, which is the aggregate value
+ of the count function; the :meth:`_query.Query.count`
+ method then returns
+ that single integer value.
+
+ .. warning::
+
+ It is important to note that the value returned by
+ count() is **not the same as the number of ORM objects that this
+ Query would return from a method such as the .all() method**.
+ The :class:`_query.Query` object,
+ when asked to return full entities,
+ will **deduplicate entries based on primary key**, meaning if the
+ same primary key value would appear in the results more than once,
+ only one object of that primary key would be present. This does
+ not apply to a query that is against individual columns.
+
+ .. seealso::
+
+ :ref:`faq_query_deduplicating`
+
+ For fine grained control over specific columns to count, to skip the
+ usage of a subquery or otherwise control of the FROM clause, or to use
+ other aggregate functions, use :attr:`~sqlalchemy.sql.expression.func`
+ expressions in conjunction with :meth:`~.Session.query`, i.e.::
+
+ from sqlalchemy import func
+
+ # count User records, without
+ # using a subquery.
+ session.query(func.count(User.id))
+
+ # return count of user "id" grouped
+ # by "name"
+ session.query(func.count(User.id)).\
+ group_by(User.name)
+
+ from sqlalchemy import distinct
+
+ # count distinct "name" values
+ session.query(func.count(distinct(User.name)))
+
+ .. seealso::
+
+ :ref:`migration_20_query_usage`
+
+ """
+ col = sql.func.count(sql.literal_column("*"))
+ return ( # type: ignore
+ self._legacy_from_self(col).enable_eagerloads(False).scalar()
+ )
+
+ def delete(
+ self, synchronize_session: SynchronizeSessionArgument = "auto"
+ ) -> int:
+ r"""Perform a DELETE with an arbitrary WHERE clause.
+
+ Deletes rows matched by this query from the database.
+
+ E.g.::
+
+ sess.query(User).filter(User.age == 25).\
+ delete(synchronize_session=False)
+
+ sess.query(User).filter(User.age == 25).\
+ delete(synchronize_session='evaluate')
+
+ .. warning::
+
+ See the section :ref:`orm_expression_update_delete` for important
+ caveats and warnings, including limitations when using bulk UPDATE
+ and DELETE with mapper inheritance configurations.
+
+ :param synchronize_session: chooses the strategy to update the
+ attributes on objects in the session. See the section
+ :ref:`orm_expression_update_delete` for a discussion of these
+ strategies.
+
+ :return: the count of rows matched as returned by the database's
+ "row count" feature.
+
+ .. seealso::
+
+ :ref:`orm_expression_update_delete`
+
+ """
+
+ bulk_del = BulkDelete(self)
+ if self.dispatch.before_compile_delete:
+ for fn in self.dispatch.before_compile_delete:
+ new_query = fn(bulk_del.query, bulk_del)
+ if new_query is not None:
+ bulk_del.query = new_query
+
+ self = bulk_del.query
+
+ delete_ = sql.delete(*self._raw_columns) # type: ignore
+ delete_._where_criteria = self._where_criteria
+ result: CursorResult[Any] = self.session.execute(
+ delete_,
+ self._params,
+ execution_options=self._execution_options.union(
+ {"synchronize_session": synchronize_session}
+ ),
+ )
+ bulk_del.result = result # type: ignore
+ self.session.dispatch.after_bulk_delete(bulk_del)
+ result.close()
+
+ return result.rowcount
+
+ def update(
+ self,
+ values: Dict[_DMLColumnArgument, Any],
+ synchronize_session: SynchronizeSessionArgument = "auto",
+ update_args: Optional[Dict[Any, Any]] = None,
+ ) -> int:
+ r"""Perform an UPDATE with an arbitrary WHERE clause.
+
+ Updates rows matched by this query in the database.
+
+ E.g.::
+
+ sess.query(User).filter(User.age == 25).\
+ update({User.age: User.age - 10}, synchronize_session=False)
+
+ sess.query(User).filter(User.age == 25).\
+ update({"age": User.age - 10}, synchronize_session='evaluate')
+
+ .. warning::
+
+ See the section :ref:`orm_expression_update_delete` for important
+ caveats and warnings, including limitations when using arbitrary
+ 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
+ values or sql expressions as values. If :ref:`parameter-ordered
+ mode <tutorial_parameter_ordered_updates>` is desired, the values can
+ be passed as a list of 2-tuples; this requires that the
+ :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`
+ flag is passed to the :paramref:`.Query.update.update_args` dictionary
+ as well.
+
+ :param synchronize_session: chooses the strategy to update the
+ attributes on objects in the session. See the section
+ :ref:`orm_expression_update_delete` for a discussion of these
+ strategies.
+
+ :param update_args: Optional dictionary, if present will be passed
+ to the underlying :func:`_expression.update`
+ construct as the ``**kw`` for
+ the object. May be used to pass dialect-specific arguments such
+ as ``mysql_limit``, as well as other special arguments such as
+ :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`.
+
+ :return: the count of rows matched as returned by the database's
+ "row count" feature.
+
+
+ .. seealso::
+
+ :ref:`orm_expression_update_delete`
+
+ """
+
+ update_args = update_args or {}
+
+ bulk_ud = BulkUpdate(self, values, update_args)
+
+ if self.dispatch.before_compile_update:
+ for fn in self.dispatch.before_compile_update:
+ new_query = fn(bulk_ud.query, bulk_ud)
+ if new_query is not None:
+ bulk_ud.query = new_query
+ self = bulk_ud.query
+
+ upd = sql.update(*self._raw_columns) # type: ignore
+
+ ppo = update_args.pop("preserve_parameter_order", False)
+ if ppo:
+ upd = upd.ordered_values(*values) # type: ignore
+ else:
+ upd = upd.values(values)
+ if update_args:
+ upd = upd.with_dialect_options(**update_args)
+
+ upd._where_criteria = self._where_criteria
+ result: CursorResult[Any] = self.session.execute(
+ upd,
+ self._params,
+ execution_options=self._execution_options.union(
+ {"synchronize_session": synchronize_session}
+ ),
+ )
+ bulk_ud.result = result # type: ignore
+ self.session.dispatch.after_bulk_update(bulk_ud)
+ result.close()
+ return result.rowcount
+
+ def _compile_state(
+ self, for_statement: bool = False, **kw: Any
+ ) -> ORMCompileState:
+ """Create an out-of-compiler ORMCompileState object.
+
+ The ORMCompileState object is normally created directly as a result
+ of the SQLCompiler.process() method being handed a Select()
+ or FromStatement() object that uses the "orm" plugin. This method
+ provides a means of creating this ORMCompileState object directly
+ without using the compiler.
+
+ This method is used only for deprecated cases, which include
+ the .from_self() method for a Query that has multiple levels
+ of .from_self() in use, as well as the instances() method. It is
+ also used within the test suite to generate ORMCompileState objects
+ for test purposes.
+
+ """
+
+ stmt = self._statement_20(for_statement=for_statement, **kw)
+ assert for_statement == stmt._compile_options._for_statement
+
+ # this chooses between ORMFromStatementCompileState and
+ # ORMSelectCompileState. We could also base this on
+ # query._statement is not None as we have the ORM Query here
+ # however this is the more general path.
+ compile_state_cls = cast(
+ ORMCompileState,
+ ORMCompileState._get_plugin_class_for_plugin(stmt, "orm"),
+ )
+
+ return compile_state_cls.create_for_statement(stmt, None)
+
+ def _compile_context(self, for_statement: bool = False) -> QueryContext:
+ compile_state = self._compile_state(for_statement=for_statement)
+ context = QueryContext(
+ compile_state,
+ compile_state.statement,
+ self._params,
+ self.session,
+ self.load_options,
+ )
+
+ return context
+
+
+class AliasOption(interfaces.LoaderOption):
+ inherit_cache = False
+
+ @util.deprecated(
+ "1.4",
+ "The :class:`.AliasOption` object is not necessary "
+ "for entities to be matched up to a query that is established "
+ "via :meth:`.Query.from_statement` and now does nothing.",
+ )
+ def __init__(self, alias: Union[Alias, Subquery]):
+ r"""Return a :class:`.MapperOption` that will indicate to the
+ :class:`_query.Query`
+ that the main table has been aliased.
+
+ """
+
+ def process_compile_state(self, compile_state: ORMCompileState) -> None:
+ pass
+
+
+class BulkUD:
+ """State used for the orm.Query version of update() / delete().
+
+ This object is now specific to Query only.
+
+ """
+
+ def __init__(self, query: Query[Any]):
+ self.query = query.enable_eagerloads(False)
+ self._validate_query_state()
+ self.mapper = self.query._entity_from_pre_ent_zero()
+
+ def _validate_query_state(self) -> None:
+ for attr, methname, notset, op in (
+ ("_limit_clause", "limit()", None, operator.is_),
+ ("_offset_clause", "offset()", None, operator.is_),
+ ("_order_by_clauses", "order_by()", (), operator.eq),
+ ("_group_by_clauses", "group_by()", (), operator.eq),
+ ("_distinct", "distinct()", False, operator.is_),
+ (
+ "_from_obj",
+ "join(), outerjoin(), select_from(), or from_self()",
+ (),
+ operator.eq,
+ ),
+ (
+ "_setup_joins",
+ "join(), outerjoin(), select_from(), or from_self()",
+ (),
+ operator.eq,
+ ),
+ ):
+ if not op(getattr(self.query, attr), notset):
+ raise sa_exc.InvalidRequestError(
+ "Can't call Query.update() or Query.delete() "
+ "when %s has been called" % (methname,)
+ )
+
+ @property
+ def session(self) -> Session:
+ return self.query.session
+
+
+class BulkUpdate(BulkUD):
+ """BulkUD which handles UPDATEs."""
+
+ def __init__(
+ self,
+ query: Query[Any],
+ values: Dict[_DMLColumnArgument, Any],
+ update_kwargs: Optional[Dict[Any, Any]],
+ ):
+ super().__init__(query)
+ self.values = values
+ self.update_kwargs = update_kwargs
+
+
+class BulkDelete(BulkUD):
+ """BulkUD which handles DELETEs."""
+
+
+class RowReturningQuery(Query[Row[_TP]]):
+ if TYPE_CHECKING:
+
+ def tuples(self) -> Query[_TP]: # type: ignore
+ ...
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py
new file mode 100644
index 0000000..b5e33ff
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py
@@ -0,0 +1,3500 @@
+# orm/relationships.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""Heuristics related to join conditions as used in
+:func:`_orm.relationship`.
+
+Provides the :class:`.JoinCondition` object, which encapsulates
+SQL annotation and aliasing behavior focused on the `primaryjoin`
+and `secondaryjoin` aspects of :func:`_orm.relationship`.
+
+"""
+from __future__ import annotations
+
+import collections
+from collections import abc
+import dataclasses
+import inspect as _py_inspect
+import itertools
+import re
+import typing
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Collection
+from typing import Dict
+from typing import FrozenSet
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import NamedTuple
+from typing import NoReturn
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import attributes
+from . import strategy_options
+from ._typing import insp_is_aliased_class
+from ._typing import is_has_collection_adapter
+from .base import _DeclarativeMapped
+from .base import _is_mapped_class
+from .base import class_mapper
+from .base import DynamicMapped
+from .base import LoaderCallableStatus
+from .base import PassiveFlag
+from .base import state_str
+from .base import WriteOnlyMapped
+from .interfaces import _AttributeOptions
+from .interfaces import _IntrospectsAnnotations
+from .interfaces import MANYTOMANY
+from .interfaces import MANYTOONE
+from .interfaces import ONETOMANY
+from .interfaces import PropComparator
+from .interfaces import RelationshipDirection
+from .interfaces import StrategizedProperty
+from .util import _orm_annotate
+from .util import _orm_deannotate
+from .util import CascadeOptions
+from .. import exc as sa_exc
+from .. import Exists
+from .. import log
+from .. import schema
+from .. import sql
+from .. import util
+from ..inspection import inspect
+from ..sql import coercions
+from ..sql import expression
+from ..sql import operators
+from ..sql import roles
+from ..sql import visitors
+from ..sql._typing import _ColumnExpressionArgument
+from ..sql._typing import _HasClauseElement
+from ..sql.annotation import _safe_annotate
+from ..sql.elements import ColumnClause
+from ..sql.elements import ColumnElement
+from ..sql.util import _deep_annotate
+from ..sql.util import _deep_deannotate
+from ..sql.util import _shallow_annotate
+from ..sql.util import adapt_criterion_to_null
+from ..sql.util import ClauseAdapter
+from ..sql.util import join_condition
+from ..sql.util import selectables_overlap
+from ..sql.util import visit_binary_product
+from ..util.typing import de_optionalize_union_types
+from ..util.typing import Literal
+from ..util.typing import resolve_name_to_real_class_name
+
+if typing.TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _ExternalEntityType
+ from ._typing import _IdentityKeyType
+ from ._typing import _InstanceDict
+ from ._typing import _InternalEntityType
+ from ._typing import _O
+ from ._typing import _RegistryType
+ from .base import Mapped
+ from .clsregistry import _class_resolver
+ from .clsregistry import _ModNS
+ from .decl_base import _ClassScanMapperConfig
+ from .dependency import DependencyProcessor
+ from .mapper import Mapper
+ from .query import Query
+ from .session import Session
+ from .state import InstanceState
+ from .strategies import LazyLoader
+ from .util import AliasedClass
+ from .util import AliasedInsp
+ from ..sql._typing import _CoreAdapterProto
+ from ..sql._typing import _EquivalentColumnMap
+ from ..sql._typing import _InfoType
+ from ..sql.annotation import _AnnotationDict
+ from ..sql.annotation import SupportsAnnotations
+ from ..sql.elements import BinaryExpression
+ from ..sql.elements import BindParameter
+ from ..sql.elements import ClauseElement
+ from ..sql.schema import Table
+ from ..sql.selectable import FromClause
+ from ..util.typing import _AnnotationScanType
+ from ..util.typing import RODescriptorReference
+
+_T = TypeVar("_T", bound=Any)
+_T1 = TypeVar("_T1", bound=Any)
+_T2 = TypeVar("_T2", bound=Any)
+
+_PT = TypeVar("_PT", bound=Any)
+
+_PT2 = TypeVar("_PT2", bound=Any)
+
+
+_RelationshipArgumentType = Union[
+ str,
+ Type[_T],
+ Callable[[], Type[_T]],
+ "Mapper[_T]",
+ "AliasedClass[_T]",
+ Callable[[], "Mapper[_T]"],
+ Callable[[], "AliasedClass[_T]"],
+]
+
+_LazyLoadArgumentType = Literal[
+ "select",
+ "joined",
+ "selectin",
+ "subquery",
+ "raise",
+ "raise_on_sql",
+ "noload",
+ "immediate",
+ "write_only",
+ "dynamic",
+ True,
+ False,
+ None,
+]
+
+
+_RelationshipJoinConditionArgument = Union[
+ str, _ColumnExpressionArgument[bool]
+]
+_RelationshipSecondaryArgument = Union[
+ "FromClause", str, Callable[[], "FromClause"]
+]
+_ORMOrderByArgument = Union[
+ Literal[False],
+ str,
+ _ColumnExpressionArgument[Any],
+ Callable[[], _ColumnExpressionArgument[Any]],
+ Callable[[], Iterable[_ColumnExpressionArgument[Any]]],
+ Iterable[Union[str, _ColumnExpressionArgument[Any]]],
+]
+ORMBackrefArgument = Union[str, Tuple[str, Dict[str, Any]]]
+
+_ORMColCollectionElement = Union[
+ ColumnClause[Any],
+ _HasClauseElement[Any],
+ roles.DMLColumnRole,
+ "Mapped[Any]",
+]
+_ORMColCollectionArgument = Union[
+ str,
+ Sequence[_ORMColCollectionElement],
+ Callable[[], Sequence[_ORMColCollectionElement]],
+ Callable[[], _ORMColCollectionElement],
+ _ORMColCollectionElement,
+]
+
+
+_CEA = TypeVar("_CEA", bound=_ColumnExpressionArgument[Any])
+
+_CE = TypeVar("_CE", bound="ColumnElement[Any]")
+
+
+_ColumnPairIterable = Iterable[Tuple[ColumnElement[Any], ColumnElement[Any]]]
+
+_ColumnPairs = Sequence[Tuple[ColumnElement[Any], ColumnElement[Any]]]
+
+_MutableColumnPairs = List[Tuple[ColumnElement[Any], ColumnElement[Any]]]
+
+
+def remote(expr: _CEA) -> _CEA:
+ """Annotate a portion of a primaryjoin expression
+ with a 'remote' annotation.
+
+ See the section :ref:`relationship_custom_foreign` for a
+ description of use.
+
+ .. seealso::
+
+ :ref:`relationship_custom_foreign`
+
+ :func:`.foreign`
+
+ """
+ return _annotate_columns( # type: ignore
+ coercions.expect(roles.ColumnArgumentRole, expr), {"remote": True}
+ )
+
+
+def foreign(expr: _CEA) -> _CEA:
+ """Annotate a portion of a primaryjoin expression
+ with a 'foreign' annotation.
+
+ See the section :ref:`relationship_custom_foreign` for a
+ description of use.
+
+ .. seealso::
+
+ :ref:`relationship_custom_foreign`
+
+ :func:`.remote`
+
+ """
+
+ return _annotate_columns( # type: ignore
+ coercions.expect(roles.ColumnArgumentRole, expr), {"foreign": True}
+ )
+
+
+@dataclasses.dataclass
+class _RelationshipArg(Generic[_T1, _T2]):
+ """stores a user-defined parameter value that must be resolved and
+ parsed later at mapper configuration time.
+
+ """
+
+ __slots__ = "name", "argument", "resolved"
+ name: str
+ argument: _T1
+ resolved: Optional[_T2]
+
+ def _is_populated(self) -> bool:
+ return self.argument is not None
+
+ def _resolve_against_registry(
+ self, clsregistry_resolver: Callable[[str, bool], _class_resolver]
+ ) -> None:
+ attr_value = self.argument
+
+ if isinstance(attr_value, str):
+ self.resolved = clsregistry_resolver(
+ attr_value, self.name == "secondary"
+ )()
+ elif callable(attr_value) and not _is_mapped_class(attr_value):
+ self.resolved = attr_value()
+ else:
+ self.resolved = attr_value
+
+
+_RelationshipOrderByArg = Union[Literal[False], Tuple[ColumnElement[Any], ...]]
+
+
+class _RelationshipArgs(NamedTuple):
+ """stores user-passed parameters that are resolved at mapper configuration
+ time.
+
+ """
+
+ secondary: _RelationshipArg[
+ Optional[_RelationshipSecondaryArgument],
+ Optional[FromClause],
+ ]
+ primaryjoin: _RelationshipArg[
+ Optional[_RelationshipJoinConditionArgument],
+ Optional[ColumnElement[Any]],
+ ]
+ secondaryjoin: _RelationshipArg[
+ Optional[_RelationshipJoinConditionArgument],
+ Optional[ColumnElement[Any]],
+ ]
+ order_by: _RelationshipArg[_ORMOrderByArgument, _RelationshipOrderByArg]
+ foreign_keys: _RelationshipArg[
+ Optional[_ORMColCollectionArgument], Set[ColumnElement[Any]]
+ ]
+ remote_side: _RelationshipArg[
+ Optional[_ORMColCollectionArgument], Set[ColumnElement[Any]]
+ ]
+
+
+@log.class_logger
+class RelationshipProperty(
+ _IntrospectsAnnotations, StrategizedProperty[_T], log.Identified
+):
+ """Describes an object property that holds a single item or list
+ of items that correspond to a related database table.
+
+ Public constructor is the :func:`_orm.relationship` function.
+
+ .. seealso::
+
+ :ref:`relationship_config_toplevel`
+
+ """
+
+ strategy_wildcard_key = strategy_options._RELATIONSHIP_TOKEN
+ inherit_cache = True
+ """:meta private:"""
+
+ _links_to_entity = True
+ _is_relationship = True
+
+ _overlaps: Sequence[str]
+
+ _lazy_strategy: LazyLoader
+
+ _persistence_only = dict(
+ passive_deletes=False,
+ passive_updates=True,
+ enable_typechecks=True,
+ active_history=False,
+ cascade_backrefs=False,
+ )
+
+ _dependency_processor: Optional[DependencyProcessor] = None
+
+ primaryjoin: ColumnElement[bool]
+ secondaryjoin: Optional[ColumnElement[bool]]
+ secondary: Optional[FromClause]
+ _join_condition: JoinCondition
+ order_by: _RelationshipOrderByArg
+
+ _user_defined_foreign_keys: Set[ColumnElement[Any]]
+ _calculated_foreign_keys: Set[ColumnElement[Any]]
+
+ remote_side: Set[ColumnElement[Any]]
+ local_columns: Set[ColumnElement[Any]]
+
+ synchronize_pairs: _ColumnPairs
+ secondary_synchronize_pairs: Optional[_ColumnPairs]
+
+ local_remote_pairs: Optional[_ColumnPairs]
+
+ direction: RelationshipDirection
+
+ _init_args: _RelationshipArgs
+
+ def __init__(
+ self,
+ argument: Optional[_RelationshipArgumentType[_T]] = None,
+ secondary: Optional[_RelationshipSecondaryArgument] = None,
+ *,
+ uselist: Optional[bool] = None,
+ collection_class: Optional[
+ Union[Type[Collection[Any]], Callable[[], Collection[Any]]]
+ ] = None,
+ primaryjoin: Optional[_RelationshipJoinConditionArgument] = None,
+ secondaryjoin: Optional[_RelationshipJoinConditionArgument] = None,
+ back_populates: Optional[str] = None,
+ order_by: _ORMOrderByArgument = False,
+ backref: Optional[ORMBackrefArgument] = None,
+ overlaps: Optional[str] = None,
+ post_update: bool = False,
+ cascade: str = "save-update, merge",
+ viewonly: bool = False,
+ attribute_options: Optional[_AttributeOptions] = None,
+ lazy: _LazyLoadArgumentType = "select",
+ passive_deletes: Union[Literal["all"], bool] = False,
+ passive_updates: bool = True,
+ active_history: bool = False,
+ enable_typechecks: bool = True,
+ foreign_keys: Optional[_ORMColCollectionArgument] = None,
+ remote_side: Optional[_ORMColCollectionArgument] = None,
+ join_depth: Optional[int] = None,
+ comparator_factory: Optional[
+ Type[RelationshipProperty.Comparator[Any]]
+ ] = None,
+ single_parent: bool = False,
+ innerjoin: bool = False,
+ distinct_target_key: Optional[bool] = None,
+ load_on_pending: bool = False,
+ query_class: Optional[Type[Query[Any]]] = None,
+ info: Optional[_InfoType] = None,
+ omit_join: Literal[None, False] = None,
+ sync_backref: Optional[bool] = None,
+ doc: Optional[str] = None,
+ bake_queries: Literal[True] = True,
+ cascade_backrefs: Literal[False] = False,
+ _local_remote_pairs: Optional[_ColumnPairs] = None,
+ _legacy_inactive_history_style: bool = False,
+ ):
+ super().__init__(attribute_options=attribute_options)
+
+ self.uselist = uselist
+ self.argument = argument
+
+ self._init_args = _RelationshipArgs(
+ _RelationshipArg("secondary", secondary, None),
+ _RelationshipArg("primaryjoin", primaryjoin, None),
+ _RelationshipArg("secondaryjoin", secondaryjoin, None),
+ _RelationshipArg("order_by", order_by, None),
+ _RelationshipArg("foreign_keys", foreign_keys, None),
+ _RelationshipArg("remote_side", remote_side, None),
+ )
+
+ self.post_update = post_update
+ self.viewonly = viewonly
+ if viewonly:
+ self._warn_for_persistence_only_flags(
+ passive_deletes=passive_deletes,
+ passive_updates=passive_updates,
+ enable_typechecks=enable_typechecks,
+ active_history=active_history,
+ cascade_backrefs=cascade_backrefs,
+ )
+ if viewonly and sync_backref:
+ raise sa_exc.ArgumentError(
+ "sync_backref and viewonly cannot both be True"
+ )
+ self.sync_backref = sync_backref
+ self.lazy = lazy
+ self.single_parent = single_parent
+ self.collection_class = collection_class
+ self.passive_deletes = passive_deletes
+
+ if cascade_backrefs:
+ raise sa_exc.ArgumentError(
+ "The 'cascade_backrefs' parameter passed to "
+ "relationship() may only be set to False."
+ )
+
+ self.passive_updates = passive_updates
+ self.enable_typechecks = enable_typechecks
+ self.query_class = query_class
+ self.innerjoin = innerjoin
+ self.distinct_target_key = distinct_target_key
+ self.doc = doc
+ self.active_history = active_history
+ self._legacy_inactive_history_style = _legacy_inactive_history_style
+
+ self.join_depth = join_depth
+ if omit_join:
+ util.warn(
+ "setting omit_join to True is not supported; selectin "
+ "loading of this relationship may not work correctly if this "
+ "flag is set explicitly. omit_join optimization is "
+ "automatically detected for conditions under which it is "
+ "supported."
+ )
+
+ self.omit_join = omit_join
+ self.local_remote_pairs = _local_remote_pairs
+ self.load_on_pending = load_on_pending
+ self.comparator_factory = (
+ comparator_factory or RelationshipProperty.Comparator
+ )
+ util.set_creation_order(self)
+
+ if info is not None:
+ self.info.update(info)
+
+ self.strategy_key = (("lazy", self.lazy),)
+
+ self._reverse_property: Set[RelationshipProperty[Any]] = set()
+
+ if overlaps:
+ self._overlaps = set(re.split(r"\s*,\s*", overlaps)) # type: ignore # noqa: E501
+ else:
+ self._overlaps = ()
+
+ # mypy ignoring the @property setter
+ self.cascade = cascade # type: ignore
+
+ self.back_populates = back_populates
+
+ if self.back_populates:
+ if backref:
+ raise sa_exc.ArgumentError(
+ "backref and back_populates keyword arguments "
+ "are mutually exclusive"
+ )
+ self.backref = None
+ else:
+ self.backref = backref
+
+ def _warn_for_persistence_only_flags(self, **kw: Any) -> None:
+ for k, v in kw.items():
+ if v != self._persistence_only[k]:
+ # we are warning here rather than warn deprecated as this is a
+ # configuration mistake, and Python shows regular warnings more
+ # aggressively than deprecation warnings by default. Unlike the
+ # case of setting viewonly with cascade, the settings being
+ # warned about here are not actively doing the wrong thing
+ # against viewonly=True, so it is not as urgent to have these
+ # raise an error.
+ util.warn(
+ "Setting %s on relationship() while also "
+ "setting viewonly=True does not make sense, as a "
+ "viewonly=True relationship does not perform persistence "
+ "operations. This configuration may raise an error "
+ "in a future release." % (k,)
+ )
+
+ def instrument_class(self, mapper: Mapper[Any]) -> None:
+ attributes.register_descriptor(
+ mapper.class_,
+ self.key,
+ comparator=self.comparator_factory(self, mapper),
+ parententity=mapper,
+ doc=self.doc,
+ )
+
+ class Comparator(util.MemoizedSlots, PropComparator[_PT]):
+ """Produce boolean, comparison, and other operators for
+ :class:`.RelationshipProperty` attributes.
+
+ See the documentation for :class:`.PropComparator` for a brief
+ overview of ORM level operator definition.
+
+ .. seealso::
+
+ :class:`.PropComparator`
+
+ :class:`.ColumnProperty.Comparator`
+
+ :class:`.ColumnOperators`
+
+ :ref:`types_operators`
+
+ :attr:`.TypeEngine.comparator_factory`
+
+ """
+
+ __slots__ = (
+ "entity",
+ "mapper",
+ "property",
+ "_of_type",
+ "_extra_criteria",
+ )
+
+ prop: RODescriptorReference[RelationshipProperty[_PT]]
+ _of_type: Optional[_EntityType[_PT]]
+
+ def __init__(
+ self,
+ prop: RelationshipProperty[_PT],
+ parentmapper: _InternalEntityType[Any],
+ adapt_to_entity: Optional[AliasedInsp[Any]] = None,
+ of_type: Optional[_EntityType[_PT]] = None,
+ extra_criteria: Tuple[ColumnElement[bool], ...] = (),
+ ):
+ """Construction of :class:`.RelationshipProperty.Comparator`
+ is internal to the ORM's attribute mechanics.
+
+ """
+ self.prop = prop
+ self._parententity = parentmapper
+ self._adapt_to_entity = adapt_to_entity
+ if of_type:
+ self._of_type = of_type
+ else:
+ self._of_type = None
+ self._extra_criteria = extra_criteria
+
+ def adapt_to_entity(
+ self, adapt_to_entity: AliasedInsp[Any]
+ ) -> RelationshipProperty.Comparator[Any]:
+ return self.__class__(
+ self.prop,
+ self._parententity,
+ adapt_to_entity=adapt_to_entity,
+ of_type=self._of_type,
+ )
+
+ entity: _InternalEntityType[_PT]
+ """The target entity referred to by this
+ :class:`.RelationshipProperty.Comparator`.
+
+ This is either a :class:`_orm.Mapper` or :class:`.AliasedInsp`
+ object.
+
+ This is the "target" or "remote" side of the
+ :func:`_orm.relationship`.
+
+ """
+
+ mapper: Mapper[_PT]
+ """The target :class:`_orm.Mapper` referred to by this
+ :class:`.RelationshipProperty.Comparator`.
+
+ This is the "target" or "remote" side of the
+ :func:`_orm.relationship`.
+
+ """
+
+ def _memoized_attr_entity(self) -> _InternalEntityType[_PT]:
+ if self._of_type:
+ return inspect(self._of_type) # type: ignore
+ else:
+ return self.prop.entity
+
+ def _memoized_attr_mapper(self) -> Mapper[_PT]:
+ return self.entity.mapper
+
+ def _source_selectable(self) -> FromClause:
+ if self._adapt_to_entity:
+ return self._adapt_to_entity.selectable
+ else:
+ return self.property.parent._with_polymorphic_selectable
+
+ def __clause_element__(self) -> ColumnElement[bool]:
+ adapt_from = self._source_selectable()
+ if self._of_type:
+ of_type_entity = inspect(self._of_type)
+ else:
+ of_type_entity = None
+
+ (
+ pj,
+ sj,
+ source,
+ dest,
+ secondary,
+ target_adapter,
+ ) = self.prop._create_joins(
+ source_selectable=adapt_from,
+ source_polymorphic=True,
+ of_type_entity=of_type_entity,
+ alias_secondary=True,
+ extra_criteria=self._extra_criteria,
+ )
+ if sj is not None:
+ return pj & sj
+ else:
+ return pj
+
+ def of_type(self, class_: _EntityType[Any]) -> PropComparator[_PT]:
+ r"""Redefine this object in terms of a polymorphic subclass.
+
+ See :meth:`.PropComparator.of_type` for an example.
+
+
+ """
+ return RelationshipProperty.Comparator(
+ self.prop,
+ self._parententity,
+ adapt_to_entity=self._adapt_to_entity,
+ of_type=class_,
+ extra_criteria=self._extra_criteria,
+ )
+
+ def and_(
+ self, *criteria: _ColumnExpressionArgument[bool]
+ ) -> PropComparator[Any]:
+ """Add AND criteria.
+
+ See :meth:`.PropComparator.and_` for an example.
+
+ .. versionadded:: 1.4
+
+ """
+ exprs = tuple(
+ coercions.expect(roles.WhereHavingRole, clause)
+ for clause in util.coerce_generator_arg(criteria)
+ )
+
+ return RelationshipProperty.Comparator(
+ self.prop,
+ self._parententity,
+ adapt_to_entity=self._adapt_to_entity,
+ of_type=self._of_type,
+ extra_criteria=self._extra_criteria + exprs,
+ )
+
+ def in_(self, other: Any) -> NoReturn:
+ """Produce an IN clause - this is not implemented
+ for :func:`_orm.relationship`-based attributes at this time.
+
+ """
+ raise NotImplementedError(
+ "in_() not yet supported for "
+ "relationships. For a simple "
+ "many-to-one, use in_() against "
+ "the set of foreign key values."
+ )
+
+ # https://github.com/python/mypy/issues/4266
+ __hash__ = None # type: ignore
+
+ def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
+ """Implement the ``==`` operator.
+
+ In a many-to-one context, such as::
+
+ MyClass.some_prop == <some object>
+
+ this will typically produce a
+ clause such as::
+
+ mytable.related_id == <some id>
+
+ Where ``<some id>`` is the primary key of the given
+ object.
+
+ The ``==`` operator provides partial functionality for non-
+ many-to-one comparisons:
+
+ * Comparisons against collections are not supported.
+ Use :meth:`~.Relationship.Comparator.contains`.
+ * Compared to a scalar one-to-many, will produce a
+ clause that compares the target columns in the parent to
+ the given target.
+ * Compared to a scalar many-to-many, an alias
+ of the association table will be rendered as
+ well, forming a natural join that is part of the
+ main body of the query. This will not work for
+ queries that go beyond simple AND conjunctions of
+ comparisons, such as those which use OR. Use
+ explicit joins, outerjoins, or
+ :meth:`~.Relationship.Comparator.has` for
+ more comprehensive non-many-to-one scalar
+ membership tests.
+ * Comparisons against ``None`` given in a one-to-many
+ or many-to-many context produce a NOT EXISTS clause.
+
+ """
+ if other is None or isinstance(other, expression.Null):
+ if self.property.direction in [ONETOMANY, MANYTOMANY]:
+ return ~self._criterion_exists()
+ else:
+ return _orm_annotate(
+ self.property._optimized_compare(
+ None, adapt_source=self.adapter
+ )
+ )
+ elif self.property.uselist:
+ raise sa_exc.InvalidRequestError(
+ "Can't compare a collection to an object or collection; "
+ "use contains() to test for membership."
+ )
+ else:
+ return _orm_annotate(
+ self.property._optimized_compare(
+ other, adapt_source=self.adapter
+ )
+ )
+
+ def _criterion_exists(
+ self,
+ criterion: Optional[_ColumnExpressionArgument[bool]] = None,
+ **kwargs: Any,
+ ) -> Exists:
+ where_criteria = (
+ coercions.expect(roles.WhereHavingRole, criterion)
+ if criterion is not None
+ else None
+ )
+
+ if getattr(self, "_of_type", None):
+ info: Optional[_InternalEntityType[Any]] = inspect(
+ self._of_type
+ )
+ assert info is not None
+ target_mapper, to_selectable, is_aliased_class = (
+ info.mapper,
+ info.selectable,
+ info.is_aliased_class,
+ )
+ if self.property._is_self_referential and not is_aliased_class:
+ to_selectable = to_selectable._anonymous_fromclause()
+
+ single_crit = target_mapper._single_table_criterion
+ if single_crit is not None:
+ if where_criteria is not None:
+ where_criteria = single_crit & where_criteria
+ else:
+ where_criteria = single_crit
+ else:
+ is_aliased_class = False
+ to_selectable = None
+
+ if self.adapter:
+ source_selectable = self._source_selectable()
+ else:
+ source_selectable = None
+
+ (
+ pj,
+ sj,
+ source,
+ dest,
+ secondary,
+ target_adapter,
+ ) = self.property._create_joins(
+ dest_selectable=to_selectable,
+ source_selectable=source_selectable,
+ )
+
+ for k in kwargs:
+ crit = getattr(self.property.mapper.class_, k) == kwargs[k]
+ if where_criteria is None:
+ where_criteria = crit
+ else:
+ where_criteria = where_criteria & crit
+
+ # annotate the *local* side of the join condition, in the case
+ # of pj + sj this is the full primaryjoin, in the case of just
+ # pj its the local side of the primaryjoin.
+ if sj is not None:
+ j = _orm_annotate(pj) & sj
+ else:
+ j = _orm_annotate(pj, exclude=self.property.remote_side)
+
+ if (
+ where_criteria is not None
+ and target_adapter
+ and not is_aliased_class
+ ):
+ # limit this adapter to annotated only?
+ where_criteria = target_adapter.traverse(where_criteria)
+
+ # only have the "joined left side" of what we
+ # return be subject to Query adaption. The right
+ # side of it is used for an exists() subquery and
+ # should not correlate or otherwise reach out
+ # to anything in the enclosing query.
+ if where_criteria is not None:
+ where_criteria = where_criteria._annotate(
+ {"no_replacement_traverse": True}
+ )
+
+ crit = j & sql.True_._ifnone(where_criteria)
+
+ if secondary is not None:
+ ex = (
+ sql.exists(1)
+ .where(crit)
+ .select_from(dest, secondary)
+ .correlate_except(dest, secondary)
+ )
+ else:
+ ex = (
+ sql.exists(1)
+ .where(crit)
+ .select_from(dest)
+ .correlate_except(dest)
+ )
+ return ex
+
+ def any(
+ self,
+ criterion: Optional[_ColumnExpressionArgument[bool]] = None,
+ **kwargs: Any,
+ ) -> ColumnElement[bool]:
+ """Produce an expression that tests a collection against
+ particular criterion, using EXISTS.
+
+ An expression like::
+
+ session.query(MyClass).filter(
+ MyClass.somereference.any(SomeRelated.x==2)
+ )
+
+
+ Will produce a query like::
+
+ SELECT * FROM my_table WHERE
+ EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id
+ AND related.x=2)
+
+ Because :meth:`~.Relationship.Comparator.any` uses
+ a correlated subquery, its performance is not nearly as
+ good when compared against large target tables as that of
+ using a join.
+
+ :meth:`~.Relationship.Comparator.any` is particularly
+ useful for testing for empty collections::
+
+ session.query(MyClass).filter(
+ ~MyClass.somereference.any()
+ )
+
+ will produce::
+
+ SELECT * FROM my_table WHERE
+ NOT (EXISTS (SELECT 1 FROM related WHERE
+ related.my_id=my_table.id))
+
+ :meth:`~.Relationship.Comparator.any` is only
+ valid for collections, i.e. a :func:`_orm.relationship`
+ that has ``uselist=True``. For scalar references,
+ use :meth:`~.Relationship.Comparator.has`.
+
+ """
+ if not self.property.uselist:
+ raise sa_exc.InvalidRequestError(
+ "'any()' not implemented for scalar "
+ "attributes. Use has()."
+ )
+
+ return self._criterion_exists(criterion, **kwargs)
+
+ def has(
+ self,
+ criterion: Optional[_ColumnExpressionArgument[bool]] = None,
+ **kwargs: Any,
+ ) -> ColumnElement[bool]:
+ """Produce an expression that tests a scalar reference against
+ particular criterion, using EXISTS.
+
+ An expression like::
+
+ session.query(MyClass).filter(
+ MyClass.somereference.has(SomeRelated.x==2)
+ )
+
+
+ Will produce a query like::
+
+ SELECT * FROM my_table WHERE
+ EXISTS (SELECT 1 FROM related WHERE
+ related.id==my_table.related_id AND related.x=2)
+
+ Because :meth:`~.Relationship.Comparator.has` uses
+ a correlated subquery, its performance is not nearly as
+ good when compared against large target tables as that of
+ using a join.
+
+ :meth:`~.Relationship.Comparator.has` is only
+ valid for scalar references, i.e. a :func:`_orm.relationship`
+ that has ``uselist=False``. For collection references,
+ use :meth:`~.Relationship.Comparator.any`.
+
+ """
+ if self.property.uselist:
+ raise sa_exc.InvalidRequestError(
+ "'has()' not implemented for collections. Use any()."
+ )
+ return self._criterion_exists(criterion, **kwargs)
+
+ def contains(
+ self, other: _ColumnExpressionArgument[Any], **kwargs: Any
+ ) -> ColumnElement[bool]:
+ """Return a simple expression that tests a collection for
+ containment of a particular item.
+
+ :meth:`~.Relationship.Comparator.contains` is
+ only valid for a collection, i.e. a
+ :func:`_orm.relationship` that implements
+ one-to-many or many-to-many with ``uselist=True``.
+
+ When used in a simple one-to-many context, an
+ expression like::
+
+ MyClass.contains(other)
+
+ Produces a clause like::
+
+ mytable.id == <some id>
+
+ Where ``<some id>`` is the value of the foreign key
+ attribute on ``other`` which refers to the primary
+ key of its parent object. From this it follows that
+ :meth:`~.Relationship.Comparator.contains` is
+ very useful when used with simple one-to-many
+ operations.
+
+ For many-to-many operations, the behavior of
+ :meth:`~.Relationship.Comparator.contains`
+ has more caveats. The association table will be
+ rendered in the statement, producing an "implicit"
+ join, that is, includes multiple tables in the FROM
+ clause which are equated in the WHERE clause::
+
+ query(MyClass).filter(MyClass.contains(other))
+
+ Produces a query like::
+
+ SELECT * FROM my_table, my_association_table AS
+ my_association_table_1 WHERE
+ my_table.id = my_association_table_1.parent_id
+ AND my_association_table_1.child_id = <some id>
+
+ Where ``<some id>`` would be the primary key of
+ ``other``. From the above, it is clear that
+ :meth:`~.Relationship.Comparator.contains`
+ will **not** work with many-to-many collections when
+ used in queries that move beyond simple AND
+ conjunctions, such as multiple
+ :meth:`~.Relationship.Comparator.contains`
+ expressions joined by OR. In such cases subqueries or
+ explicit "outer joins" will need to be used instead.
+ See :meth:`~.Relationship.Comparator.any` for
+ a less-performant alternative using EXISTS, or refer
+ to :meth:`_query.Query.outerjoin`
+ as well as :ref:`orm_queryguide_joins`
+ for more details on constructing outer joins.
+
+ kwargs may be ignored by this operator but are required for API
+ conformance.
+ """
+ if not self.prop.uselist:
+ raise sa_exc.InvalidRequestError(
+ "'contains' not implemented for scalar "
+ "attributes. Use =="
+ )
+
+ clause = self.prop._optimized_compare(
+ other, adapt_source=self.adapter
+ )
+
+ if self.prop.secondaryjoin is not None:
+ clause.negation_clause = self.__negated_contains_or_equals(
+ other
+ )
+
+ return clause
+
+ def __negated_contains_or_equals(
+ self, other: Any
+ ) -> ColumnElement[bool]:
+ if self.prop.direction == MANYTOONE:
+ state = attributes.instance_state(other)
+
+ def state_bindparam(
+ local_col: ColumnElement[Any],
+ state: InstanceState[Any],
+ remote_col: ColumnElement[Any],
+ ) -> BindParameter[Any]:
+ dict_ = state.dict
+ return sql.bindparam(
+ local_col.key,
+ type_=local_col.type,
+ unique=True,
+ callable_=self.prop._get_attr_w_warn_on_none(
+ self.prop.mapper, state, dict_, remote_col
+ ),
+ )
+
+ def adapt(col: _CE) -> _CE:
+ if self.adapter:
+ return self.adapter(col)
+ else:
+ return col
+
+ if self.property._use_get:
+ return sql.and_(
+ *[
+ sql.or_(
+ adapt(x)
+ != state_bindparam(adapt(x), state, y),
+ adapt(x) == None,
+ )
+ for (x, y) in self.property.local_remote_pairs
+ ]
+ )
+
+ criterion = sql.and_(
+ *[
+ x == y
+ for (x, y) in zip(
+ self.property.mapper.primary_key,
+ self.property.mapper.primary_key_from_instance(other),
+ )
+ ]
+ )
+
+ return ~self._criterion_exists(criterion)
+
+ def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
+ """Implement the ``!=`` operator.
+
+ In a many-to-one context, such as::
+
+ MyClass.some_prop != <some object>
+
+ This will typically produce a clause such as::
+
+ mytable.related_id != <some id>
+
+ Where ``<some id>`` is the primary key of the
+ given object.
+
+ The ``!=`` operator provides partial functionality for non-
+ many-to-one comparisons:
+
+ * Comparisons against collections are not supported.
+ Use
+ :meth:`~.Relationship.Comparator.contains`
+ in conjunction with :func:`_expression.not_`.
+ * Compared to a scalar one-to-many, will produce a
+ clause that compares the target columns in the parent to
+ the given target.
+ * Compared to a scalar many-to-many, an alias
+ of the association table will be rendered as
+ well, forming a natural join that is part of the
+ main body of the query. This will not work for
+ queries that go beyond simple AND conjunctions of
+ comparisons, such as those which use OR. Use
+ explicit joins, outerjoins, or
+ :meth:`~.Relationship.Comparator.has` in
+ conjunction with :func:`_expression.not_` for
+ more comprehensive non-many-to-one scalar
+ membership tests.
+ * Comparisons against ``None`` given in a one-to-many
+ or many-to-many context produce an EXISTS clause.
+
+ """
+ if other is None or isinstance(other, expression.Null):
+ if self.property.direction == MANYTOONE:
+ return _orm_annotate(
+ ~self.property._optimized_compare(
+ None, adapt_source=self.adapter
+ )
+ )
+
+ else:
+ return self._criterion_exists()
+ elif self.property.uselist:
+ raise sa_exc.InvalidRequestError(
+ "Can't compare a collection"
+ " to an object or collection; use "
+ "contains() to test for membership."
+ )
+ else:
+ return _orm_annotate(self.__negated_contains_or_equals(other))
+
+ def _memoized_attr_property(self) -> RelationshipProperty[_PT]:
+ self.prop.parent._check_configure()
+ return self.prop
+
+ def _with_parent(
+ self,
+ instance: object,
+ alias_secondary: bool = True,
+ from_entity: Optional[_EntityType[Any]] = None,
+ ) -> ColumnElement[bool]:
+ assert instance is not None
+ adapt_source: Optional[_CoreAdapterProto] = None
+ if from_entity is not None:
+ insp: Optional[_InternalEntityType[Any]] = inspect(from_entity)
+ assert insp is not None
+ if insp_is_aliased_class(insp):
+ adapt_source = insp._adapter.adapt_clause
+ return self._optimized_compare(
+ instance,
+ value_is_parent=True,
+ adapt_source=adapt_source,
+ alias_secondary=alias_secondary,
+ )
+
+ def _optimized_compare(
+ self,
+ state: Any,
+ value_is_parent: bool = False,
+ adapt_source: Optional[_CoreAdapterProto] = None,
+ alias_secondary: bool = True,
+ ) -> ColumnElement[bool]:
+ if state is not None:
+ try:
+ state = inspect(state)
+ except sa_exc.NoInspectionAvailable:
+ state = None
+
+ if state is None or not getattr(state, "is_instance", False):
+ raise sa_exc.ArgumentError(
+ "Mapped instance expected for relationship "
+ "comparison to object. Classes, queries and other "
+ "SQL elements are not accepted in this context; for "
+ "comparison with a subquery, "
+ "use %s.has(**criteria)." % self
+ )
+ reverse_direction = not value_is_parent
+
+ if state is None:
+ return self._lazy_none_clause(
+ reverse_direction, adapt_source=adapt_source
+ )
+
+ if not reverse_direction:
+ criterion, bind_to_col = (
+ self._lazy_strategy._lazywhere,
+ self._lazy_strategy._bind_to_col,
+ )
+ else:
+ criterion, bind_to_col = (
+ self._lazy_strategy._rev_lazywhere,
+ self._lazy_strategy._rev_bind_to_col,
+ )
+
+ if reverse_direction:
+ mapper = self.mapper
+ else:
+ mapper = self.parent
+
+ dict_ = attributes.instance_dict(state.obj())
+
+ def visit_bindparam(bindparam: BindParameter[Any]) -> None:
+ if bindparam._identifying_key in bind_to_col:
+ bindparam.callable = self._get_attr_w_warn_on_none(
+ mapper,
+ state,
+ dict_,
+ bind_to_col[bindparam._identifying_key],
+ )
+
+ if self.secondary is not None and alias_secondary:
+ criterion = ClauseAdapter(
+ self.secondary._anonymous_fromclause()
+ ).traverse(criterion)
+
+ criterion = visitors.cloned_traverse(
+ criterion, {}, {"bindparam": visit_bindparam}
+ )
+
+ if adapt_source:
+ criterion = adapt_source(criterion)
+ return criterion
+
+ def _get_attr_w_warn_on_none(
+ self,
+ mapper: Mapper[Any],
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ column: ColumnElement[Any],
+ ) -> Callable[[], Any]:
+ """Create the callable that is used in a many-to-one expression.
+
+ E.g.::
+
+ u1 = s.query(User).get(5)
+
+ expr = Address.user == u1
+
+ Above, the SQL should be "address.user_id = 5". The callable
+ returned by this method produces the value "5" based on the identity
+ of ``u1``.
+
+ """
+
+ # in this callable, we're trying to thread the needle through
+ # a wide variety of scenarios, including:
+ #
+ # * the object hasn't been flushed yet and there's no value for
+ # the attribute as of yet
+ #
+ # * the object hasn't been flushed yet but it has a user-defined
+ # value
+ #
+ # * the object has a value but it's expired and not locally present
+ #
+ # * the object has a value but it's expired and not locally present,
+ # and the object is also detached
+ #
+ # * The object hadn't been flushed yet, there was no value, but
+ # later, the object has been expired and detached, and *now*
+ # they're trying to evaluate it
+ #
+ # * the object had a value, but it was changed to a new value, and
+ # then expired
+ #
+ # * the object had a value, but it was changed to a new value, and
+ # then expired, then the object was detached
+ #
+ # * the object has a user-set value, but it's None and we don't do
+ # the comparison correctly for that so warn
+ #
+
+ prop = mapper.get_property_by_column(column)
+
+ # by invoking this method, InstanceState will track the last known
+ # value for this key each time the attribute is to be expired.
+ # this feature was added explicitly for use in this method.
+ state._track_last_known_value(prop.key)
+
+ lkv_fixed = state._last_known_values
+
+ def _go() -> Any:
+ assert lkv_fixed is not None
+ last_known = to_return = lkv_fixed[prop.key]
+ existing_is_available = (
+ last_known is not LoaderCallableStatus.NO_VALUE
+ )
+
+ # we support that the value may have changed. so here we
+ # try to get the most recent value including re-fetching.
+ # only if we can't get a value now due to detachment do we return
+ # the last known value
+ current_value = mapper._get_state_attr_by_column(
+ state,
+ dict_,
+ column,
+ passive=(
+ PassiveFlag.PASSIVE_OFF
+ if state.persistent
+ else PassiveFlag.PASSIVE_NO_FETCH ^ PassiveFlag.INIT_OK
+ ),
+ )
+
+ if current_value is LoaderCallableStatus.NEVER_SET:
+ if not existing_is_available:
+ raise sa_exc.InvalidRequestError(
+ "Can't resolve value for column %s on object "
+ "%s; no value has been set for this column"
+ % (column, state_str(state))
+ )
+ elif current_value is LoaderCallableStatus.PASSIVE_NO_RESULT:
+ if not existing_is_available:
+ raise sa_exc.InvalidRequestError(
+ "Can't resolve value for column %s on object "
+ "%s; the object is detached and the value was "
+ "expired" % (column, state_str(state))
+ )
+ else:
+ to_return = current_value
+ if to_return is None:
+ util.warn(
+ "Got None for value of column %s; this is unsupported "
+ "for a relationship comparison and will not "
+ "currently produce an IS comparison "
+ "(but may in a future release)" % column
+ )
+ return to_return
+
+ return _go
+
+ def _lazy_none_clause(
+ self,
+ reverse_direction: bool = False,
+ adapt_source: Optional[_CoreAdapterProto] = None,
+ ) -> ColumnElement[bool]:
+ if not reverse_direction:
+ criterion, bind_to_col = (
+ self._lazy_strategy._lazywhere,
+ self._lazy_strategy._bind_to_col,
+ )
+ else:
+ criterion, bind_to_col = (
+ self._lazy_strategy._rev_lazywhere,
+ self._lazy_strategy._rev_bind_to_col,
+ )
+
+ criterion = adapt_criterion_to_null(criterion, bind_to_col)
+
+ if adapt_source:
+ criterion = adapt_source(criterion)
+ return criterion
+
+ def __str__(self) -> str:
+ return str(self.parent.class_.__name__) + "." + self.key
+
+ def merge(
+ self,
+ session: Session,
+ source_state: InstanceState[Any],
+ source_dict: _InstanceDict,
+ dest_state: InstanceState[Any],
+ dest_dict: _InstanceDict,
+ load: bool,
+ _recursive: Dict[Any, object],
+ _resolve_conflict_map: Dict[_IdentityKeyType[Any], object],
+ ) -> None:
+ if load:
+ for r in self._reverse_property:
+ if (source_state, r) in _recursive:
+ return
+
+ if "merge" not in self._cascade:
+ return
+
+ if self.key not in source_dict:
+ return
+
+ if self.uselist:
+ impl = source_state.get_impl(self.key)
+
+ assert is_has_collection_adapter(impl)
+ instances_iterable = impl.get_collection(source_state, source_dict)
+
+ # if this is a CollectionAttributeImpl, then empty should
+ # be False, otherwise "self.key in source_dict" should not be
+ # True
+ assert not instances_iterable.empty if impl.collection else True
+
+ if load:
+ # for a full merge, pre-load the destination collection,
+ # so that individual _merge of each item pulls from identity
+ # map for those already present.
+ # also assumes CollectionAttributeImpl behavior of loading
+ # "old" list in any case
+ dest_state.get_impl(self.key).get(
+ dest_state, dest_dict, passive=PassiveFlag.PASSIVE_MERGE
+ )
+
+ dest_list = []
+ for current in instances_iterable:
+ current_state = attributes.instance_state(current)
+ current_dict = attributes.instance_dict(current)
+ _recursive[(current_state, self)] = True
+ obj = session._merge(
+ current_state,
+ current_dict,
+ load=load,
+ _recursive=_recursive,
+ _resolve_conflict_map=_resolve_conflict_map,
+ )
+ if obj is not None:
+ dest_list.append(obj)
+
+ if not load:
+ coll = attributes.init_state_collection(
+ dest_state, dest_dict, self.key
+ )
+ for c in dest_list:
+ coll.append_without_event(c)
+ else:
+ dest_impl = dest_state.get_impl(self.key)
+ assert is_has_collection_adapter(dest_impl)
+ dest_impl.set(
+ dest_state,
+ dest_dict,
+ dest_list,
+ _adapt=False,
+ passive=PassiveFlag.PASSIVE_MERGE,
+ )
+ else:
+ current = source_dict[self.key]
+ if current is not None:
+ current_state = attributes.instance_state(current)
+ current_dict = attributes.instance_dict(current)
+ _recursive[(current_state, self)] = True
+ obj = session._merge(
+ current_state,
+ current_dict,
+ load=load,
+ _recursive=_recursive,
+ _resolve_conflict_map=_resolve_conflict_map,
+ )
+ else:
+ obj = None
+
+ if not load:
+ dest_dict[self.key] = obj
+ else:
+ dest_state.get_impl(self.key).set(
+ dest_state, dest_dict, obj, None
+ )
+
+ def _value_as_iterable(
+ self,
+ state: InstanceState[_O],
+ dict_: _InstanceDict,
+ key: str,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ ) -> Sequence[Tuple[InstanceState[_O], _O]]:
+ """Return a list of tuples (state, obj) for the given
+ key.
+
+ returns an empty list if the value is None/empty/PASSIVE_NO_RESULT
+ """
+
+ impl = state.manager[key].impl
+ x = impl.get(state, dict_, passive=passive)
+ if x is LoaderCallableStatus.PASSIVE_NO_RESULT or x is None:
+ return []
+ elif is_has_collection_adapter(impl):
+ return [
+ (attributes.instance_state(o), o)
+ for o in impl.get_collection(state, dict_, x, passive=passive)
+ ]
+ else:
+ return [(attributes.instance_state(x), x)]
+
+ def cascade_iterator(
+ self,
+ type_: str,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ visited_states: Set[InstanceState[Any]],
+ halt_on: Optional[Callable[[InstanceState[Any]], bool]] = None,
+ ) -> Iterator[Tuple[Any, Mapper[Any], InstanceState[Any], _InstanceDict]]:
+ # assert type_ in self._cascade
+
+ # only actively lazy load on the 'delete' cascade
+ if type_ != "delete" or self.passive_deletes:
+ passive = PassiveFlag.PASSIVE_NO_INITIALIZE
+ else:
+ passive = PassiveFlag.PASSIVE_OFF | PassiveFlag.NO_RAISE
+
+ if type_ == "save-update":
+ tuples = state.manager[self.key].impl.get_all_pending(state, dict_)
+ else:
+ tuples = self._value_as_iterable(
+ state, dict_, self.key, passive=passive
+ )
+
+ skip_pending = (
+ type_ == "refresh-expire" and "delete-orphan" not in self._cascade
+ )
+
+ for instance_state, c in tuples:
+ if instance_state in visited_states:
+ continue
+
+ if c is None:
+ # would like to emit a warning here, but
+ # would not be consistent with collection.append(None)
+ # current behavior of silently skipping.
+ # see [ticket:2229]
+ continue
+
+ assert instance_state is not None
+ instance_dict = attributes.instance_dict(c)
+
+ if halt_on and halt_on(instance_state):
+ continue
+
+ if skip_pending and not instance_state.key:
+ continue
+
+ instance_mapper = instance_state.manager.mapper
+
+ if not instance_mapper.isa(self.mapper.class_manager.mapper):
+ raise AssertionError(
+ "Attribute '%s' on class '%s' "
+ "doesn't handle objects "
+ "of type '%s'"
+ % (self.key, self.parent.class_, c.__class__)
+ )
+
+ visited_states.add(instance_state)
+
+ yield c, instance_mapper, instance_state, instance_dict
+
+ @property
+ def _effective_sync_backref(self) -> bool:
+ if self.viewonly:
+ return False
+ else:
+ return self.sync_backref is not False
+
+ @staticmethod
+ def _check_sync_backref(
+ rel_a: RelationshipProperty[Any], rel_b: RelationshipProperty[Any]
+ ) -> None:
+ if rel_a.viewonly and rel_b.sync_backref:
+ raise sa_exc.InvalidRequestError(
+ "Relationship %s cannot specify sync_backref=True since %s "
+ "includes viewonly=True." % (rel_b, rel_a)
+ )
+ if (
+ rel_a.viewonly
+ and not rel_b.viewonly
+ and rel_b.sync_backref is not False
+ ):
+ rel_b.sync_backref = False
+
+ def _add_reverse_property(self, key: str) -> None:
+ other = self.mapper.get_property(key, _configure_mappers=False)
+ if not isinstance(other, RelationshipProperty):
+ raise sa_exc.InvalidRequestError(
+ "back_populates on relationship '%s' refers to attribute '%s' "
+ "that is not a relationship. The back_populates parameter "
+ "should refer to the name of a relationship on the target "
+ "class." % (self, other)
+ )
+ # viewonly and sync_backref cases
+ # 1. self.viewonly==True and other.sync_backref==True -> error
+ # 2. self.viewonly==True and other.viewonly==False and
+ # other.sync_backref==None -> warn sync_backref=False, set to False
+ self._check_sync_backref(self, other)
+ # 3. other.viewonly==True and self.sync_backref==True -> error
+ # 4. other.viewonly==True and self.viewonly==False and
+ # self.sync_backref==None -> warn sync_backref=False, set to False
+ self._check_sync_backref(other, self)
+
+ self._reverse_property.add(other)
+ other._reverse_property.add(self)
+
+ other._setup_entity()
+
+ if not other.mapper.common_parent(self.parent):
+ raise sa_exc.ArgumentError(
+ "reverse_property %r on "
+ "relationship %s references relationship %s, which "
+ "does not reference mapper %s"
+ % (key, self, other, self.parent)
+ )
+
+ if (
+ other._configure_started
+ and self.direction in (ONETOMANY, MANYTOONE)
+ and self.direction == other.direction
+ ):
+ raise sa_exc.ArgumentError(
+ "%s and back-reference %s are "
+ "both of the same direction %r. Did you mean to "
+ "set remote_side on the many-to-one side ?"
+ % (other, self, self.direction)
+ )
+
+ @util.memoized_property
+ def entity(self) -> _InternalEntityType[_T]:
+ """Return the target mapped entity, which is an inspect() of the
+ class or aliased class that is referenced by this
+ :class:`.RelationshipProperty`.
+
+ """
+ self.parent._check_configure()
+ return self.entity
+
+ @util.memoized_property
+ def mapper(self) -> Mapper[_T]:
+ """Return the targeted :class:`_orm.Mapper` for this
+ :class:`.RelationshipProperty`.
+
+ """
+ return self.entity.mapper
+
+ def do_init(self) -> None:
+ self._check_conflicts()
+ self._process_dependent_arguments()
+ self._setup_entity()
+ self._setup_registry_dependencies()
+ self._setup_join_conditions()
+ self._check_cascade_settings(self._cascade)
+ self._post_init()
+ self._generate_backref()
+ self._join_condition._warn_for_conflicting_sync_targets()
+ super().do_init()
+ self._lazy_strategy = cast(
+ "LazyLoader", self._get_strategy((("lazy", "select"),))
+ )
+
+ def _setup_registry_dependencies(self) -> None:
+ self.parent.mapper.registry._set_depends_on(
+ self.entity.mapper.registry
+ )
+
+ def _process_dependent_arguments(self) -> None:
+ """Convert incoming configuration arguments to their
+ proper form.
+
+ Callables are resolved, ORM annotations removed.
+
+ """
+
+ # accept callables for other attributes which may require
+ # deferred initialization. This technique is used
+ # by declarative "string configs" and some recipes.
+ init_args = self._init_args
+
+ for attr in (
+ "order_by",
+ "primaryjoin",
+ "secondaryjoin",
+ "secondary",
+ "foreign_keys",
+ "remote_side",
+ ):
+ rel_arg = getattr(init_args, attr)
+
+ rel_arg._resolve_against_registry(self._clsregistry_resolvers[1])
+
+ # remove "annotations" which are present if mapped class
+ # descriptors are used to create the join expression.
+ for attr in "primaryjoin", "secondaryjoin":
+ rel_arg = getattr(init_args, attr)
+ val = rel_arg.resolved
+ if val is not None:
+ rel_arg.resolved = _orm_deannotate(
+ coercions.expect(
+ roles.ColumnArgumentRole, val, argname=attr
+ )
+ )
+
+ secondary = init_args.secondary.resolved
+ if secondary is not None and _is_mapped_class(secondary):
+ raise sa_exc.ArgumentError(
+ "secondary argument %s passed to to relationship() %s must "
+ "be a Table object or other FROM clause; can't send a mapped "
+ "class directly as rows in 'secondary' are persisted "
+ "independently of a class that is mapped "
+ "to that same table." % (secondary, self)
+ )
+
+ # ensure expressions in self.order_by, foreign_keys,
+ # remote_side are all columns, not strings.
+ if (
+ init_args.order_by.resolved is not False
+ and init_args.order_by.resolved is not None
+ ):
+ self.order_by = tuple(
+ coercions.expect(
+ roles.ColumnArgumentRole, x, argname="order_by"
+ )
+ for x in util.to_list(init_args.order_by.resolved)
+ )
+ else:
+ self.order_by = False
+
+ self._user_defined_foreign_keys = util.column_set(
+ coercions.expect(
+ roles.ColumnArgumentRole, x, argname="foreign_keys"
+ )
+ for x in util.to_column_set(init_args.foreign_keys.resolved)
+ )
+
+ self.remote_side = util.column_set(
+ coercions.expect(
+ roles.ColumnArgumentRole, x, argname="remote_side"
+ )
+ for x in util.to_column_set(init_args.remote_side.resolved)
+ )
+
+ def declarative_scan(
+ self,
+ decl_scan: _ClassScanMapperConfig,
+ registry: _RegistryType,
+ cls: Type[Any],
+ originating_module: Optional[str],
+ key: str,
+ mapped_container: Optional[Type[Mapped[Any]]],
+ annotation: Optional[_AnnotationScanType],
+ extracted_mapped_annotation: Optional[_AnnotationScanType],
+ is_dataclass_field: bool,
+ ) -> None:
+ argument = extracted_mapped_annotation
+
+ if extracted_mapped_annotation is None:
+ if self.argument is None:
+ self._raise_for_required(key, cls)
+ else:
+ return
+
+ argument = extracted_mapped_annotation
+ assert originating_module is not None
+
+ if mapped_container is not None:
+ is_write_only = issubclass(mapped_container, WriteOnlyMapped)
+ is_dynamic = issubclass(mapped_container, DynamicMapped)
+ if is_write_only:
+ self.lazy = "write_only"
+ self.strategy_key = (("lazy", self.lazy),)
+ elif is_dynamic:
+ self.lazy = "dynamic"
+ self.strategy_key = (("lazy", self.lazy),)
+ else:
+ is_write_only = is_dynamic = False
+
+ argument = de_optionalize_union_types(argument)
+
+ if hasattr(argument, "__origin__"):
+ arg_origin = argument.__origin__
+ if isinstance(arg_origin, type) and issubclass(
+ arg_origin, abc.Collection
+ ):
+ if self.collection_class is None:
+ if _py_inspect.isabstract(arg_origin):
+ raise sa_exc.ArgumentError(
+ f"Collection annotation type {arg_origin} cannot "
+ "be instantiated; please provide an explicit "
+ "'collection_class' parameter "
+ "(e.g. list, set, etc.) to the "
+ "relationship() function to accompany this "
+ "annotation"
+ )
+
+ self.collection_class = arg_origin
+
+ elif not is_write_only and not is_dynamic:
+ self.uselist = False
+
+ if argument.__args__: # type: ignore
+ if isinstance(arg_origin, type) and issubclass(
+ arg_origin, typing.Mapping
+ ):
+ type_arg = argument.__args__[-1] # type: ignore
+ else:
+ type_arg = argument.__args__[0] # type: ignore
+ if hasattr(type_arg, "__forward_arg__"):
+ str_argument = type_arg.__forward_arg__
+
+ argument = resolve_name_to_real_class_name(
+ str_argument, originating_module
+ )
+ else:
+ argument = type_arg
+ else:
+ raise sa_exc.ArgumentError(
+ f"Generic alias {argument} requires an argument"
+ )
+ elif hasattr(argument, "__forward_arg__"):
+ argument = argument.__forward_arg__
+
+ argument = resolve_name_to_real_class_name(
+ argument, originating_module
+ )
+
+ if (
+ self.collection_class is None
+ and not is_write_only
+ and not is_dynamic
+ ):
+ self.uselist = False
+
+ # ticket #8759
+ # if a lead argument was given to relationship(), like
+ # `relationship("B")`, use that, don't replace it with class we
+ # found in the annotation. The declarative_scan() method call here is
+ # still useful, as we continue to derive collection type and do
+ # checking of the annotation in any case.
+ if self.argument is None:
+ self.argument = cast("_RelationshipArgumentType[_T]", argument)
+
+ @util.preload_module("sqlalchemy.orm.mapper")
+ def _setup_entity(self, __argument: Any = None) -> None:
+ if "entity" in self.__dict__:
+ return
+
+ mapperlib = util.preloaded.orm_mapper
+
+ if __argument:
+ argument = __argument
+ else:
+ argument = self.argument
+
+ resolved_argument: _ExternalEntityType[Any]
+
+ if isinstance(argument, str):
+ # we might want to cleanup clsregistry API to make this
+ # more straightforward
+ resolved_argument = cast(
+ "_ExternalEntityType[Any]",
+ self._clsregistry_resolve_name(argument)(),
+ )
+ elif callable(argument) and not isinstance(
+ argument, (type, mapperlib.Mapper)
+ ):
+ resolved_argument = argument()
+ else:
+ resolved_argument = argument
+
+ entity: _InternalEntityType[Any]
+
+ if isinstance(resolved_argument, type):
+ entity = class_mapper(resolved_argument, configure=False)
+ else:
+ try:
+ entity = inspect(resolved_argument)
+ except sa_exc.NoInspectionAvailable:
+ entity = None # type: ignore
+
+ if not hasattr(entity, "mapper"):
+ raise sa_exc.ArgumentError(
+ "relationship '%s' expects "
+ "a class or a mapper argument (received: %s)"
+ % (self.key, type(resolved_argument))
+ )
+
+ self.entity = entity
+ self.target = self.entity.persist_selectable
+
+ def _setup_join_conditions(self) -> None:
+ self._join_condition = jc = JoinCondition(
+ parent_persist_selectable=self.parent.persist_selectable,
+ child_persist_selectable=self.entity.persist_selectable,
+ parent_local_selectable=self.parent.local_table,
+ child_local_selectable=self.entity.local_table,
+ primaryjoin=self._init_args.primaryjoin.resolved,
+ secondary=self._init_args.secondary.resolved,
+ secondaryjoin=self._init_args.secondaryjoin.resolved,
+ parent_equivalents=self.parent._equivalent_columns,
+ child_equivalents=self.mapper._equivalent_columns,
+ consider_as_foreign_keys=self._user_defined_foreign_keys,
+ local_remote_pairs=self.local_remote_pairs,
+ remote_side=self.remote_side,
+ self_referential=self._is_self_referential,
+ prop=self,
+ support_sync=not self.viewonly,
+ can_be_synced_fn=self._columns_are_mapped,
+ )
+ self.primaryjoin = jc.primaryjoin
+ self.secondaryjoin = jc.secondaryjoin
+ self.secondary = jc.secondary
+ self.direction = jc.direction
+ self.local_remote_pairs = jc.local_remote_pairs
+ self.remote_side = jc.remote_columns
+ self.local_columns = jc.local_columns
+ self.synchronize_pairs = jc.synchronize_pairs
+ self._calculated_foreign_keys = jc.foreign_key_columns
+ self.secondary_synchronize_pairs = jc.secondary_synchronize_pairs
+
+ @property
+ def _clsregistry_resolve_arg(
+ self,
+ ) -> Callable[[str, bool], _class_resolver]:
+ return self._clsregistry_resolvers[1]
+
+ @property
+ def _clsregistry_resolve_name(
+ self,
+ ) -> Callable[[str], Callable[[], Union[Type[Any], Table, _ModNS]]]:
+ return self._clsregistry_resolvers[0]
+
+ @util.memoized_property
+ @util.preload_module("sqlalchemy.orm.clsregistry")
+ def _clsregistry_resolvers(
+ self,
+ ) -> Tuple[
+ Callable[[str], Callable[[], Union[Type[Any], Table, _ModNS]]],
+ Callable[[str, bool], _class_resolver],
+ ]:
+ _resolver = util.preloaded.orm_clsregistry._resolver
+
+ return _resolver(self.parent.class_, self)
+
+ def _check_conflicts(self) -> None:
+ """Test that this relationship is legal, warn about
+ inheritance conflicts."""
+ if self.parent.non_primary and not class_mapper(
+ self.parent.class_, configure=False
+ ).has_property(self.key):
+ raise sa_exc.ArgumentError(
+ "Attempting to assign a new "
+ "relationship '%s' to a non-primary mapper on "
+ "class '%s'. New relationships can only be added "
+ "to the primary mapper, i.e. the very first mapper "
+ "created for class '%s' "
+ % (
+ self.key,
+ self.parent.class_.__name__,
+ self.parent.class_.__name__,
+ )
+ )
+
+ @property
+ def cascade(self) -> CascadeOptions:
+ """Return the current cascade setting for this
+ :class:`.RelationshipProperty`.
+ """
+ return self._cascade
+
+ @cascade.setter
+ def cascade(self, cascade: Union[str, CascadeOptions]) -> None:
+ self._set_cascade(cascade)
+
+ def _set_cascade(self, cascade_arg: Union[str, CascadeOptions]) -> None:
+ cascade = CascadeOptions(cascade_arg)
+
+ if self.viewonly:
+ cascade = CascadeOptions(
+ cascade.intersection(CascadeOptions._viewonly_cascades)
+ )
+
+ if "mapper" in self.__dict__:
+ self._check_cascade_settings(cascade)
+ self._cascade = cascade
+
+ if self._dependency_processor:
+ self._dependency_processor.cascade = cascade
+
+ def _check_cascade_settings(self, cascade: CascadeOptions) -> None:
+ if (
+ cascade.delete_orphan
+ and not self.single_parent
+ and (self.direction is MANYTOMANY or self.direction is MANYTOONE)
+ ):
+ raise sa_exc.ArgumentError(
+ "For %(direction)s relationship %(rel)s, delete-orphan "
+ "cascade is normally "
+ 'configured only on the "one" side of a one-to-many '
+ "relationship, "
+ 'and not on the "many" side of a many-to-one or many-to-many '
+ "relationship. "
+ "To force this relationship to allow a particular "
+ '"%(relatedcls)s" object to be referenced by only '
+ 'a single "%(clsname)s" object at a time via the '
+ "%(rel)s relationship, which "
+ "would allow "
+ "delete-orphan cascade to take place in this direction, set "
+ "the single_parent=True flag."
+ % {
+ "rel": self,
+ "direction": (
+ "many-to-one"
+ if self.direction is MANYTOONE
+ else "many-to-many"
+ ),
+ "clsname": self.parent.class_.__name__,
+ "relatedcls": self.mapper.class_.__name__,
+ },
+ code="bbf0",
+ )
+
+ if self.passive_deletes == "all" and (
+ "delete" in cascade or "delete-orphan" in cascade
+ ):
+ raise sa_exc.ArgumentError(
+ "On %s, can't set passive_deletes='all' in conjunction "
+ "with 'delete' or 'delete-orphan' cascade" % self
+ )
+
+ if cascade.delete_orphan:
+ self.mapper.primary_mapper()._delete_orphans.append(
+ (self.key, self.parent.class_)
+ )
+
+ def _persists_for(self, mapper: Mapper[Any]) -> bool:
+ """Return True if this property will persist values on behalf
+ of the given mapper.
+
+ """
+
+ return (
+ self.key in mapper.relationships
+ and mapper.relationships[self.key] is self
+ )
+
+ def _columns_are_mapped(self, *cols: ColumnElement[Any]) -> bool:
+ """Return True if all columns in the given collection are
+ mapped by the tables referenced by this :class:`.RelationshipProperty`.
+
+ """
+
+ secondary = self._init_args.secondary.resolved
+ for c in cols:
+ if secondary is not None and secondary.c.contains_column(c):
+ continue
+ if not self.parent.persist_selectable.c.contains_column(
+ c
+ ) and not self.target.c.contains_column(c):
+ return False
+ return True
+
+ def _generate_backref(self) -> None:
+ """Interpret the 'backref' instruction to create a
+ :func:`_orm.relationship` complementary to this one."""
+
+ if self.parent.non_primary:
+ return
+ if self.backref is not None and not self.back_populates:
+ kwargs: Dict[str, Any]
+ if isinstance(self.backref, str):
+ backref_key, kwargs = self.backref, {}
+ else:
+ backref_key, kwargs = self.backref
+ mapper = self.mapper.primary_mapper()
+
+ if not mapper.concrete:
+ check = set(mapper.iterate_to_root()).union(
+ mapper.self_and_descendants
+ )
+ for m in check:
+ if m.has_property(backref_key) and not m.concrete:
+ raise sa_exc.ArgumentError(
+ "Error creating backref "
+ "'%s' on relationship '%s': property of that "
+ "name exists on mapper '%s'"
+ % (backref_key, self, m)
+ )
+
+ # determine primaryjoin/secondaryjoin for the
+ # backref. Use the one we had, so that
+ # a custom join doesn't have to be specified in
+ # both directions.
+ if self.secondary is not None:
+ # for many to many, just switch primaryjoin/
+ # secondaryjoin. use the annotated
+ # pj/sj on the _join_condition.
+ pj = kwargs.pop(
+ "primaryjoin",
+ self._join_condition.secondaryjoin_minus_local,
+ )
+ sj = kwargs.pop(
+ "secondaryjoin",
+ self._join_condition.primaryjoin_minus_local,
+ )
+ else:
+ pj = kwargs.pop(
+ "primaryjoin",
+ self._join_condition.primaryjoin_reverse_remote,
+ )
+ sj = kwargs.pop("secondaryjoin", None)
+ if sj:
+ raise sa_exc.InvalidRequestError(
+ "Can't assign 'secondaryjoin' on a backref "
+ "against a non-secondary relationship."
+ )
+
+ foreign_keys = kwargs.pop(
+ "foreign_keys", self._user_defined_foreign_keys
+ )
+ parent = self.parent.primary_mapper()
+ kwargs.setdefault("viewonly", self.viewonly)
+ kwargs.setdefault("post_update", self.post_update)
+ kwargs.setdefault("passive_updates", self.passive_updates)
+ kwargs.setdefault("sync_backref", self.sync_backref)
+ self.back_populates = backref_key
+ relationship = RelationshipProperty(
+ parent,
+ self.secondary,
+ primaryjoin=pj,
+ secondaryjoin=sj,
+ foreign_keys=foreign_keys,
+ back_populates=self.key,
+ **kwargs,
+ )
+ mapper._configure_property(
+ backref_key, relationship, warn_for_existing=True
+ )
+
+ if self.back_populates:
+ self._add_reverse_property(self.back_populates)
+
+ @util.preload_module("sqlalchemy.orm.dependency")
+ def _post_init(self) -> None:
+ dependency = util.preloaded.orm_dependency
+
+ if self.uselist is None:
+ self.uselist = self.direction is not MANYTOONE
+ if not self.viewonly:
+ self._dependency_processor = ( # type: ignore
+ dependency.DependencyProcessor.from_relationship
+ )(self)
+
+ @util.memoized_property
+ def _use_get(self) -> bool:
+ """memoize the 'use_get' attribute of this RelationshipLoader's
+ lazyloader."""
+
+ strategy = self._lazy_strategy
+ return strategy.use_get
+
+ @util.memoized_property
+ def _is_self_referential(self) -> bool:
+ return self.mapper.common_parent(self.parent)
+
+ def _create_joins(
+ self,
+ source_polymorphic: bool = False,
+ source_selectable: Optional[FromClause] = None,
+ dest_selectable: Optional[FromClause] = None,
+ of_type_entity: Optional[_InternalEntityType[Any]] = None,
+ alias_secondary: bool = False,
+ extra_criteria: Tuple[ColumnElement[bool], ...] = (),
+ ) -> Tuple[
+ ColumnElement[bool],
+ Optional[ColumnElement[bool]],
+ FromClause,
+ FromClause,
+ Optional[FromClause],
+ Optional[ClauseAdapter],
+ ]:
+ aliased = False
+
+ if alias_secondary and self.secondary is not None:
+ aliased = True
+
+ if source_selectable is None:
+ if source_polymorphic and self.parent.with_polymorphic:
+ source_selectable = self.parent._with_polymorphic_selectable
+
+ if of_type_entity:
+ dest_mapper = of_type_entity.mapper
+ if dest_selectable is None:
+ dest_selectable = of_type_entity.selectable
+ aliased = True
+ else:
+ dest_mapper = self.mapper
+
+ if dest_selectable is None:
+ dest_selectable = self.entity.selectable
+ if self.mapper.with_polymorphic:
+ aliased = True
+
+ if self._is_self_referential and source_selectable is None:
+ dest_selectable = dest_selectable._anonymous_fromclause()
+ aliased = True
+ elif (
+ dest_selectable is not self.mapper._with_polymorphic_selectable
+ or self.mapper.with_polymorphic
+ ):
+ aliased = True
+
+ single_crit = dest_mapper._single_table_criterion
+ aliased = aliased or (
+ source_selectable is not None
+ and (
+ source_selectable
+ is not self.parent._with_polymorphic_selectable
+ or source_selectable._is_subquery
+ )
+ )
+
+ (
+ primaryjoin,
+ secondaryjoin,
+ secondary,
+ target_adapter,
+ dest_selectable,
+ ) = self._join_condition.join_targets(
+ source_selectable,
+ dest_selectable,
+ aliased,
+ single_crit,
+ extra_criteria,
+ )
+ if source_selectable is None:
+ source_selectable = self.parent.local_table
+ if dest_selectable is None:
+ dest_selectable = self.entity.local_table
+ return (
+ primaryjoin,
+ secondaryjoin,
+ source_selectable,
+ dest_selectable,
+ secondary,
+ target_adapter,
+ )
+
+
+def _annotate_columns(element: _CE, annotations: _AnnotationDict) -> _CE:
+ def clone(elem: _CE) -> _CE:
+ if isinstance(elem, expression.ColumnClause):
+ elem = elem._annotate(annotations.copy()) # type: ignore
+ elem._copy_internals(clone=clone)
+ return elem
+
+ if element is not None:
+ element = clone(element)
+ clone = None # type: ignore # remove gc cycles
+ return element
+
+
+class JoinCondition:
+ primaryjoin_initial: Optional[ColumnElement[bool]]
+ primaryjoin: ColumnElement[bool]
+ secondaryjoin: Optional[ColumnElement[bool]]
+ secondary: Optional[FromClause]
+ prop: RelationshipProperty[Any]
+
+ synchronize_pairs: _ColumnPairs
+ secondary_synchronize_pairs: _ColumnPairs
+ direction: RelationshipDirection
+
+ parent_persist_selectable: FromClause
+ child_persist_selectable: FromClause
+ parent_local_selectable: FromClause
+ child_local_selectable: FromClause
+
+ _local_remote_pairs: Optional[_ColumnPairs]
+
+ def __init__(
+ self,
+ parent_persist_selectable: FromClause,
+ child_persist_selectable: FromClause,
+ parent_local_selectable: FromClause,
+ child_local_selectable: FromClause,
+ *,
+ primaryjoin: Optional[ColumnElement[bool]] = None,
+ secondary: Optional[FromClause] = None,
+ secondaryjoin: Optional[ColumnElement[bool]] = None,
+ parent_equivalents: Optional[_EquivalentColumnMap] = None,
+ child_equivalents: Optional[_EquivalentColumnMap] = None,
+ consider_as_foreign_keys: Any = None,
+ local_remote_pairs: Optional[_ColumnPairs] = None,
+ remote_side: Any = None,
+ self_referential: Any = False,
+ prop: RelationshipProperty[Any],
+ support_sync: bool = True,
+ can_be_synced_fn: Callable[..., bool] = lambda *c: True,
+ ):
+ self.parent_persist_selectable = parent_persist_selectable
+ self.parent_local_selectable = parent_local_selectable
+ self.child_persist_selectable = child_persist_selectable
+ self.child_local_selectable = child_local_selectable
+ self.parent_equivalents = parent_equivalents
+ self.child_equivalents = child_equivalents
+ self.primaryjoin_initial = primaryjoin
+ self.secondaryjoin = secondaryjoin
+ self.secondary = secondary
+ self.consider_as_foreign_keys = consider_as_foreign_keys
+ self._local_remote_pairs = local_remote_pairs
+ self._remote_side = remote_side
+ self.prop = prop
+ self.self_referential = self_referential
+ self.support_sync = support_sync
+ self.can_be_synced_fn = can_be_synced_fn
+
+ self._determine_joins()
+ assert self.primaryjoin is not None
+
+ self._sanitize_joins()
+ self._annotate_fks()
+ self._annotate_remote()
+ self._annotate_local()
+ self._annotate_parentmapper()
+ self._setup_pairs()
+ self._check_foreign_cols(self.primaryjoin, True)
+ if self.secondaryjoin is not None:
+ self._check_foreign_cols(self.secondaryjoin, False)
+ self._determine_direction()
+ self._check_remote_side()
+ self._log_joins()
+
+ def _log_joins(self) -> None:
+ log = self.prop.logger
+ log.info("%s setup primary join %s", self.prop, self.primaryjoin)
+ log.info("%s setup secondary join %s", self.prop, self.secondaryjoin)
+ log.info(
+ "%s synchronize pairs [%s]",
+ self.prop,
+ ",".join(
+ "(%s => %s)" % (l, r) for (l, r) in self.synchronize_pairs
+ ),
+ )
+ log.info(
+ "%s secondary synchronize pairs [%s]",
+ self.prop,
+ ",".join(
+ "(%s => %s)" % (l, r)
+ for (l, r) in self.secondary_synchronize_pairs or []
+ ),
+ )
+ log.info(
+ "%s local/remote pairs [%s]",
+ self.prop,
+ ",".join(
+ "(%s / %s)" % (l, r) for (l, r) in self.local_remote_pairs
+ ),
+ )
+ log.info(
+ "%s remote columns [%s]",
+ self.prop,
+ ",".join("%s" % col for col in self.remote_columns),
+ )
+ log.info(
+ "%s local columns [%s]",
+ self.prop,
+ ",".join("%s" % col for col in self.local_columns),
+ )
+ log.info("%s relationship direction %s", self.prop, self.direction)
+
+ def _sanitize_joins(self) -> None:
+ """remove the parententity annotation from our join conditions which
+ can leak in here based on some declarative patterns and maybe others.
+
+ "parentmapper" is relied upon both by the ORM evaluator as well as
+ the use case in _join_fixture_inh_selfref_w_entity
+ that relies upon it being present, see :ticket:`3364`.
+
+ """
+
+ self.primaryjoin = _deep_deannotate(
+ self.primaryjoin, values=("parententity", "proxy_key")
+ )
+ if self.secondaryjoin is not None:
+ self.secondaryjoin = _deep_deannotate(
+ self.secondaryjoin, values=("parententity", "proxy_key")
+ )
+
+ def _determine_joins(self) -> None:
+ """Determine the 'primaryjoin' and 'secondaryjoin' attributes,
+ if not passed to the constructor already.
+
+ This is based on analysis of the foreign key relationships
+ between the parent and target mapped selectables.
+
+ """
+ if self.secondaryjoin is not None and self.secondary is None:
+ raise sa_exc.ArgumentError(
+ "Property %s specified with secondary "
+ "join condition but "
+ "no secondary argument" % self.prop
+ )
+
+ # find a join between the given mapper's mapped table and
+ # the given table. will try the mapper's local table first
+ # for more specificity, then if not found will try the more
+ # general mapped table, which in the case of inheritance is
+ # a join.
+ try:
+ consider_as_foreign_keys = self.consider_as_foreign_keys or None
+ if self.secondary is not None:
+ if self.secondaryjoin is None:
+ self.secondaryjoin = join_condition(
+ self.child_persist_selectable,
+ self.secondary,
+ a_subset=self.child_local_selectable,
+ consider_as_foreign_keys=consider_as_foreign_keys,
+ )
+ if self.primaryjoin_initial is None:
+ self.primaryjoin = join_condition(
+ self.parent_persist_selectable,
+ self.secondary,
+ a_subset=self.parent_local_selectable,
+ consider_as_foreign_keys=consider_as_foreign_keys,
+ )
+ else:
+ self.primaryjoin = self.primaryjoin_initial
+ else:
+ if self.primaryjoin_initial is None:
+ self.primaryjoin = join_condition(
+ self.parent_persist_selectable,
+ self.child_persist_selectable,
+ a_subset=self.parent_local_selectable,
+ consider_as_foreign_keys=consider_as_foreign_keys,
+ )
+ else:
+ self.primaryjoin = self.primaryjoin_initial
+ except sa_exc.NoForeignKeysError as nfe:
+ if self.secondary is not None:
+ raise sa_exc.NoForeignKeysError(
+ "Could not determine join "
+ "condition between parent/child tables on "
+ "relationship %s - there are no foreign keys "
+ "linking these tables via secondary table '%s'. "
+ "Ensure that referencing columns are associated "
+ "with a ForeignKey or ForeignKeyConstraint, or "
+ "specify 'primaryjoin' and 'secondaryjoin' "
+ "expressions." % (self.prop, self.secondary)
+ ) from nfe
+ else:
+ raise sa_exc.NoForeignKeysError(
+ "Could not determine join "
+ "condition between parent/child tables on "
+ "relationship %s - there are no foreign keys "
+ "linking these tables. "
+ "Ensure that referencing columns are associated "
+ "with a ForeignKey or ForeignKeyConstraint, or "
+ "specify a 'primaryjoin' expression." % self.prop
+ ) from nfe
+ except sa_exc.AmbiguousForeignKeysError as afe:
+ if self.secondary is not None:
+ raise sa_exc.AmbiguousForeignKeysError(
+ "Could not determine join "
+ "condition between parent/child tables on "
+ "relationship %s - there are multiple foreign key "
+ "paths linking the tables via secondary table '%s'. "
+ "Specify the 'foreign_keys' "
+ "argument, providing a list of those columns which "
+ "should be counted as containing a foreign key "
+ "reference from the secondary table to each of the "
+ "parent and child tables." % (self.prop, self.secondary)
+ ) from afe
+ else:
+ raise sa_exc.AmbiguousForeignKeysError(
+ "Could not determine join "
+ "condition between parent/child tables on "
+ "relationship %s - there are multiple foreign key "
+ "paths linking the tables. Specify the "
+ "'foreign_keys' argument, providing a list of those "
+ "columns which should be counted as containing a "
+ "foreign key reference to the parent table." % self.prop
+ ) from afe
+
+ @property
+ def primaryjoin_minus_local(self) -> ColumnElement[bool]:
+ return _deep_deannotate(self.primaryjoin, values=("local", "remote"))
+
+ @property
+ def secondaryjoin_minus_local(self) -> ColumnElement[bool]:
+ assert self.secondaryjoin is not None
+ return _deep_deannotate(self.secondaryjoin, values=("local", "remote"))
+
+ @util.memoized_property
+ def primaryjoin_reverse_remote(self) -> ColumnElement[bool]:
+ """Return the primaryjoin condition suitable for the
+ "reverse" direction.
+
+ If the primaryjoin was delivered here with pre-existing
+ "remote" annotations, the local/remote annotations
+ are reversed. Otherwise, the local/remote annotations
+ are removed.
+
+ """
+ if self._has_remote_annotations:
+
+ def replace(element: _CE, **kw: Any) -> Optional[_CE]:
+ if "remote" in element._annotations:
+ v = dict(element._annotations)
+ del v["remote"]
+ v["local"] = True
+ return element._with_annotations(v)
+ elif "local" in element._annotations:
+ v = dict(element._annotations)
+ del v["local"]
+ v["remote"] = True
+ return element._with_annotations(v)
+
+ return None
+
+ return visitors.replacement_traverse(self.primaryjoin, {}, replace)
+ else:
+ if self._has_foreign_annotations:
+ # TODO: coverage
+ return _deep_deannotate(
+ self.primaryjoin, values=("local", "remote")
+ )
+ else:
+ return _deep_deannotate(self.primaryjoin)
+
+ def _has_annotation(self, clause: ClauseElement, annotation: str) -> bool:
+ for col in visitors.iterate(clause, {}):
+ if annotation in col._annotations:
+ return True
+ else:
+ return False
+
+ @util.memoized_property
+ def _has_foreign_annotations(self) -> bool:
+ return self._has_annotation(self.primaryjoin, "foreign")
+
+ @util.memoized_property
+ def _has_remote_annotations(self) -> bool:
+ return self._has_annotation(self.primaryjoin, "remote")
+
+ def _annotate_fks(self) -> None:
+ """Annotate the primaryjoin and secondaryjoin
+ structures with 'foreign' annotations marking columns
+ considered as foreign.
+
+ """
+ if self._has_foreign_annotations:
+ return
+
+ if self.consider_as_foreign_keys:
+ self._annotate_from_fk_list()
+ else:
+ self._annotate_present_fks()
+
+ def _annotate_from_fk_list(self) -> None:
+ def check_fk(element: _CE, **kw: Any) -> Optional[_CE]:
+ if element in self.consider_as_foreign_keys:
+ return element._annotate({"foreign": True})
+ return None
+
+ self.primaryjoin = visitors.replacement_traverse(
+ self.primaryjoin, {}, check_fk
+ )
+ if self.secondaryjoin is not None:
+ self.secondaryjoin = visitors.replacement_traverse(
+ self.secondaryjoin, {}, check_fk
+ )
+
+ def _annotate_present_fks(self) -> None:
+ if self.secondary is not None:
+ secondarycols = util.column_set(self.secondary.c)
+ else:
+ secondarycols = set()
+
+ def is_foreign(
+ a: ColumnElement[Any], b: ColumnElement[Any]
+ ) -> Optional[ColumnElement[Any]]:
+ if isinstance(a, schema.Column) and isinstance(b, schema.Column):
+ if a.references(b):
+ return a
+ elif b.references(a):
+ return b
+
+ if secondarycols:
+ if a in secondarycols and b not in secondarycols:
+ return a
+ elif b in secondarycols and a not in secondarycols:
+ return b
+
+ return None
+
+ def visit_binary(binary: BinaryExpression[Any]) -> None:
+ if not isinstance(
+ binary.left, sql.ColumnElement
+ ) or not isinstance(binary.right, sql.ColumnElement):
+ return
+
+ if (
+ "foreign" not in binary.left._annotations
+ and "foreign" not in binary.right._annotations
+ ):
+ col = is_foreign(binary.left, binary.right)
+ if col is not None:
+ if col.compare(binary.left):
+ binary.left = binary.left._annotate({"foreign": True})
+ elif col.compare(binary.right):
+ binary.right = binary.right._annotate(
+ {"foreign": True}
+ )
+
+ self.primaryjoin = visitors.cloned_traverse(
+ self.primaryjoin, {}, {"binary": visit_binary}
+ )
+ if self.secondaryjoin is not None:
+ self.secondaryjoin = visitors.cloned_traverse(
+ self.secondaryjoin, {}, {"binary": visit_binary}
+ )
+
+ def _refers_to_parent_table(self) -> bool:
+ """Return True if the join condition contains column
+ comparisons where both columns are in both tables.
+
+ """
+ pt = self.parent_persist_selectable
+ mt = self.child_persist_selectable
+ result = False
+
+ def visit_binary(binary: BinaryExpression[Any]) -> None:
+ nonlocal result
+ c, f = binary.left, binary.right
+ if (
+ isinstance(c, expression.ColumnClause)
+ and isinstance(f, expression.ColumnClause)
+ and pt.is_derived_from(c.table)
+ and pt.is_derived_from(f.table)
+ and mt.is_derived_from(c.table)
+ and mt.is_derived_from(f.table)
+ ):
+ result = True
+
+ visitors.traverse(self.primaryjoin, {}, {"binary": visit_binary})
+ return result
+
+ def _tables_overlap(self) -> bool:
+ """Return True if parent/child tables have some overlap."""
+
+ return selectables_overlap(
+ self.parent_persist_selectable, self.child_persist_selectable
+ )
+
+ def _annotate_remote(self) -> None:
+ """Annotate the primaryjoin and secondaryjoin
+ structures with 'remote' annotations marking columns
+ considered as part of the 'remote' side.
+
+ """
+ if self._has_remote_annotations:
+ return
+
+ if self.secondary is not None:
+ self._annotate_remote_secondary()
+ elif self._local_remote_pairs or self._remote_side:
+ self._annotate_remote_from_args()
+ elif self._refers_to_parent_table():
+ self._annotate_selfref(
+ lambda col: "foreign" in col._annotations, False
+ )
+ elif self._tables_overlap():
+ self._annotate_remote_with_overlap()
+ else:
+ self._annotate_remote_distinct_selectables()
+
+ def _annotate_remote_secondary(self) -> None:
+ """annotate 'remote' in primaryjoin, secondaryjoin
+ when 'secondary' is present.
+
+ """
+
+ assert self.secondary is not None
+ fixed_secondary = self.secondary
+
+ def repl(element: _CE, **kw: Any) -> Optional[_CE]:
+ if fixed_secondary.c.contains_column(element):
+ return element._annotate({"remote": True})
+ return None
+
+ self.primaryjoin = visitors.replacement_traverse(
+ self.primaryjoin, {}, repl
+ )
+
+ assert self.secondaryjoin is not None
+ self.secondaryjoin = visitors.replacement_traverse(
+ self.secondaryjoin, {}, repl
+ )
+
+ def _annotate_selfref(
+ self, fn: Callable[[ColumnElement[Any]], bool], remote_side_given: bool
+ ) -> None:
+ """annotate 'remote' in primaryjoin, secondaryjoin
+ when the relationship is detected as self-referential.
+
+ """
+
+ def visit_binary(binary: BinaryExpression[Any]) -> None:
+ equated = binary.left.compare(binary.right)
+ if isinstance(binary.left, expression.ColumnClause) and isinstance(
+ binary.right, expression.ColumnClause
+ ):
+ # assume one to many - FKs are "remote"
+ if fn(binary.left):
+ binary.left = binary.left._annotate({"remote": True})
+ if fn(binary.right) and not equated:
+ binary.right = binary.right._annotate({"remote": True})
+ elif not remote_side_given:
+ self._warn_non_column_elements()
+
+ self.primaryjoin = visitors.cloned_traverse(
+ self.primaryjoin, {}, {"binary": visit_binary}
+ )
+
+ def _annotate_remote_from_args(self) -> None:
+ """annotate 'remote' in primaryjoin, secondaryjoin
+ when the 'remote_side' or '_local_remote_pairs'
+ arguments are used.
+
+ """
+ if self._local_remote_pairs:
+ if self._remote_side:
+ raise sa_exc.ArgumentError(
+ "remote_side argument is redundant "
+ "against more detailed _local_remote_side "
+ "argument."
+ )
+
+ remote_side = [r for (l, r) in self._local_remote_pairs]
+ else:
+ remote_side = self._remote_side
+
+ if self._refers_to_parent_table():
+ self._annotate_selfref(lambda col: col in remote_side, True)
+ else:
+
+ def repl(element: _CE, **kw: Any) -> Optional[_CE]:
+ # use set() to avoid generating ``__eq__()`` expressions
+ # against each element
+ if element in set(remote_side):
+ return element._annotate({"remote": True})
+ return None
+
+ self.primaryjoin = visitors.replacement_traverse(
+ self.primaryjoin, {}, repl
+ )
+
+ def _annotate_remote_with_overlap(self) -> None:
+ """annotate 'remote' in primaryjoin, secondaryjoin
+ when the parent/child tables have some set of
+ tables in common, though is not a fully self-referential
+ relationship.
+
+ """
+
+ def visit_binary(binary: BinaryExpression[Any]) -> None:
+ binary.left, binary.right = proc_left_right(
+ binary.left, binary.right
+ )
+ binary.right, binary.left = proc_left_right(
+ binary.right, binary.left
+ )
+
+ check_entities = (
+ self.prop is not None and self.prop.mapper is not self.prop.parent
+ )
+
+ def proc_left_right(
+ left: ColumnElement[Any], right: ColumnElement[Any]
+ ) -> Tuple[ColumnElement[Any], ColumnElement[Any]]:
+ if isinstance(left, expression.ColumnClause) and isinstance(
+ right, expression.ColumnClause
+ ):
+ if self.child_persist_selectable.c.contains_column(
+ right
+ ) and self.parent_persist_selectable.c.contains_column(left):
+ right = right._annotate({"remote": True})
+ elif (
+ check_entities
+ and right._annotations.get("parentmapper") is self.prop.mapper
+ ):
+ right = right._annotate({"remote": True})
+ elif (
+ check_entities
+ and left._annotations.get("parentmapper") is self.prop.mapper
+ ):
+ left = left._annotate({"remote": True})
+ else:
+ self._warn_non_column_elements()
+
+ return left, right
+
+ self.primaryjoin = visitors.cloned_traverse(
+ self.primaryjoin, {}, {"binary": visit_binary}
+ )
+
+ def _annotate_remote_distinct_selectables(self) -> None:
+ """annotate 'remote' in primaryjoin, secondaryjoin
+ when the parent/child tables are entirely
+ separate.
+
+ """
+
+ def repl(element: _CE, **kw: Any) -> Optional[_CE]:
+ if self.child_persist_selectable.c.contains_column(element) and (
+ not self.parent_local_selectable.c.contains_column(element)
+ or self.child_local_selectable.c.contains_column(element)
+ ):
+ return element._annotate({"remote": True})
+ return None
+
+ self.primaryjoin = visitors.replacement_traverse(
+ self.primaryjoin, {}, repl
+ )
+
+ def _warn_non_column_elements(self) -> None:
+ util.warn(
+ "Non-simple column elements in primary "
+ "join condition for property %s - consider using "
+ "remote() annotations to mark the remote side." % self.prop
+ )
+
+ def _annotate_local(self) -> None:
+ """Annotate the primaryjoin and secondaryjoin
+ structures with 'local' annotations.
+
+ This annotates all column elements found
+ simultaneously in the parent table
+ and the join condition that don't have a
+ 'remote' annotation set up from
+ _annotate_remote() or user-defined.
+
+ """
+ if self._has_annotation(self.primaryjoin, "local"):
+ return
+
+ if self._local_remote_pairs:
+ local_side = util.column_set(
+ [l for (l, r) in self._local_remote_pairs]
+ )
+ else:
+ local_side = util.column_set(self.parent_persist_selectable.c)
+
+ def locals_(element: _CE, **kw: Any) -> Optional[_CE]:
+ if "remote" not in element._annotations and element in local_side:
+ return element._annotate({"local": True})
+ return None
+
+ self.primaryjoin = visitors.replacement_traverse(
+ self.primaryjoin, {}, locals_
+ )
+
+ def _annotate_parentmapper(self) -> None:
+ def parentmappers_(element: _CE, **kw: Any) -> Optional[_CE]:
+ if "remote" in element._annotations:
+ return element._annotate({"parentmapper": self.prop.mapper})
+ elif "local" in element._annotations:
+ return element._annotate({"parentmapper": self.prop.parent})
+ return None
+
+ self.primaryjoin = visitors.replacement_traverse(
+ self.primaryjoin, {}, parentmappers_
+ )
+
+ def _check_remote_side(self) -> None:
+ if not self.local_remote_pairs:
+ raise sa_exc.ArgumentError(
+ "Relationship %s could "
+ "not determine any unambiguous local/remote column "
+ "pairs based on join condition and remote_side "
+ "arguments. "
+ "Consider using the remote() annotation to "
+ "accurately mark those elements of the join "
+ "condition that are on the remote side of "
+ "the relationship." % (self.prop,)
+ )
+ else:
+ not_target = util.column_set(
+ self.parent_persist_selectable.c
+ ).difference(self.child_persist_selectable.c)
+
+ for _, rmt in self.local_remote_pairs:
+ if rmt in not_target:
+ util.warn(
+ "Expression %s is marked as 'remote', but these "
+ "column(s) are local to the local side. The "
+ "remote() annotation is needed only for a "
+ "self-referential relationship where both sides "
+ "of the relationship refer to the same tables."
+ % (rmt,)
+ )
+
+ def _check_foreign_cols(
+ self, join_condition: ColumnElement[bool], primary: bool
+ ) -> None:
+ """Check the foreign key columns collected and emit error
+ messages."""
+
+ can_sync = False
+
+ foreign_cols = self._gather_columns_with_annotation(
+ join_condition, "foreign"
+ )
+
+ has_foreign = bool(foreign_cols)
+
+ if primary:
+ can_sync = bool(self.synchronize_pairs)
+ else:
+ can_sync = bool(self.secondary_synchronize_pairs)
+
+ if (
+ self.support_sync
+ and can_sync
+ or (not self.support_sync and has_foreign)
+ ):
+ return
+
+ # from here below is just determining the best error message
+ # to report. Check for a join condition using any operator
+ # (not just ==), perhaps they need to turn on "viewonly=True".
+ if self.support_sync and has_foreign and not can_sync:
+ err = (
+ "Could not locate any simple equality expressions "
+ "involving locally mapped foreign key columns for "
+ "%s join condition "
+ "'%s' on relationship %s."
+ % (
+ primary and "primary" or "secondary",
+ join_condition,
+ self.prop,
+ )
+ )
+ err += (
+ " Ensure that referencing columns are associated "
+ "with a ForeignKey or ForeignKeyConstraint, or are "
+ "annotated in the join condition with the foreign() "
+ "annotation. To allow comparison operators other than "
+ "'==', the relationship can be marked as viewonly=True."
+ )
+
+ raise sa_exc.ArgumentError(err)
+ else:
+ err = (
+ "Could not locate any relevant foreign key columns "
+ "for %s join condition '%s' on relationship %s."
+ % (
+ primary and "primary" or "secondary",
+ join_condition,
+ self.prop,
+ )
+ )
+ err += (
+ " Ensure that referencing columns are associated "
+ "with a ForeignKey or ForeignKeyConstraint, or are "
+ "annotated in the join condition with the foreign() "
+ "annotation."
+ )
+ raise sa_exc.ArgumentError(err)
+
+ def _determine_direction(self) -> None:
+ """Determine if this relationship is one to many, many to one,
+ many to many.
+
+ """
+ if self.secondaryjoin is not None:
+ self.direction = MANYTOMANY
+ else:
+ parentcols = util.column_set(self.parent_persist_selectable.c)
+ targetcols = util.column_set(self.child_persist_selectable.c)
+
+ # fk collection which suggests ONETOMANY.
+ onetomany_fk = targetcols.intersection(self.foreign_key_columns)
+
+ # fk collection which suggests MANYTOONE.
+
+ manytoone_fk = parentcols.intersection(self.foreign_key_columns)
+
+ if onetomany_fk and manytoone_fk:
+ # fks on both sides. test for overlap of local/remote
+ # with foreign key.
+ # we will gather columns directly from their annotations
+ # without deannotating, so that we can distinguish on a column
+ # that refers to itself.
+
+ # 1. columns that are both remote and FK suggest
+ # onetomany.
+ onetomany_local = self._gather_columns_with_annotation(
+ self.primaryjoin, "remote", "foreign"
+ )
+
+ # 2. columns that are FK but are not remote (e.g. local)
+ # suggest manytoone.
+ manytoone_local = {
+ c
+ for c in self._gather_columns_with_annotation(
+ self.primaryjoin, "foreign"
+ )
+ if "remote" not in c._annotations
+ }
+
+ # 3. if both collections are present, remove columns that
+ # refer to themselves. This is for the case of
+ # and_(Me.id == Me.remote_id, Me.version == Me.version)
+ if onetomany_local and manytoone_local:
+ self_equated = self.remote_columns.intersection(
+ self.local_columns
+ )
+ onetomany_local = onetomany_local.difference(self_equated)
+ manytoone_local = manytoone_local.difference(self_equated)
+
+ # at this point, if only one or the other collection is
+ # present, we know the direction, otherwise it's still
+ # ambiguous.
+
+ if onetomany_local and not manytoone_local:
+ self.direction = ONETOMANY
+ elif manytoone_local and not onetomany_local:
+ self.direction = MANYTOONE
+ else:
+ raise sa_exc.ArgumentError(
+ "Can't determine relationship"
+ " direction for relationship '%s' - foreign "
+ "key columns within the join condition are present "
+ "in both the parent and the child's mapped tables. "
+ "Ensure that only those columns referring "
+ "to a parent column are marked as foreign, "
+ "either via the foreign() annotation or "
+ "via the foreign_keys argument." % self.prop
+ )
+ elif onetomany_fk:
+ self.direction = ONETOMANY
+ elif manytoone_fk:
+ self.direction = MANYTOONE
+ else:
+ raise sa_exc.ArgumentError(
+ "Can't determine relationship "
+ "direction for relationship '%s' - foreign "
+ "key columns are present in neither the parent "
+ "nor the child's mapped tables" % self.prop
+ )
+
+ def _deannotate_pairs(
+ self, collection: _ColumnPairIterable
+ ) -> _MutableColumnPairs:
+ """provide deannotation for the various lists of
+ pairs, so that using them in hashes doesn't incur
+ high-overhead __eq__() comparisons against
+ original columns mapped.
+
+ """
+ return [(x._deannotate(), y._deannotate()) for x, y in collection]
+
+ def _setup_pairs(self) -> None:
+ sync_pairs: _MutableColumnPairs = []
+ lrp: util.OrderedSet[Tuple[ColumnElement[Any], ColumnElement[Any]]] = (
+ util.OrderedSet([])
+ )
+ secondary_sync_pairs: _MutableColumnPairs = []
+
+ def go(
+ joincond: ColumnElement[bool],
+ collection: _MutableColumnPairs,
+ ) -> None:
+ def visit_binary(
+ binary: BinaryExpression[Any],
+ left: ColumnElement[Any],
+ right: ColumnElement[Any],
+ ) -> None:
+ if (
+ "remote" in right._annotations
+ and "remote" not in left._annotations
+ and self.can_be_synced_fn(left)
+ ):
+ lrp.add((left, right))
+ elif (
+ "remote" in left._annotations
+ and "remote" not in right._annotations
+ and self.can_be_synced_fn(right)
+ ):
+ lrp.add((right, left))
+ if binary.operator is operators.eq and self.can_be_synced_fn(
+ left, right
+ ):
+ if "foreign" in right._annotations:
+ collection.append((left, right))
+ elif "foreign" in left._annotations:
+ collection.append((right, left))
+
+ visit_binary_product(visit_binary, joincond)
+
+ for joincond, collection in [
+ (self.primaryjoin, sync_pairs),
+ (self.secondaryjoin, secondary_sync_pairs),
+ ]:
+ if joincond is None:
+ continue
+ go(joincond, collection)
+
+ self.local_remote_pairs = self._deannotate_pairs(lrp)
+ self.synchronize_pairs = self._deannotate_pairs(sync_pairs)
+ self.secondary_synchronize_pairs = self._deannotate_pairs(
+ secondary_sync_pairs
+ )
+
+ _track_overlapping_sync_targets: weakref.WeakKeyDictionary[
+ ColumnElement[Any],
+ weakref.WeakKeyDictionary[
+ RelationshipProperty[Any], ColumnElement[Any]
+ ],
+ ] = weakref.WeakKeyDictionary()
+
+ def _warn_for_conflicting_sync_targets(self) -> None:
+ if not self.support_sync:
+ return
+
+ # we would like to detect if we are synchronizing any column
+ # pairs in conflict with another relationship that wishes to sync
+ # an entirely different column to the same target. This is a
+ # very rare edge case so we will try to minimize the memory/overhead
+ # impact of this check
+ for from_, to_ in [
+ (from_, to_) for (from_, to_) in self.synchronize_pairs
+ ] + [
+ (from_, to_) for (from_, to_) in self.secondary_synchronize_pairs
+ ]:
+ # save ourselves a ton of memory and overhead by only
+ # considering columns that are subject to a overlapping
+ # FK constraints at the core level. This condition can arise
+ # if multiple relationships overlap foreign() directly, but
+ # we're going to assume it's typically a ForeignKeyConstraint-
+ # level configuration that benefits from this warning.
+
+ if to_ not in self._track_overlapping_sync_targets:
+ self._track_overlapping_sync_targets[to_] = (
+ weakref.WeakKeyDictionary({self.prop: from_})
+ )
+ else:
+ other_props = []
+ prop_to_from = self._track_overlapping_sync_targets[to_]
+
+ for pr, fr_ in prop_to_from.items():
+ if (
+ not pr.mapper._dispose_called
+ and pr not in self.prop._reverse_property
+ and pr.key not in self.prop._overlaps
+ and self.prop.key not in pr._overlaps
+ # note: the "__*" symbol is used internally by
+ # SQLAlchemy as a general means of suppressing the
+ # overlaps warning for some extension cases, however
+ # this is not currently
+ # a publicly supported symbol and may change at
+ # any time.
+ and "__*" not in self.prop._overlaps
+ and "__*" not in pr._overlaps
+ and not self.prop.parent.is_sibling(pr.parent)
+ and not self.prop.mapper.is_sibling(pr.mapper)
+ and not self.prop.parent.is_sibling(pr.mapper)
+ and not self.prop.mapper.is_sibling(pr.parent)
+ and (
+ self.prop.key != pr.key
+ or not self.prop.parent.common_parent(pr.parent)
+ )
+ ):
+ other_props.append((pr, fr_))
+
+ if other_props:
+ util.warn(
+ "relationship '%s' will copy column %s to column %s, "
+ "which conflicts with relationship(s): %s. "
+ "If this is not the intention, consider if these "
+ "relationships should be linked with "
+ "back_populates, or if viewonly=True should be "
+ "applied to one or more if they are read-only. "
+ "For the less common case that foreign key "
+ "constraints are partially overlapping, the "
+ "orm.foreign() "
+ "annotation can be used to isolate the columns that "
+ "should be written towards. To silence this "
+ "warning, add the parameter 'overlaps=\"%s\"' to the "
+ "'%s' relationship."
+ % (
+ self.prop,
+ from_,
+ to_,
+ ", ".join(
+ sorted(
+ "'%s' (copies %s to %s)" % (pr, fr_, to_)
+ for (pr, fr_) in other_props
+ )
+ ),
+ ",".join(sorted(pr.key for pr, fr in other_props)),
+ self.prop,
+ ),
+ code="qzyx",
+ )
+ self._track_overlapping_sync_targets[to_][self.prop] = from_
+
+ @util.memoized_property
+ def remote_columns(self) -> Set[ColumnElement[Any]]:
+ return self._gather_join_annotations("remote")
+
+ @util.memoized_property
+ def local_columns(self) -> Set[ColumnElement[Any]]:
+ return self._gather_join_annotations("local")
+
+ @util.memoized_property
+ def foreign_key_columns(self) -> Set[ColumnElement[Any]]:
+ return self._gather_join_annotations("foreign")
+
+ def _gather_join_annotations(
+ self, annotation: str
+ ) -> Set[ColumnElement[Any]]:
+ s = set(
+ self._gather_columns_with_annotation(self.primaryjoin, annotation)
+ )
+ if self.secondaryjoin is not None:
+ s.update(
+ self._gather_columns_with_annotation(
+ self.secondaryjoin, annotation
+ )
+ )
+ return {x._deannotate() for x in s}
+
+ def _gather_columns_with_annotation(
+ self, clause: ColumnElement[Any], *annotation: Iterable[str]
+ ) -> Set[ColumnElement[Any]]:
+ annotation_set = set(annotation)
+ return {
+ cast(ColumnElement[Any], col)
+ for col in visitors.iterate(clause, {})
+ if annotation_set.issubset(col._annotations)
+ }
+
+ @util.memoized_property
+ def _secondary_lineage_set(self) -> FrozenSet[ColumnElement[Any]]:
+ if self.secondary is not None:
+ return frozenset(
+ itertools.chain(*[c.proxy_set for c in self.secondary.c])
+ )
+ else:
+ return util.EMPTY_SET
+
+ def join_targets(
+ self,
+ source_selectable: Optional[FromClause],
+ dest_selectable: FromClause,
+ aliased: bool,
+ single_crit: Optional[ColumnElement[bool]] = None,
+ extra_criteria: Tuple[ColumnElement[bool], ...] = (),
+ ) -> Tuple[
+ ColumnElement[bool],
+ Optional[ColumnElement[bool]],
+ Optional[FromClause],
+ Optional[ClauseAdapter],
+ FromClause,
+ ]:
+ """Given a source and destination selectable, create a
+ join between them.
+
+ This takes into account aliasing the join clause
+ to reference the appropriate corresponding columns
+ in the target objects, as well as the extra child
+ criterion, equivalent column sets, etc.
+
+ """
+ # place a barrier on the destination such that
+ # replacement traversals won't ever dig into it.
+ # its internal structure remains fixed
+ # regardless of context.
+ dest_selectable = _shallow_annotate(
+ dest_selectable, {"no_replacement_traverse": True}
+ )
+
+ primaryjoin, secondaryjoin, secondary = (
+ self.primaryjoin,
+ self.secondaryjoin,
+ self.secondary,
+ )
+
+ # adjust the join condition for single table inheritance,
+ # in the case that the join is to a subclass
+ # this is analogous to the
+ # "_adjust_for_single_table_inheritance()" method in Query.
+
+ if single_crit is not None:
+ if secondaryjoin is not None:
+ secondaryjoin = secondaryjoin & single_crit
+ else:
+ primaryjoin = primaryjoin & single_crit
+
+ if extra_criteria:
+
+ def mark_exclude_cols(
+ elem: SupportsAnnotations, annotations: _AnnotationDict
+ ) -> SupportsAnnotations:
+ """note unrelated columns in the "extra criteria" as either
+ should be adapted or not adapted, even though they are not
+ part of our "local" or "remote" side.
+
+ see #9779 for this case, as well as #11010 for a follow up
+
+ """
+
+ parentmapper_for_element = elem._annotations.get(
+ "parentmapper", None
+ )
+
+ if (
+ parentmapper_for_element is not self.prop.parent
+ and parentmapper_for_element is not self.prop.mapper
+ and elem not in self._secondary_lineage_set
+ ):
+ return _safe_annotate(elem, annotations)
+ else:
+ return elem
+
+ extra_criteria = tuple(
+ _deep_annotate(
+ elem,
+ {"should_not_adapt": True},
+ annotate_callable=mark_exclude_cols,
+ )
+ for elem in extra_criteria
+ )
+
+ if secondaryjoin is not None:
+ secondaryjoin = secondaryjoin & sql.and_(*extra_criteria)
+ else:
+ primaryjoin = primaryjoin & sql.and_(*extra_criteria)
+
+ if aliased:
+ if secondary is not None:
+ secondary = secondary._anonymous_fromclause(flat=True)
+ primary_aliasizer = ClauseAdapter(
+ secondary,
+ exclude_fn=_local_col_exclude,
+ )
+ secondary_aliasizer = ClauseAdapter(
+ dest_selectable, equivalents=self.child_equivalents
+ ).chain(primary_aliasizer)
+ if source_selectable is not None:
+ primary_aliasizer = ClauseAdapter(
+ secondary,
+ exclude_fn=_local_col_exclude,
+ ).chain(
+ ClauseAdapter(
+ source_selectable,
+ equivalents=self.parent_equivalents,
+ )
+ )
+
+ secondaryjoin = secondary_aliasizer.traverse(secondaryjoin)
+ else:
+ primary_aliasizer = ClauseAdapter(
+ dest_selectable,
+ exclude_fn=_local_col_exclude,
+ equivalents=self.child_equivalents,
+ )
+ if source_selectable is not None:
+ primary_aliasizer.chain(
+ ClauseAdapter(
+ source_selectable,
+ exclude_fn=_remote_col_exclude,
+ equivalents=self.parent_equivalents,
+ )
+ )
+ secondary_aliasizer = None
+
+ primaryjoin = primary_aliasizer.traverse(primaryjoin)
+ target_adapter = secondary_aliasizer or primary_aliasizer
+ target_adapter.exclude_fn = None
+ else:
+ target_adapter = None
+ return (
+ primaryjoin,
+ secondaryjoin,
+ secondary,
+ target_adapter,
+ dest_selectable,
+ )
+
+ def create_lazy_clause(self, reverse_direction: bool = False) -> Tuple[
+ ColumnElement[bool],
+ Dict[str, ColumnElement[Any]],
+ Dict[ColumnElement[Any], ColumnElement[Any]],
+ ]:
+ binds: Dict[ColumnElement[Any], BindParameter[Any]] = {}
+ equated_columns: Dict[ColumnElement[Any], ColumnElement[Any]] = {}
+
+ has_secondary = self.secondaryjoin is not None
+
+ if has_secondary:
+ lookup = collections.defaultdict(list)
+ for l, r in self.local_remote_pairs:
+ lookup[l].append((l, r))
+ equated_columns[r] = l
+ elif not reverse_direction:
+ for l, r in self.local_remote_pairs:
+ equated_columns[r] = l
+ else:
+ for l, r in self.local_remote_pairs:
+ equated_columns[l] = r
+
+ def col_to_bind(
+ element: ColumnElement[Any], **kw: Any
+ ) -> Optional[BindParameter[Any]]:
+ if (
+ (not reverse_direction and "local" in element._annotations)
+ or reverse_direction
+ and (
+ (has_secondary and element in lookup)
+ or (not has_secondary and "remote" in element._annotations)
+ )
+ ):
+ if element not in binds:
+ binds[element] = sql.bindparam(
+ None, None, type_=element.type, unique=True
+ )
+ return binds[element]
+ return None
+
+ lazywhere = self.primaryjoin
+ if self.secondaryjoin is None or not reverse_direction:
+ lazywhere = visitors.replacement_traverse(
+ lazywhere, {}, col_to_bind
+ )
+
+ if self.secondaryjoin is not None:
+ secondaryjoin = self.secondaryjoin
+ if reverse_direction:
+ secondaryjoin = visitors.replacement_traverse(
+ secondaryjoin, {}, col_to_bind
+ )
+ lazywhere = sql.and_(lazywhere, secondaryjoin)
+
+ bind_to_col = {binds[col].key: col for col in binds}
+
+ return lazywhere, bind_to_col, equated_columns
+
+
+class _ColInAnnotations:
+ """Serializable object that tests for names in c._annotations.
+
+ TODO: does this need to be serializable anymore? can we find what the
+ use case was for that?
+
+ """
+
+ __slots__ = ("names",)
+
+ def __init__(self, *names: str):
+ self.names = frozenset(names)
+
+ def __call__(self, c: ClauseElement) -> bool:
+ return bool(self.names.intersection(c._annotations))
+
+
+_local_col_exclude = _ColInAnnotations("local", "should_not_adapt")
+_remote_col_exclude = _ColInAnnotations("remote", "should_not_adapt")
+
+
+class Relationship(
+ RelationshipProperty[_T],
+ _DeclarativeMapped[_T],
+):
+ """Describes an object property that holds a single item or list
+ of items that correspond to a related database table.
+
+ Public constructor is the :func:`_orm.relationship` function.
+
+ .. seealso::
+
+ :ref:`relationship_config_toplevel`
+
+ .. versionchanged:: 2.0 Added :class:`_orm.Relationship` as a Declarative
+ compatible subclass for :class:`_orm.RelationshipProperty`.
+
+ """
+
+ inherit_cache = True
+ """:meta private:"""
+
+
+class _RelationshipDeclared( # type: ignore[misc]
+ Relationship[_T],
+ WriteOnlyMapped[_T], # not compatible with Mapped[_T]
+ DynamicMapped[_T], # not compatible with Mapped[_T]
+):
+ """Relationship subclass used implicitly for declarative mapping."""
+
+ inherit_cache = True
+ """:meta private:"""
+
+ @classmethod
+ def _mapper_property_name(cls) -> str:
+ return "Relationship"
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/scoping.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/scoping.py
new file mode 100644
index 0000000..819616a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/scoping.py
@@ -0,0 +1,2165 @@
+# orm/scoping.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from .session import _S
+from .session import Session
+from .. import exc as sa_exc
+from .. import util
+from ..util import create_proxy_methods
+from ..util import ScopedRegistry
+from ..util import ThreadLocalRegistry
+from ..util import warn
+from ..util import warn_deprecated
+from ..util.typing import Protocol
+
+if TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _IdentityKeyType
+ from ._typing import OrmExecuteOptionsParameter
+ from .identity import IdentityMap
+ from .interfaces import ORMOption
+ from .mapper import Mapper
+ from .query import Query
+ from .query import RowReturningQuery
+ from .session import _BindArguments
+ from .session import _EntityBindKey
+ from .session import _PKIdentityArgument
+ from .session import _SessionBind
+ from .session import sessionmaker
+ from .session import SessionTransaction
+ from ..engine import Connection
+ from ..engine import CursorResult
+ from ..engine import Engine
+ from ..engine import Result
+ from ..engine import Row
+ from ..engine import RowMapping
+ from ..engine.interfaces import _CoreAnyExecuteParams
+ from ..engine.interfaces import _CoreSingleExecuteParams
+ from ..engine.interfaces import CoreExecuteOptionsParameter
+ from ..engine.result import ScalarResult
+ from ..sql._typing import _ColumnsClauseArgument
+ from ..sql._typing import _T0
+ from ..sql._typing import _T1
+ from ..sql._typing import _T2
+ from ..sql._typing import _T3
+ from ..sql._typing import _T4
+ from ..sql._typing import _T5
+ from ..sql._typing import _T6
+ from ..sql._typing import _T7
+ from ..sql._typing import _TypedColumnClauseArgument as _TCCA
+ from ..sql.base import Executable
+ from ..sql.dml import UpdateBase
+ from ..sql.elements import ClauseElement
+ from ..sql.roles import TypedColumnsClauseRole
+ from ..sql.selectable import ForUpdateParameter
+ from ..sql.selectable import TypedReturnsRows
+
+_T = TypeVar("_T", bound=Any)
+
+
+class QueryPropertyDescriptor(Protocol):
+ """Describes the type applied to a class-level
+ :meth:`_orm.scoped_session.query_property` attribute.
+
+ .. versionadded:: 2.0.5
+
+ """
+
+ def __get__(self, instance: Any, owner: Type[_T]) -> Query[_T]: ...
+
+
+_O = TypeVar("_O", bound=object)
+
+__all__ = ["scoped_session"]
+
+
+@create_proxy_methods(
+ Session,
+ ":class:`_orm.Session`",
+ ":class:`_orm.scoping.scoped_session`",
+ classmethods=["close_all", "object_session", "identity_key"],
+ methods=[
+ "__contains__",
+ "__iter__",
+ "add",
+ "add_all",
+ "begin",
+ "begin_nested",
+ "close",
+ "reset",
+ "commit",
+ "connection",
+ "delete",
+ "execute",
+ "expire",
+ "expire_all",
+ "expunge",
+ "expunge_all",
+ "flush",
+ "get",
+ "get_one",
+ "get_bind",
+ "is_modified",
+ "bulk_save_objects",
+ "bulk_insert_mappings",
+ "bulk_update_mappings",
+ "merge",
+ "query",
+ "refresh",
+ "rollback",
+ "scalar",
+ "scalars",
+ ],
+ attributes=[
+ "bind",
+ "dirty",
+ "deleted",
+ "new",
+ "identity_map",
+ "is_active",
+ "autoflush",
+ "no_autoflush",
+ "info",
+ ],
+)
+class scoped_session(Generic[_S]):
+ """Provides scoped management of :class:`.Session` objects.
+
+ See :ref:`unitofwork_contextual` for a tutorial.
+
+ .. note::
+
+ When using :ref:`asyncio_toplevel`, the async-compatible
+ :class:`_asyncio.async_scoped_session` class should be
+ used in place of :class:`.scoped_session`.
+
+ """
+
+ _support_async: bool = False
+
+ session_factory: sessionmaker[_S]
+ """The `session_factory` provided to `__init__` is stored in this
+ attribute and may be accessed at a later time. This can be useful when
+ a new non-scoped :class:`.Session` is needed."""
+
+ registry: ScopedRegistry[_S]
+
+ def __init__(
+ self,
+ session_factory: sessionmaker[_S],
+ scopefunc: Optional[Callable[[], Any]] = None,
+ ):
+ """Construct a new :class:`.scoped_session`.
+
+ :param session_factory: a factory to create new :class:`.Session`
+ instances. This is usually, but not necessarily, an instance
+ of :class:`.sessionmaker`.
+ :param scopefunc: optional function which defines
+ the current scope. If not passed, the :class:`.scoped_session`
+ object assumes "thread-local" scope, and will use
+ a Python ``threading.local()`` in order to maintain the current
+ :class:`.Session`. If passed, the function should return
+ a hashable token; this token will be used as the key in a
+ dictionary in order to store and retrieve the current
+ :class:`.Session`.
+
+ """
+ self.session_factory = session_factory
+
+ if scopefunc:
+ self.registry = ScopedRegistry(session_factory, scopefunc)
+ else:
+ self.registry = ThreadLocalRegistry(session_factory)
+
+ @property
+ def _proxied(self) -> _S:
+ return self.registry()
+
+ def __call__(self, **kw: Any) -> _S:
+ r"""Return the current :class:`.Session`, creating it
+ using the :attr:`.scoped_session.session_factory` if not present.
+
+ :param \**kw: Keyword arguments will be passed to the
+ :attr:`.scoped_session.session_factory` callable, if an existing
+ :class:`.Session` is not present. If the :class:`.Session` is present
+ and keyword arguments have been passed,
+ :exc:`~sqlalchemy.exc.InvalidRequestError` is raised.
+
+ """
+ if kw:
+ if self.registry.has():
+ raise sa_exc.InvalidRequestError(
+ "Scoped session is already present; "
+ "no new arguments may be specified."
+ )
+ else:
+ sess = self.session_factory(**kw)
+ self.registry.set(sess)
+ else:
+ sess = self.registry()
+ if not self._support_async and sess._is_asyncio:
+ warn_deprecated(
+ "Using `scoped_session` with asyncio is deprecated and "
+ "will raise an error in a future version. "
+ "Please use `async_scoped_session` instead.",
+ "1.4.23",
+ )
+ return sess
+
+ def configure(self, **kwargs: Any) -> None:
+ """reconfigure the :class:`.sessionmaker` used by this
+ :class:`.scoped_session`.
+
+ See :meth:`.sessionmaker.configure`.
+
+ """
+
+ if self.registry.has():
+ warn(
+ "At least one scoped session is already present. "
+ " configure() can not affect sessions that have "
+ "already been created."
+ )
+
+ self.session_factory.configure(**kwargs)
+
+ def remove(self) -> None:
+ """Dispose of the current :class:`.Session`, if present.
+
+ This will first call :meth:`.Session.close` method
+ on the current :class:`.Session`, which releases any existing
+ transactional/connection resources still being held; transactions
+ specifically are rolled back. The :class:`.Session` is then
+ discarded. Upon next usage within the same scope,
+ the :class:`.scoped_session` will produce a new
+ :class:`.Session` object.
+
+ """
+
+ if self.registry.has():
+ self.registry().close()
+ self.registry.clear()
+
+ def query_property(
+ self, query_cls: Optional[Type[Query[_T]]] = None
+ ) -> QueryPropertyDescriptor:
+ """return a class property which produces a legacy
+ :class:`_query.Query` object against the class and the current
+ :class:`.Session` when called.
+
+ .. legacy:: The :meth:`_orm.scoped_session.query_property` accessor
+ is specific to the legacy :class:`.Query` object and is not
+ considered to be part of :term:`2.0-style` ORM use.
+
+ e.g.::
+
+ from sqlalchemy.orm import QueryPropertyDescriptor
+ from sqlalchemy.orm import scoped_session
+ from sqlalchemy.orm import sessionmaker
+
+ Session = scoped_session(sessionmaker())
+
+ class MyClass:
+ query: QueryPropertyDescriptor = Session.query_property()
+
+ # after mappers are defined
+ result = MyClass.query.filter(MyClass.name=='foo').all()
+
+ Produces instances of the session's configured query class by
+ default. To override and use a custom implementation, provide
+ a ``query_cls`` callable. The callable will be invoked with
+ the class's mapper as a positional argument and a session
+ keyword argument.
+
+ There is no limit to the number of query properties placed on
+ a class.
+
+ """
+
+ class query:
+ def __get__(s, instance: Any, owner: Type[_O]) -> Query[_O]:
+ if query_cls:
+ # custom query class
+ return query_cls(owner, session=self.registry()) # type: ignore # noqa: E501
+ else:
+ # session's configured query class
+ return self.registry().query(owner)
+
+ return query()
+
+ # START PROXY METHODS scoped_session
+
+ # code within this block is **programmatically,
+ # statically generated** by tools/generate_proxy_methods.py
+
+ def __contains__(self, instance: object) -> bool:
+ r"""Return True if the instance is associated with this session.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ The instance may be pending or persistent within the Session for a
+ result of True.
+
+
+ """ # noqa: E501
+
+ return self._proxied.__contains__(instance)
+
+ def __iter__(self) -> Iterator[object]:
+ r"""Iterate over all pending or persistent instances within this
+ Session.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+
+ """ # noqa: E501
+
+ return self._proxied.__iter__()
+
+ def add(self, instance: object, _warn: bool = True) -> None:
+ r"""Place an object into this :class:`_orm.Session`.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ Objects that are in the :term:`transient` state when passed to the
+ :meth:`_orm.Session.add` method will move to the
+ :term:`pending` state, until the next flush, at which point they
+ will move to the :term:`persistent` state.
+
+ Objects that are in the :term:`detached` state when passed to the
+ :meth:`_orm.Session.add` method will move to the :term:`persistent`
+ state directly.
+
+ If the transaction used by the :class:`_orm.Session` is rolled back,
+ objects which were transient when they were passed to
+ :meth:`_orm.Session.add` will be moved back to the
+ :term:`transient` state, and will no longer be present within this
+ :class:`_orm.Session`.
+
+ .. seealso::
+
+ :meth:`_orm.Session.add_all`
+
+ :ref:`session_adding` - at :ref:`session_basics`
+
+
+ """ # noqa: E501
+
+ return self._proxied.add(instance, _warn=_warn)
+
+ def add_all(self, instances: Iterable[object]) -> None:
+ r"""Add the given collection of instances to this :class:`_orm.Session`.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ See the documentation for :meth:`_orm.Session.add` for a general
+ behavioral description.
+
+ .. seealso::
+
+ :meth:`_orm.Session.add`
+
+ :ref:`session_adding` - at :ref:`session_basics`
+
+
+ """ # noqa: E501
+
+ return self._proxied.add_all(instances)
+
+ def begin(self, nested: bool = False) -> SessionTransaction:
+ r"""Begin a transaction, or nested transaction,
+ on this :class:`.Session`, if one is not already begun.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ The :class:`_orm.Session` object features **autobegin** behavior,
+ so that normally it is not necessary to call the
+ :meth:`_orm.Session.begin`
+ method explicitly. However, it may be used in order to control
+ the scope of when the transactional state is begun.
+
+ When used to begin the outermost transaction, an error is raised
+ if this :class:`.Session` is already inside of a transaction.
+
+ :param nested: if True, begins a SAVEPOINT transaction and is
+ equivalent to calling :meth:`~.Session.begin_nested`. For
+ documentation on SAVEPOINT transactions, please see
+ :ref:`session_begin_nested`.
+
+ :return: the :class:`.SessionTransaction` object. Note that
+ :class:`.SessionTransaction`
+ acts as a Python context manager, allowing :meth:`.Session.begin`
+ to be used in a "with" block. See :ref:`session_explicit_begin` for
+ an example.
+
+ .. seealso::
+
+ :ref:`session_autobegin`
+
+ :ref:`unitofwork_transaction`
+
+ :meth:`.Session.begin_nested`
+
+
+
+ """ # noqa: E501
+
+ return self._proxied.begin(nested=nested)
+
+ def begin_nested(self) -> SessionTransaction:
+ r"""Begin a "nested" transaction on this Session, e.g. SAVEPOINT.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ The target database(s) and associated drivers must support SQL
+ SAVEPOINT for this method to function correctly.
+
+ For documentation on SAVEPOINT
+ transactions, please see :ref:`session_begin_nested`.
+
+ :return: the :class:`.SessionTransaction` object. Note that
+ :class:`.SessionTransaction` acts as a context manager, allowing
+ :meth:`.Session.begin_nested` to be used in a "with" block.
+ See :ref:`session_begin_nested` for a usage example.
+
+ .. seealso::
+
+ :ref:`session_begin_nested`
+
+ :ref:`pysqlite_serializable` - special workarounds required
+ with the SQLite driver in order for SAVEPOINT to work
+ correctly. For asyncio use cases, see the section
+ :ref:`aiosqlite_serializable`.
+
+
+ """ # noqa: E501
+
+ return self._proxied.begin_nested()
+
+ def close(self) -> None:
+ r"""Close out the transactional resources and ORM objects used by this
+ :class:`_orm.Session`.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ This expunges all ORM objects associated with this
+ :class:`_orm.Session`, ends any transaction in progress and
+ :term:`releases` any :class:`_engine.Connection` objects which this
+ :class:`_orm.Session` itself has checked out from associated
+ :class:`_engine.Engine` objects. The operation then leaves the
+ :class:`_orm.Session` in a state which it may be used again.
+
+ .. tip::
+
+ In the default running mode the :meth:`_orm.Session.close`
+ method **does not prevent the Session from being used again**.
+ The :class:`_orm.Session` itself does not actually have a
+ distinct "closed" state; it merely means
+ the :class:`_orm.Session` will release all database connections
+ and ORM objects.
+
+ Setting the parameter :paramref:`_orm.Session.close_resets_only`
+ to ``False`` will instead make the ``close`` final, meaning that
+ any further action on the session will be forbidden.
+
+ .. versionchanged:: 1.4 The :meth:`.Session.close` method does not
+ immediately create a new :class:`.SessionTransaction` object;
+ instead, the new :class:`.SessionTransaction` is created only if
+ the :class:`.Session` is used again for a database operation.
+
+ .. seealso::
+
+ :ref:`session_closing` - detail on the semantics of
+ :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
+
+ :meth:`_orm.Session.reset` - a similar method that behaves like
+ ``close()`` with the parameter
+ :paramref:`_orm.Session.close_resets_only` set to ``True``.
+
+
+ """ # noqa: E501
+
+ return self._proxied.close()
+
+ def reset(self) -> None:
+ r"""Close out the transactional resources and ORM objects used by this
+ :class:`_orm.Session`, resetting the session to its initial state.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ This method provides for same "reset-only" behavior that the
+ :meth:`_orm.Session.close` method has provided historically, where the
+ state of the :class:`_orm.Session` is reset as though the object were
+ brand new, and ready to be used again.
+ This method may then be useful for :class:`_orm.Session` objects
+ which set :paramref:`_orm.Session.close_resets_only` to ``False``,
+ so that "reset only" behavior is still available.
+
+ .. versionadded:: 2.0.22
+
+ .. seealso::
+
+ :ref:`session_closing` - detail on the semantics of
+ :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
+
+ :meth:`_orm.Session.close` - a similar method will additionally
+ prevent re-use of the Session when the parameter
+ :paramref:`_orm.Session.close_resets_only` is set to ``False``.
+
+ """ # noqa: E501
+
+ return self._proxied.reset()
+
+ def commit(self) -> None:
+ r"""Flush pending changes and commit the current transaction.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ When the COMMIT operation is complete, all objects are fully
+ :term:`expired`, erasing their internal contents, which will be
+ automatically re-loaded when the objects are next accessed. In the
+ interim, these objects are in an expired state and will not function if
+ they are :term:`detached` from the :class:`.Session`. Additionally,
+ this re-load operation is not supported when using asyncio-oriented
+ APIs. The :paramref:`.Session.expire_on_commit` parameter may be used
+ to disable this behavior.
+
+ When there is no transaction in place for the :class:`.Session`,
+ indicating that no operations were invoked on this :class:`.Session`
+ since the previous call to :meth:`.Session.commit`, the method will
+ begin and commit an internal-only "logical" transaction, that does not
+ normally affect the database unless pending flush changes were
+ detected, but will still invoke event handlers and object expiration
+ rules.
+
+ The outermost database transaction is committed unconditionally,
+ automatically releasing any SAVEPOINTs in effect.
+
+ .. seealso::
+
+ :ref:`session_committing`
+
+ :ref:`unitofwork_transaction`
+
+ :ref:`asyncio_orm_avoid_lazyloads`
+
+
+ """ # noqa: E501
+
+ return self._proxied.commit()
+
+ def connection(
+ self,
+ bind_arguments: Optional[_BindArguments] = None,
+ execution_options: Optional[CoreExecuteOptionsParameter] = None,
+ ) -> Connection:
+ r"""Return a :class:`_engine.Connection` object corresponding to this
+ :class:`.Session` object's transactional state.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ Either the :class:`_engine.Connection` corresponding to the current
+ transaction is returned, or if no transaction is in progress, a new
+ one is begun and the :class:`_engine.Connection`
+ returned (note that no
+ transactional state is established with the DBAPI until the first
+ SQL statement is emitted).
+
+ Ambiguity in multi-bind or unbound :class:`.Session` objects can be
+ resolved through any of the optional keyword arguments. This
+ ultimately makes usage of the :meth:`.get_bind` method for resolution.
+
+ :param bind_arguments: dictionary of bind arguments. May include
+ "mapper", "bind", "clause", other custom arguments that are passed
+ to :meth:`.Session.get_bind`.
+
+ :param execution_options: a dictionary of execution options that will
+ be passed to :meth:`_engine.Connection.execution_options`, **when the
+ connection is first procured only**. If the connection is already
+ present within the :class:`.Session`, a warning is emitted and
+ the arguments are ignored.
+
+ .. seealso::
+
+ :ref:`session_transaction_isolation`
+
+
+ """ # noqa: E501
+
+ return self._proxied.connection(
+ bind_arguments=bind_arguments, execution_options=execution_options
+ )
+
+ def delete(self, instance: object) -> None:
+ r"""Mark an instance as deleted.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ The object is assumed to be either :term:`persistent` or
+ :term:`detached` when passed; after the method is called, the
+ object will remain in the :term:`persistent` state until the next
+ flush proceeds. During this time, the object will also be a member
+ of the :attr:`_orm.Session.deleted` collection.
+
+ When the next flush proceeds, the object will move to the
+ :term:`deleted` state, indicating a ``DELETE`` statement was emitted
+ for its row within the current transaction. When the transaction
+ is successfully committed,
+ the deleted object is moved to the :term:`detached` state and is
+ no longer present within this :class:`_orm.Session`.
+
+ .. seealso::
+
+ :ref:`session_deleting` - at :ref:`session_basics`
+
+
+ """ # noqa: E501
+
+ return self._proxied.delete(instance)
+
+ @overload
+ def execute(
+ self,
+ statement: TypedReturnsRows[_T],
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ ) -> Result[_T]: ...
+
+ @overload
+ def execute(
+ self,
+ statement: UpdateBase,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ ) -> CursorResult[Any]: ...
+
+ @overload
+ def execute(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ ) -> Result[Any]: ...
+
+ def execute(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ ) -> Result[Any]:
+ r"""Execute a SQL expression construct.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ Returns a :class:`_engine.Result` object representing
+ results of the statement execution.
+
+ E.g.::
+
+ from sqlalchemy import select
+ result = session.execute(
+ select(User).where(User.id == 5)
+ )
+
+ The API contract of :meth:`_orm.Session.execute` is similar to that
+ of :meth:`_engine.Connection.execute`, the :term:`2.0 style` version
+ of :class:`_engine.Connection`.
+
+ .. versionchanged:: 1.4 the :meth:`_orm.Session.execute` method is
+ now the primary point of ORM statement execution when using
+ :term:`2.0 style` ORM usage.
+
+ :param statement:
+ An executable statement (i.e. an :class:`.Executable` expression
+ such as :func:`_expression.select`).
+
+ :param params:
+ Optional dictionary, or list of dictionaries, containing
+ bound parameter values. If a single dictionary, single-row
+ execution occurs; if a list of dictionaries, an
+ "executemany" will be invoked. The keys in each dictionary
+ must correspond to parameter names present in the statement.
+
+ :param execution_options: optional dictionary of execution options,
+ which will be associated with the statement execution. This
+ dictionary can provide a subset of the options that are accepted
+ by :meth:`_engine.Connection.execution_options`, and may also
+ provide additional options understood only in an ORM context.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_execution_options` - ORM-specific execution
+ options
+
+ :param bind_arguments: dictionary of additional arguments to determine
+ the bind. May include "mapper", "bind", or other custom arguments.
+ Contents of this dictionary are passed to the
+ :meth:`.Session.get_bind` method.
+
+ :return: a :class:`_engine.Result` object.
+
+
+
+ """ # noqa: E501
+
+ return self._proxied.execute(
+ statement,
+ params=params,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ _parent_execute_state=_parent_execute_state,
+ _add_event=_add_event,
+ )
+
+ def expire(
+ self, instance: object, attribute_names: Optional[Iterable[str]] = None
+ ) -> None:
+ r"""Expire the attributes on an instance.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ Marks the attributes of an instance as out of date. When an expired
+ attribute is next accessed, a query will be issued to the
+ :class:`.Session` object's current transactional context in order to
+ load all expired attributes for the given instance. Note that
+ a highly isolated transaction will return the same values as were
+ previously read in that same transaction, regardless of changes
+ in database state outside of that transaction.
+
+ To expire all objects in the :class:`.Session` simultaneously,
+ use :meth:`Session.expire_all`.
+
+ The :class:`.Session` object's default behavior is to
+ expire all state whenever the :meth:`Session.rollback`
+ or :meth:`Session.commit` methods are called, so that new
+ state can be loaded for the new transaction. For this reason,
+ calling :meth:`Session.expire` only makes sense for the specific
+ case that a non-ORM SQL statement was emitted in the current
+ transaction.
+
+ :param instance: The instance to be refreshed.
+ :param attribute_names: optional list of string attribute names
+ indicating a subset of attributes to be expired.
+
+ .. seealso::
+
+ :ref:`session_expire` - introductory material
+
+ :meth:`.Session.expire`
+
+ :meth:`.Session.refresh`
+
+ :meth:`_orm.Query.populate_existing`
+
+
+ """ # noqa: E501
+
+ return self._proxied.expire(instance, attribute_names=attribute_names)
+
+ def expire_all(self) -> None:
+ r"""Expires all persistent instances within this Session.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ When any attributes on a persistent instance is next accessed,
+ a query will be issued using the
+ :class:`.Session` object's current transactional context in order to
+ load all expired attributes for the given instance. Note that
+ a highly isolated transaction will return the same values as were
+ previously read in that same transaction, regardless of changes
+ in database state outside of that transaction.
+
+ To expire individual objects and individual attributes
+ on those objects, use :meth:`Session.expire`.
+
+ The :class:`.Session` object's default behavior is to
+ expire all state whenever the :meth:`Session.rollback`
+ or :meth:`Session.commit` methods are called, so that new
+ state can be loaded for the new transaction. For this reason,
+ calling :meth:`Session.expire_all` is not usually needed,
+ assuming the transaction is isolated.
+
+ .. seealso::
+
+ :ref:`session_expire` - introductory material
+
+ :meth:`.Session.expire`
+
+ :meth:`.Session.refresh`
+
+ :meth:`_orm.Query.populate_existing`
+
+
+ """ # noqa: E501
+
+ return self._proxied.expire_all()
+
+ def expunge(self, instance: object) -> None:
+ r"""Remove the `instance` from this ``Session``.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ This will free all internal references to the instance. Cascading
+ will be applied according to the *expunge* cascade rule.
+
+
+ """ # noqa: E501
+
+ return self._proxied.expunge(instance)
+
+ def expunge_all(self) -> None:
+ r"""Remove all object instances from this ``Session``.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ This is equivalent to calling ``expunge(obj)`` on all objects in this
+ ``Session``.
+
+
+ """ # noqa: E501
+
+ return self._proxied.expunge_all()
+
+ def flush(self, objects: Optional[Sequence[Any]] = None) -> None:
+ r"""Flush all the object changes to the database.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ Writes out all pending object creations, deletions and modifications
+ to the database as INSERTs, DELETEs, UPDATEs, etc. Operations are
+ automatically ordered by the Session's unit of work dependency
+ solver.
+
+ Database operations will be issued in the current transactional
+ context and do not affect the state of the transaction, unless an
+ error occurs, in which case the entire transaction is rolled back.
+ You may flush() as often as you like within a transaction to move
+ changes from Python to the database's transaction buffer.
+
+ :param objects: Optional; restricts the flush operation to operate
+ only on elements that are in the given collection.
+
+ This feature is for an extremely narrow set of use cases where
+ particular objects may need to be operated upon before the
+ full flush() occurs. It is not intended for general use.
+
+
+ """ # noqa: E501
+
+ return self._proxied.flush(objects=objects)
+
+ def get(
+ self,
+ entity: _EntityBindKey[_O],
+ ident: _PKIdentityArgument,
+ *,
+ options: Optional[Sequence[ORMOption]] = None,
+ populate_existing: bool = False,
+ with_for_update: ForUpdateParameter = None,
+ identity_token: Optional[Any] = None,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ ) -> Optional[_O]:
+ r"""Return an instance based on the given primary key identifier,
+ or ``None`` if not found.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ 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 legacy :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.
+
+ :param execution_options: optional dictionary of execution options,
+ which will be associated with the query execution if one is emitted.
+ This dictionary can provide a subset of the options that are
+ accepted by :meth:`_engine.Connection.execution_options`, and may
+ also provide additional options understood only in an ORM context.
+
+ .. versionadded:: 1.4.29
+
+ .. seealso::
+
+ :ref:`orm_queryguide_execution_options` - ORM-specific execution
+ options
+
+ :param bind_arguments: dictionary of additional arguments to determine
+ the bind. May include "mapper", "bind", or other custom arguments.
+ Contents of this dictionary are passed to the
+ :meth:`.Session.get_bind` method.
+
+ .. versionadded: 2.0.0rc1
+
+ :return: The object instance, or ``None``.
+
+
+ """ # noqa: E501
+
+ return self._proxied.get(
+ entity,
+ ident,
+ options=options,
+ populate_existing=populate_existing,
+ with_for_update=with_for_update,
+ identity_token=identity_token,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ )
+
+ def get_one(
+ self,
+ entity: _EntityBindKey[_O],
+ ident: _PKIdentityArgument,
+ *,
+ options: Optional[Sequence[ORMOption]] = None,
+ populate_existing: bool = False,
+ with_for_update: ForUpdateParameter = None,
+ identity_token: Optional[Any] = None,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ ) -> _O:
+ r"""Return exactly one instance based on the given primary key
+ identifier, or raise an exception if not found.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ Raises ``sqlalchemy.orm.exc.NoResultFound`` if the query
+ selects no rows.
+
+ For a detailed documentation of the arguments see the
+ method :meth:`.Session.get`.
+
+ .. versionadded:: 2.0.22
+
+ :return: The object instance.
+
+ .. seealso::
+
+ :meth:`.Session.get` - equivalent method that instead
+ returns ``None`` if no row was found with the provided primary
+ key
+
+
+ """ # noqa: E501
+
+ return self._proxied.get_one(
+ entity,
+ ident,
+ options=options,
+ populate_existing=populate_existing,
+ with_for_update=with_for_update,
+ identity_token=identity_token,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ )
+
+ def get_bind(
+ self,
+ mapper: Optional[_EntityBindKey[_O]] = None,
+ *,
+ clause: Optional[ClauseElement] = None,
+ bind: Optional[_SessionBind] = None,
+ _sa_skip_events: Optional[bool] = None,
+ _sa_skip_for_implicit_returning: bool = False,
+ **kw: Any,
+ ) -> Union[Engine, Connection]:
+ r"""Return a "bind" to which this :class:`.Session` is bound.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ The "bind" is usually an instance of :class:`_engine.Engine`,
+ except in the case where the :class:`.Session` has been
+ explicitly bound directly to a :class:`_engine.Connection`.
+
+ For a multiply-bound or unbound :class:`.Session`, the
+ ``mapper`` or ``clause`` arguments are used to determine the
+ appropriate bind to return.
+
+ Note that the "mapper" argument is usually present
+ when :meth:`.Session.get_bind` is called via an ORM
+ operation such as a :meth:`.Session.query`, each
+ individual INSERT/UPDATE/DELETE operation within a
+ :meth:`.Session.flush`, call, etc.
+
+ The order of resolution is:
+
+ 1. if mapper given and :paramref:`.Session.binds` is present,
+ locate a bind based first on the mapper in use, then
+ on the mapped class in use, then on any base classes that are
+ present in the ``__mro__`` of the mapped class, from more specific
+ superclasses to more general.
+ 2. if clause given and ``Session.binds`` is present,
+ locate a bind based on :class:`_schema.Table` objects
+ found in the given clause present in ``Session.binds``.
+ 3. if ``Session.binds`` is present, return that.
+ 4. if clause given, attempt to return a bind
+ linked to the :class:`_schema.MetaData` ultimately
+ associated with the clause.
+ 5. if mapper given, attempt to return a bind
+ linked to the :class:`_schema.MetaData` ultimately
+ associated with the :class:`_schema.Table` or other
+ selectable to which the mapper is mapped.
+ 6. No bind can be found, :exc:`~sqlalchemy.exc.UnboundExecutionError`
+ is raised.
+
+ Note that the :meth:`.Session.get_bind` method can be overridden on
+ a user-defined subclass of :class:`.Session` to provide any kind
+ of bind resolution scheme. See the example at
+ :ref:`session_custom_partitioning`.
+
+ :param mapper:
+ Optional mapped class or corresponding :class:`_orm.Mapper` instance.
+ The bind can be derived from a :class:`_orm.Mapper` first by
+ consulting the "binds" map associated with this :class:`.Session`,
+ and secondly by consulting the :class:`_schema.MetaData` associated
+ with the :class:`_schema.Table` to which the :class:`_orm.Mapper` is
+ mapped for a bind.
+
+ :param clause:
+ A :class:`_expression.ClauseElement` (i.e.
+ :func:`_expression.select`,
+ :func:`_expression.text`,
+ etc.). If the ``mapper`` argument is not present or could not
+ produce a bind, the given expression construct will be searched
+ for a bound element, typically a :class:`_schema.Table`
+ associated with
+ bound :class:`_schema.MetaData`.
+
+ .. seealso::
+
+ :ref:`session_partitioning`
+
+ :paramref:`.Session.binds`
+
+ :meth:`.Session.bind_mapper`
+
+ :meth:`.Session.bind_table`
+
+
+ """ # noqa: E501
+
+ return self._proxied.get_bind(
+ mapper=mapper,
+ clause=clause,
+ bind=bind,
+ _sa_skip_events=_sa_skip_events,
+ _sa_skip_for_implicit_returning=_sa_skip_for_implicit_returning,
+ **kw,
+ )
+
+ def is_modified(
+ self, instance: object, include_collections: bool = True
+ ) -> bool:
+ r"""Return ``True`` if the given instance has locally
+ modified attributes.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ This method retrieves the history for each instrumented
+ attribute on the instance and performs a comparison of the current
+ value to its previously committed value, if any.
+
+ It is in effect a more expensive and accurate
+ version of checking for the given instance in the
+ :attr:`.Session.dirty` collection; a full test for
+ each attribute's net "dirty" status is performed.
+
+ E.g.::
+
+ return session.is_modified(someobject)
+
+ A few caveats to this method apply:
+
+ * Instances present in the :attr:`.Session.dirty` collection may
+ report ``False`` when tested with this method. This is because
+ the object may have received change events via attribute mutation,
+ thus placing it in :attr:`.Session.dirty`, but ultimately the state
+ is the same as that loaded from the database, resulting in no net
+ change here.
+ * Scalar attributes may not have recorded the previously set
+ value when a new value was applied, if the attribute was not loaded,
+ or was expired, at the time the new value was received - in these
+ cases, the attribute is assumed to have a change, even if there is
+ ultimately no net change against its database value. SQLAlchemy in
+ most cases does not need the "old" value when a set event occurs, so
+ it skips the expense of a SQL call if the old value isn't present,
+ based on the assumption that an UPDATE of the scalar value is
+ usually needed, and in those few cases where it isn't, is less
+ expensive on average than issuing a defensive SELECT.
+
+ The "old" value is fetched unconditionally upon set only if the
+ attribute container has the ``active_history`` flag set to ``True``.
+ This flag is set typically for primary key attributes and scalar
+ object references that are not a simple many-to-one. To set this
+ flag for any arbitrary mapped column, use the ``active_history``
+ argument with :func:`.column_property`.
+
+ :param instance: mapped instance to be tested for pending changes.
+ :param include_collections: Indicates if multivalued collections
+ should be included in the operation. Setting this to ``False`` is a
+ way to detect only local-column based properties (i.e. scalar columns
+ or many-to-one foreign keys) that would result in an UPDATE for this
+ instance upon flush.
+
+
+ """ # noqa: E501
+
+ return self._proxied.is_modified(
+ instance, include_collections=include_collections
+ )
+
+ def bulk_save_objects(
+ self,
+ objects: Iterable[object],
+ return_defaults: bool = False,
+ update_changed_only: bool = True,
+ preserve_order: bool = True,
+ ) -> None:
+ r"""Perform a bulk save of the given list of objects.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ .. legacy::
+
+ This method is a legacy feature as of the 2.0 series of
+ SQLAlchemy. For modern bulk INSERT and UPDATE, see
+ the sections :ref:`orm_queryguide_bulk_insert` and
+ :ref:`orm_queryguide_bulk_update`.
+
+ For general INSERT and UPDATE of existing ORM mapped objects,
+ prefer standard :term:`unit of work` data management patterns,
+ introduced in the :ref:`unified_tutorial` at
+ :ref:`tutorial_orm_data_manipulation`. SQLAlchemy 2.0
+ now uses :ref:`engine_insertmanyvalues` with modern dialects
+ which solves previous issues of bulk INSERT slowness.
+
+ :param objects: a sequence of mapped object instances. The mapped
+ objects are persisted as is, and are **not** associated with the
+ :class:`.Session` afterwards.
+
+ For each object, whether the object is sent as an INSERT or an
+ UPDATE is dependent on the same rules used by the :class:`.Session`
+ in traditional operation; if the object has the
+ :attr:`.InstanceState.key`
+ attribute set, then the object is assumed to be "detached" and
+ will result in an UPDATE. Otherwise, an INSERT is used.
+
+ In the case of an UPDATE, statements are grouped based on which
+ attributes have changed, and are thus to be the subject of each
+ SET clause. If ``update_changed_only`` is False, then all
+ attributes present within each object are applied to the UPDATE
+ statement, which may help in allowing the statements to be grouped
+ together into a larger executemany(), and will also reduce the
+ overhead of checking history on attributes.
+
+ :param return_defaults: when True, rows that are missing values which
+ generate defaults, namely integer primary key defaults and sequences,
+ will be inserted **one at a time**, so that the primary key value
+ is available. In particular this will allow joined-inheritance
+ and other multi-table mappings to insert correctly without the need
+ to provide primary key values ahead of time; however,
+ :paramref:`.Session.bulk_save_objects.return_defaults` **greatly
+ reduces the performance gains** of the method overall. It is strongly
+ advised to please use the standard :meth:`_orm.Session.add_all`
+ approach.
+
+ :param update_changed_only: when True, UPDATE statements are rendered
+ based on those attributes in each state that have logged changes.
+ When False, all attributes present are rendered into the SET clause
+ with the exception of primary key attributes.
+
+ :param preserve_order: when True, the order of inserts and updates
+ matches exactly the order in which the objects are given. When
+ False, common types of objects are grouped into inserts
+ and updates, to allow for more batching opportunities.
+
+ .. seealso::
+
+ :doc:`queryguide/dml`
+
+ :meth:`.Session.bulk_insert_mappings`
+
+ :meth:`.Session.bulk_update_mappings`
+
+
+ """ # noqa: E501
+
+ return self._proxied.bulk_save_objects(
+ objects,
+ return_defaults=return_defaults,
+ update_changed_only=update_changed_only,
+ preserve_order=preserve_order,
+ )
+
+ def bulk_insert_mappings(
+ self,
+ mapper: Mapper[Any],
+ mappings: Iterable[Dict[str, Any]],
+ return_defaults: bool = False,
+ render_nulls: bool = False,
+ ) -> None:
+ r"""Perform a bulk insert of the given list of mapping dictionaries.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ .. legacy::
+
+ This method is a legacy feature as of the 2.0 series of
+ SQLAlchemy. For modern bulk INSERT and UPDATE, see
+ the sections :ref:`orm_queryguide_bulk_insert` and
+ :ref:`orm_queryguide_bulk_update`. The 2.0 API shares
+ implementation details with this method and adds new features
+ as well.
+
+ :param mapper: a mapped class, or the actual :class:`_orm.Mapper`
+ object,
+ representing the single kind of object represented within the mapping
+ list.
+
+ :param mappings: a sequence of dictionaries, each one containing the
+ state of the mapped row to be inserted, in terms of the attribute
+ names on the mapped class. If the mapping refers to multiple tables,
+ such as a joined-inheritance mapping, each dictionary must contain all
+ keys to be populated into all tables.
+
+ :param return_defaults: when True, the INSERT process will be altered
+ to ensure that newly generated primary key values will be fetched.
+ The rationale for this parameter is typically to enable
+ :ref:`Joined Table Inheritance <joined_inheritance>` mappings to
+ be bulk inserted.
+
+ .. note:: for backends that don't support RETURNING, the
+ :paramref:`_orm.Session.bulk_insert_mappings.return_defaults`
+ parameter can significantly decrease performance as INSERT
+ statements can no longer be batched. See
+ :ref:`engine_insertmanyvalues`
+ for background on which backends are affected.
+
+ :param render_nulls: When True, a value of ``None`` will result
+ in a NULL value being included in the INSERT statement, rather
+ than the column being omitted from the INSERT. This allows all
+ the rows being INSERTed to have the identical set of columns which
+ allows the full set of rows to be batched to the DBAPI. Normally,
+ each column-set that contains a different combination of NULL values
+ than the previous row must omit a different series of columns from
+ the rendered INSERT statement, which means it must be emitted as a
+ separate statement. By passing this flag, the full set of rows
+ are guaranteed to be batchable into one batch; the cost however is
+ that server-side defaults which are invoked by an omitted column will
+ be skipped, so care must be taken to ensure that these are not
+ necessary.
+
+ .. warning::
+
+ When this flag is set, **server side default SQL values will
+ not be invoked** for those columns that are inserted as NULL;
+ the NULL value will be sent explicitly. Care must be taken
+ to ensure that no server-side default functions need to be
+ invoked for the operation as a whole.
+
+ .. seealso::
+
+ :doc:`queryguide/dml`
+
+ :meth:`.Session.bulk_save_objects`
+
+ :meth:`.Session.bulk_update_mappings`
+
+
+ """ # noqa: E501
+
+ return self._proxied.bulk_insert_mappings(
+ mapper,
+ mappings,
+ return_defaults=return_defaults,
+ render_nulls=render_nulls,
+ )
+
+ def bulk_update_mappings(
+ self, mapper: Mapper[Any], mappings: Iterable[Dict[str, Any]]
+ ) -> None:
+ r"""Perform a bulk update of the given list of mapping dictionaries.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ .. legacy::
+
+ This method is a legacy feature as of the 2.0 series of
+ SQLAlchemy. For modern bulk INSERT and UPDATE, see
+ the sections :ref:`orm_queryguide_bulk_insert` and
+ :ref:`orm_queryguide_bulk_update`. The 2.0 API shares
+ implementation details with this method and adds new features
+ as well.
+
+ :param mapper: a mapped class, or the actual :class:`_orm.Mapper`
+ object,
+ representing the single kind of object represented within the mapping
+ list.
+
+ :param mappings: a sequence of dictionaries, each one containing the
+ state of the mapped row to be updated, in terms of the attribute names
+ on the mapped class. If the mapping refers to multiple tables, such
+ as a joined-inheritance mapping, each dictionary may contain keys
+ corresponding to all tables. All those keys which are present and
+ are not part of the primary key are applied to the SET clause of the
+ UPDATE statement; the primary key values, which are required, are
+ applied to the WHERE clause.
+
+
+ .. seealso::
+
+ :doc:`queryguide/dml`
+
+ :meth:`.Session.bulk_insert_mappings`
+
+ :meth:`.Session.bulk_save_objects`
+
+
+ """ # noqa: E501
+
+ return self._proxied.bulk_update_mappings(mapper, mappings)
+
+ def merge(
+ self,
+ instance: _O,
+ *,
+ load: bool = True,
+ options: Optional[Sequence[ORMOption]] = None,
+ ) -> _O:
+ r"""Copy the state of a given instance into a corresponding instance
+ within this :class:`.Session`.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ :meth:`.Session.merge` examines the primary key attributes of the
+ source instance, and attempts to reconcile it with an instance of the
+ same primary key in the session. If not found locally, it attempts
+ to load the object from the database based on primary key, and if
+ none can be located, creates a new instance. The state of each
+ attribute on the source instance is then copied to the target
+ instance. The resulting target instance is then returned by the
+ method; the original source instance is left unmodified, and
+ un-associated with the :class:`.Session` if not already.
+
+ This operation cascades to associated instances if the association is
+ mapped with ``cascade="merge"``.
+
+ See :ref:`unitofwork_merging` for a detailed discussion of merging.
+
+ :param instance: Instance to be merged.
+ :param load: Boolean, when False, :meth:`.merge` switches into
+ a "high performance" mode which causes it to forego emitting history
+ events as well as all database access. This flag is used for
+ cases such as transferring graphs of objects into a :class:`.Session`
+ from a second level cache, or to transfer just-loaded objects
+ into the :class:`.Session` owned by a worker thread or process
+ without re-querying the database.
+
+ The ``load=False`` use case adds the caveat that the given
+ object has to be in a "clean" state, that is, has no pending changes
+ to be flushed - even if the incoming object is detached from any
+ :class:`.Session`. This is so that when
+ the merge operation populates local attributes and
+ cascades to related objects and
+ collections, the values can be "stamped" onto the
+ target object as is, without generating any history or attribute
+ events, and without the need to reconcile the incoming data with
+ any existing related objects or collections that might not
+ be loaded. The resulting objects from ``load=False`` are always
+ produced as "clean", so it is only appropriate that the given objects
+ should be "clean" as well, else this suggests a mis-use of the
+ method.
+ :param options: optional sequence of loader options which will be
+ applied to the :meth:`_orm.Session.get` method when the merge
+ operation loads the existing version of the object from the database.
+
+ .. versionadded:: 1.4.24
+
+
+ .. seealso::
+
+ :func:`.make_transient_to_detached` - provides for an alternative
+ means of "merging" a single object into the :class:`.Session`
+
+
+ """ # noqa: E501
+
+ return self._proxied.merge(instance, load=load, options=options)
+
+ @overload
+ def query(self, _entity: _EntityType[_O]) -> Query[_O]: ...
+
+ @overload
+ def query(
+ self, _colexpr: TypedColumnsClauseRole[_T]
+ ) -> RowReturningQuery[Tuple[_T]]: ...
+
+ # START OVERLOADED FUNCTIONS self.query RowReturningQuery 2-8
+
+ # code within this block is **programmatically,
+ # statically generated** by tools/generate_tuple_map_overloads.py
+
+ @overload
+ def query(
+ self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1]
+ ) -> RowReturningQuery[Tuple[_T0, _T1]]: ...
+
+ @overload
+ def query(
+ self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2]
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ __ent5: _TCCA[_T5],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ __ent5: _TCCA[_T5],
+ __ent6: _TCCA[_T6],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ __ent5: _TCCA[_T5],
+ __ent6: _TCCA[_T6],
+ __ent7: _TCCA[_T7],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: ...
+
+ # END OVERLOADED FUNCTIONS self.query
+
+ @overload
+ def query(
+ self, *entities: _ColumnsClauseArgument[Any], **kwargs: Any
+ ) -> Query[Any]: ...
+
+ def query(
+ self, *entities: _ColumnsClauseArgument[Any], **kwargs: Any
+ ) -> Query[Any]:
+ r"""Return a new :class:`_query.Query` object corresponding to this
+ :class:`_orm.Session`.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ Note that the :class:`_query.Query` object is legacy as of
+ SQLAlchemy 2.0; the :func:`_sql.select` construct is now used
+ to construct ORM queries.
+
+ .. seealso::
+
+ :ref:`unified_tutorial`
+
+ :ref:`queryguide_toplevel`
+
+ :ref:`query_api_toplevel` - legacy API doc
+
+
+ """ # noqa: E501
+
+ return self._proxied.query(*entities, **kwargs)
+
+ def refresh(
+ self,
+ instance: object,
+ attribute_names: Optional[Iterable[str]] = None,
+ with_for_update: ForUpdateParameter = None,
+ ) -> None:
+ r"""Expire and refresh attributes on the given instance.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ The selected attributes will first be expired as they would when using
+ :meth:`_orm.Session.expire`; then a SELECT statement will be issued to
+ the database to refresh column-oriented attributes with the current
+ value available in the current transaction.
+
+ :func:`_orm.relationship` oriented attributes will also be immediately
+ loaded if they were already eagerly loaded on the object, using the
+ same eager loading strategy that they were loaded with originally.
+
+ .. versionadded:: 1.4 - the :meth:`_orm.Session.refresh` method
+ can also refresh eagerly loaded attributes.
+
+ :func:`_orm.relationship` oriented attributes that would normally
+ load using the ``select`` (or "lazy") loader strategy will also
+ load **if they are named explicitly in the attribute_names
+ collection**, emitting a SELECT statement for the attribute using the
+ ``immediate`` loader strategy. If lazy-loaded relationships are not
+ named in :paramref:`_orm.Session.refresh.attribute_names`, then
+ they remain as "lazy loaded" attributes and are not implicitly
+ refreshed.
+
+ .. versionchanged:: 2.0.4 The :meth:`_orm.Session.refresh` method
+ will now refresh lazy-loaded :func:`_orm.relationship` oriented
+ attributes for those which are named explicitly in the
+ :paramref:`_orm.Session.refresh.attribute_names` collection.
+
+ .. tip::
+
+ While the :meth:`_orm.Session.refresh` method is capable of
+ refreshing both column and relationship oriented attributes, its
+ primary focus is on refreshing of local column-oriented attributes
+ on a single instance. For more open ended "refresh" functionality,
+ including the ability to refresh the attributes on many objects at
+ once while having explicit control over relationship loader
+ strategies, use the
+ :ref:`populate existing <orm_queryguide_populate_existing>` feature
+ instead.
+
+ Note that a highly isolated transaction will return the same values as
+ were previously read in that same transaction, regardless of changes
+ in database state outside of that transaction. Refreshing
+ attributes usually only makes sense at the start of a transaction
+ where database rows have not yet been accessed.
+
+ :param attribute_names: optional. An iterable collection of
+ string attribute names indicating a subset of attributes to
+ be refreshed.
+
+ :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.
+
+ .. seealso::
+
+ :ref:`session_expire` - introductory material
+
+ :meth:`.Session.expire`
+
+ :meth:`.Session.expire_all`
+
+ :ref:`orm_queryguide_populate_existing` - allows any ORM query
+ to refresh objects as they would be loaded normally.
+
+
+ """ # noqa: E501
+
+ return self._proxied.refresh(
+ instance,
+ attribute_names=attribute_names,
+ with_for_update=with_for_update,
+ )
+
+ def rollback(self) -> None:
+ r"""Rollback the current transaction in progress.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ If no transaction is in progress, this method is a pass-through.
+
+ The method always rolls back
+ the topmost database transaction, discarding any nested
+ transactions that may be in progress.
+
+ .. seealso::
+
+ :ref:`session_rollback`
+
+ :ref:`unitofwork_transaction`
+
+
+ """ # noqa: E501
+
+ return self._proxied.rollback()
+
+ @overload
+ def scalar(
+ self,
+ statement: TypedReturnsRows[Tuple[_T]],
+ params: Optional[_CoreSingleExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> Optional[_T]: ...
+
+ @overload
+ def scalar(
+ self,
+ statement: Executable,
+ params: Optional[_CoreSingleExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> Any: ...
+
+ def scalar(
+ self,
+ statement: Executable,
+ params: Optional[_CoreSingleExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> Any:
+ r"""Execute a statement and return a scalar result.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ Usage and parameters are the same as that of
+ :meth:`_orm.Session.execute`; the return result is a scalar Python
+ value.
+
+
+ """ # noqa: E501
+
+ return self._proxied.scalar(
+ statement,
+ params=params,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ **kw,
+ )
+
+ @overload
+ def scalars(
+ self,
+ statement: TypedReturnsRows[Tuple[_T]],
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> ScalarResult[_T]: ...
+
+ @overload
+ def scalars(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> ScalarResult[Any]: ...
+
+ def scalars(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> ScalarResult[Any]:
+ r"""Execute a statement and return the results as scalars.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ Usage and parameters are the same as that of
+ :meth:`_orm.Session.execute`; the return result is a
+ :class:`_result.ScalarResult` filtering object which
+ will return single elements rather than :class:`_row.Row` objects.
+
+ :return: a :class:`_result.ScalarResult` object
+
+ .. versionadded:: 1.4.24 Added :meth:`_orm.Session.scalars`
+
+ .. versionadded:: 1.4.26 Added :meth:`_orm.scoped_session.scalars`
+
+ .. seealso::
+
+ :ref:`orm_queryguide_select_orm_entities` - contrasts the behavior
+ of :meth:`_orm.Session.execute` to :meth:`_orm.Session.scalars`
+
+
+ """ # noqa: E501
+
+ return self._proxied.scalars(
+ statement,
+ params=params,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ **kw,
+ )
+
+ @property
+ def bind(self) -> Optional[Union[Engine, Connection]]:
+ r"""Proxy for the :attr:`_orm.Session.bind` attribute
+ on behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ """ # noqa: E501
+
+ return self._proxied.bind
+
+ @bind.setter
+ def bind(self, attr: Optional[Union[Engine, Connection]]) -> None:
+ self._proxied.bind = attr
+
+ @property
+ def dirty(self) -> Any:
+ r"""The set of all persistent instances considered dirty.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class
+ on behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ E.g.::
+
+ some_mapped_object in session.dirty
+
+ Instances are considered dirty when they were modified but not
+ deleted.
+
+ Note that this 'dirty' calculation is 'optimistic'; most
+ attribute-setting or collection modification operations will
+ mark an instance as 'dirty' and place it in this set, even if
+ there is no net change to the attribute's value. At flush
+ time, the value of each attribute is compared to its
+ previously saved value, and if there's no net change, no SQL
+ operation will occur (this is a more expensive operation so
+ it's only done at flush time).
+
+ To check if an instance has actionable net changes to its
+ attributes, use the :meth:`.Session.is_modified` method.
+
+
+ """ # noqa: E501
+
+ return self._proxied.dirty
+
+ @property
+ def deleted(self) -> Any:
+ r"""The set of all instances marked as 'deleted' within this ``Session``
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class
+ on behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ """ # noqa: E501
+
+ return self._proxied.deleted
+
+ @property
+ def new(self) -> Any:
+ r"""The set of all instances marked as 'new' within this ``Session``.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class
+ on behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ """ # noqa: E501
+
+ return self._proxied.new
+
+ @property
+ def identity_map(self) -> IdentityMap:
+ r"""Proxy for the :attr:`_orm.Session.identity_map` attribute
+ on behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ """ # noqa: E501
+
+ return self._proxied.identity_map
+
+ @identity_map.setter
+ def identity_map(self, attr: IdentityMap) -> None:
+ self._proxied.identity_map = attr
+
+ @property
+ def is_active(self) -> Any:
+ r"""True if this :class:`.Session` not in "partial rollback" state.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class
+ on behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ .. 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`
+
+
+ """ # noqa: E501
+
+ return self._proxied.is_active
+
+ @property
+ def autoflush(self) -> bool:
+ r"""Proxy for the :attr:`_orm.Session.autoflush` attribute
+ on behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ """ # noqa: E501
+
+ return self._proxied.autoflush
+
+ @autoflush.setter
+ def autoflush(self, attr: bool) -> None:
+ self._proxied.autoflush = attr
+
+ @property
+ def no_autoflush(self) -> Any:
+ r"""Return a context manager that disables autoflush.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class
+ on behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ e.g.::
+
+ with session.no_autoflush:
+
+ some_object = SomeClass()
+ session.add(some_object)
+ # won't autoflush
+ some_object.related_thing = session.query(SomeRelated).first()
+
+ Operations that proceed within the ``with:`` block
+ will not be subject to flushes occurring upon query
+ access. This is useful when initializing a series
+ of objects which involve existing database queries,
+ where the uncompleted object should not yet be flushed.
+
+
+ """ # noqa: E501
+
+ return self._proxied.no_autoflush
+
+ @property
+ def info(self) -> Any:
+ r"""A user-modifiable dictionary.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class
+ on behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ The initial value of this dictionary can be populated using the
+ ``info`` argument to the :class:`.Session` constructor or
+ :class:`.sessionmaker` constructor or factory methods. The dictionary
+ here is always local to this :class:`.Session` and can be modified
+ independently of all other :class:`.Session` objects.
+
+
+ """ # noqa: E501
+
+ return self._proxied.info
+
+ @classmethod
+ def close_all(cls) -> None:
+ r"""Close *all* sessions in memory.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ .. deprecated:: 1.3 The :meth:`.Session.close_all` method is deprecated and will be removed in a future release. Please refer to :func:`.session.close_all_sessions`.
+
+ """ # noqa: E501
+
+ return Session.close_all()
+
+ @classmethod
+ def object_session(cls, instance: object) -> Optional[Session]:
+ r"""Return the :class:`.Session` to which an object belongs.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ This is an alias of :func:`.object_session`.
+
+
+ """ # noqa: E501
+
+ return Session.object_session(instance)
+
+ @classmethod
+ def identity_key(
+ cls,
+ class_: Optional[Type[Any]] = None,
+ ident: Union[Any, Tuple[Any, ...]] = None,
+ *,
+ instance: Optional[Any] = None,
+ row: Optional[Union[Row[Any], RowMapping]] = None,
+ identity_token: Optional[Any] = None,
+ ) -> _IdentityKeyType[Any]:
+ r"""Return an identity key.
+
+ .. container:: class_bases
+
+ Proxied for the :class:`_orm.Session` class on
+ behalf of the :class:`_orm.scoping.scoped_session` class.
+
+ This is an alias of :func:`.util.identity_key`.
+
+
+ """ # noqa: E501
+
+ return Session.identity_key(
+ class_=class_,
+ ident=ident,
+ instance=instance,
+ row=row,
+ identity_token=identity_token,
+ )
+
+ # END PROXY METHODS scoped_session
+
+
+ScopedSession = scoped_session
+"""Old name for backwards compatibility."""
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py
new file mode 100644
index 0000000..3eba5aa
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py
@@ -0,0 +1,5238 @@
+# orm/session.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""Provides the Session class and related utilities."""
+
+from __future__ import annotations
+
+import contextlib
+from enum import Enum
+import itertools
+import sys
+import typing
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import NoReturn
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import attributes
+from . import bulk_persistence
+from . import context
+from . import descriptor_props
+from . import exc
+from . import identity
+from . import loading
+from . import query
+from . import state as statelib
+from ._typing import _O
+from ._typing import insp_is_mapper
+from ._typing import is_composite_class
+from ._typing import is_orm_option
+from ._typing import is_user_defined_option
+from .base import _class_to_mapper
+from .base import _none_set
+from .base import _state_mapper
+from .base import instance_str
+from .base import LoaderCallableStatus
+from .base import object_mapper
+from .base import object_state
+from .base import PassiveFlag
+from .base import state_str
+from .context import FromStatement
+from .context import ORMCompileState
+from .identity import IdentityMap
+from .query import Query
+from .state import InstanceState
+from .state_changes import _StateChange
+from .state_changes import _StateChangeState
+from .state_changes import _StateChangeStates
+from .unitofwork import UOWTransaction
+from .. import engine
+from .. import exc as sa_exc
+from .. import sql
+from .. import util
+from ..engine import Connection
+from ..engine import Engine
+from ..engine.util import TransactionalContext
+from ..event import dispatcher
+from ..event import EventTarget
+from ..inspection import inspect
+from ..inspection import Inspectable
+from ..sql import coercions
+from ..sql import dml
+from ..sql import roles
+from ..sql import Select
+from ..sql import TableClause
+from ..sql import visitors
+from ..sql.base import _NoArg
+from ..sql.base import CompileState
+from ..sql.schema import Table
+from ..sql.selectable import ForUpdateArg
+from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
+from ..util import IdentitySet
+from ..util.typing import Literal
+from ..util.typing import Protocol
+
+if typing.TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _IdentityKeyType
+ from ._typing import _InstanceDict
+ from ._typing import OrmExecuteOptionsParameter
+ from .interfaces import ORMOption
+ from .interfaces import UserDefinedOption
+ from .mapper import Mapper
+ from .path_registry import PathRegistry
+ from .query import RowReturningQuery
+ from ..engine import CursorResult
+ from ..engine import Result
+ from ..engine import Row
+ from ..engine import RowMapping
+ from ..engine.base import Transaction
+ from ..engine.base import TwoPhaseTransaction
+ from ..engine.interfaces import _CoreAnyExecuteParams
+ from ..engine.interfaces import _CoreSingleExecuteParams
+ from ..engine.interfaces import _ExecuteOptions
+ from ..engine.interfaces import CoreExecuteOptionsParameter
+ from ..engine.result import ScalarResult
+ from ..event import _InstanceLevelDispatch
+ from ..sql._typing import _ColumnsClauseArgument
+ from ..sql._typing import _InfoType
+ from ..sql._typing import _T0
+ from ..sql._typing import _T1
+ from ..sql._typing import _T2
+ from ..sql._typing import _T3
+ from ..sql._typing import _T4
+ from ..sql._typing import _T5
+ from ..sql._typing import _T6
+ from ..sql._typing import _T7
+ from ..sql._typing import _TypedColumnClauseArgument as _TCCA
+ from ..sql.base import Executable
+ from ..sql.base import ExecutableOption
+ from ..sql.dml import UpdateBase
+ from ..sql.elements import ClauseElement
+ from ..sql.roles import TypedColumnsClauseRole
+ from ..sql.selectable import ForUpdateParameter
+ from ..sql.selectable import TypedReturnsRows
+
+_T = TypeVar("_T", bound=Any)
+
+__all__ = [
+ "Session",
+ "SessionTransaction",
+ "sessionmaker",
+ "ORMExecuteState",
+ "close_all_sessions",
+ "make_transient",
+ "make_transient_to_detached",
+ "object_session",
+]
+
+_sessions: weakref.WeakValueDictionary[int, Session] = (
+ weakref.WeakValueDictionary()
+)
+"""Weak-referencing dictionary of :class:`.Session` objects.
+"""
+
+statelib._sessions = _sessions
+
+_PKIdentityArgument = Union[Any, Tuple[Any, ...]]
+
+_BindArguments = Dict[str, Any]
+
+_EntityBindKey = Union[Type[_O], "Mapper[_O]"]
+_SessionBindKey = Union[Type[Any], "Mapper[Any]", "TableClause", str]
+_SessionBind = Union["Engine", "Connection"]
+
+JoinTransactionMode = Literal[
+ "conditional_savepoint",
+ "rollback_only",
+ "control_fully",
+ "create_savepoint",
+]
+
+
+class _ConnectionCallableProto(Protocol):
+ """a callable that returns a :class:`.Connection` given an instance.
+
+ This callable, when present on a :class:`.Session`, is called only from the
+ ORM's persistence mechanism (i.e. the unit of work flush process) to allow
+ for connection-per-instance schemes (i.e. horizontal sharding) to be used
+ as persistence time.
+
+ This callable is not present on a plain :class:`.Session`, however
+ is established when using the horizontal sharding extension.
+
+ """
+
+ def __call__(
+ self,
+ mapper: Optional[Mapper[Any]] = None,
+ instance: Optional[object] = None,
+ **kw: Any,
+ ) -> Connection: ...
+
+
+def _state_session(state: InstanceState[Any]) -> Optional[Session]:
+ """Given an :class:`.InstanceState`, return the :class:`.Session`
+ associated, if any.
+ """
+ return state.session
+
+
+class _SessionClassMethods:
+ """Class-level methods for :class:`.Session`, :class:`.sessionmaker`."""
+
+ @classmethod
+ @util.deprecated(
+ "1.3",
+ "The :meth:`.Session.close_all` method is deprecated and will be "
+ "removed in a future release. Please refer to "
+ ":func:`.session.close_all_sessions`.",
+ )
+ def close_all(cls) -> None:
+ """Close *all* sessions in memory."""
+
+ close_all_sessions()
+
+ @classmethod
+ @util.preload_module("sqlalchemy.orm.util")
+ def identity_key(
+ cls,
+ class_: Optional[Type[Any]] = None,
+ ident: Union[Any, Tuple[Any, ...]] = None,
+ *,
+ instance: Optional[Any] = None,
+ row: Optional[Union[Row[Any], RowMapping]] = None,
+ identity_token: Optional[Any] = None,
+ ) -> _IdentityKeyType[Any]:
+ """Return an identity key.
+
+ This is an alias of :func:`.util.identity_key`.
+
+ """
+ return util.preloaded.orm_util.identity_key(
+ class_,
+ ident,
+ instance=instance,
+ row=row,
+ identity_token=identity_token,
+ )
+
+ @classmethod
+ def object_session(cls, instance: object) -> Optional[Session]:
+ """Return the :class:`.Session` to which an object belongs.
+
+ This is an alias of :func:`.object_session`.
+
+ """
+
+ return object_session(instance)
+
+
+class SessionTransactionState(_StateChangeState):
+ ACTIVE = 1
+ PREPARED = 2
+ COMMITTED = 3
+ DEACTIVE = 4
+ CLOSED = 5
+ PROVISIONING_CONNECTION = 6
+
+
+# backwards compatibility
+ACTIVE, PREPARED, COMMITTED, DEACTIVE, CLOSED, PROVISIONING_CONNECTION = tuple(
+ SessionTransactionState
+)
+
+
+class ORMExecuteState(util.MemoizedSlots):
+ """Represents a call to the :meth:`_orm.Session.execute` method, as passed
+ to the :meth:`.SessionEvents.do_orm_execute` event hook.
+
+ .. versionadded:: 1.4
+
+ .. seealso::
+
+ :ref:`session_execute_events` - top level documentation on how
+ to use :meth:`_orm.SessionEvents.do_orm_execute`
+
+ """
+
+ __slots__ = (
+ "session",
+ "statement",
+ "parameters",
+ "execution_options",
+ "local_execution_options",
+ "bind_arguments",
+ "identity_token",
+ "_compile_state_cls",
+ "_starting_event_idx",
+ "_events_todo",
+ "_update_execution_options",
+ )
+
+ session: Session
+ """The :class:`_orm.Session` in use."""
+
+ statement: Executable
+ """The SQL statement being invoked.
+
+ For an ORM selection as would
+ be retrieved from :class:`_orm.Query`, this is an instance of
+ :class:`_sql.select` that was generated from the ORM query.
+ """
+
+ parameters: Optional[_CoreAnyExecuteParams]
+ """Dictionary of parameters that was passed to
+ :meth:`_orm.Session.execute`."""
+
+ execution_options: _ExecuteOptions
+ """The complete dictionary of current execution options.
+
+ This is a merge of the statement level options with the
+ locally passed execution options.
+
+ .. seealso::
+
+ :attr:`_orm.ORMExecuteState.local_execution_options`
+
+ :meth:`_sql.Executable.execution_options`
+
+ :ref:`orm_queryguide_execution_options`
+
+ """
+
+ local_execution_options: _ExecuteOptions
+ """Dictionary view of the execution options passed to the
+ :meth:`.Session.execute` method.
+
+ This does not include options that may be associated with the statement
+ being invoked.
+
+ .. seealso::
+
+ :attr:`_orm.ORMExecuteState.execution_options`
+
+ """
+
+ bind_arguments: _BindArguments
+ """The dictionary passed as the
+ :paramref:`_orm.Session.execute.bind_arguments` dictionary.
+
+ This dictionary may be used by extensions to :class:`_orm.Session` to pass
+ arguments that will assist in determining amongst a set of database
+ connections which one should be used to invoke this statement.
+
+ """
+
+ _compile_state_cls: Optional[Type[ORMCompileState]]
+ _starting_event_idx: int
+ _events_todo: List[Any]
+ _update_execution_options: Optional[_ExecuteOptions]
+
+ def __init__(
+ self,
+ session: Session,
+ statement: Executable,
+ parameters: Optional[_CoreAnyExecuteParams],
+ execution_options: _ExecuteOptions,
+ bind_arguments: _BindArguments,
+ compile_state_cls: Optional[Type[ORMCompileState]],
+ events_todo: List[_InstanceLevelDispatch[Session]],
+ ):
+ """Construct a new :class:`_orm.ORMExecuteState`.
+
+ this object is constructed internally.
+
+ """
+ self.session = session
+ self.statement = statement
+ self.parameters = parameters
+ self.local_execution_options = execution_options
+ self.execution_options = statement._execution_options.union(
+ execution_options
+ )
+ self.bind_arguments = bind_arguments
+ self._compile_state_cls = compile_state_cls
+ self._events_todo = list(events_todo)
+
+ def _remaining_events(self) -> List[_InstanceLevelDispatch[Session]]:
+ return self._events_todo[self._starting_event_idx + 1 :]
+
+ def invoke_statement(
+ self,
+ statement: Optional[Executable] = None,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ execution_options: Optional[OrmExecuteOptionsParameter] = None,
+ bind_arguments: Optional[_BindArguments] = None,
+ ) -> Result[Any]:
+ """Execute the statement represented by this
+ :class:`.ORMExecuteState`, without re-invoking events that have
+ already proceeded.
+
+ This method essentially performs a re-entrant execution of the current
+ statement for which the :meth:`.SessionEvents.do_orm_execute` event is
+ being currently invoked. The use case for this is for event handlers
+ that want to override how the ultimate
+ :class:`_engine.Result` object is returned, such as for schemes that
+ retrieve results from an offline cache or which concatenate results
+ from multiple executions.
+
+ When the :class:`_engine.Result` object is returned by the actual
+ handler function within :meth:`_orm.SessionEvents.do_orm_execute` and
+ is propagated to the calling
+ :meth:`_orm.Session.execute` method, the remainder of the
+ :meth:`_orm.Session.execute` method is preempted and the
+ :class:`_engine.Result` object is returned to the caller of
+ :meth:`_orm.Session.execute` immediately.
+
+ :param statement: optional statement to be invoked, in place of the
+ statement currently represented by :attr:`.ORMExecuteState.statement`.
+
+ :param params: optional dictionary of parameters or list of parameters
+ which will be merged into the existing
+ :attr:`.ORMExecuteState.parameters` of this :class:`.ORMExecuteState`.
+
+ .. versionchanged:: 2.0 a list of parameter dictionaries is accepted
+ for executemany executions.
+
+ :param execution_options: optional dictionary of execution options
+ will be merged into the existing
+ :attr:`.ORMExecuteState.execution_options` of this
+ :class:`.ORMExecuteState`.
+
+ :param bind_arguments: optional dictionary of bind_arguments
+ which will be merged amongst the current
+ :attr:`.ORMExecuteState.bind_arguments`
+ of this :class:`.ORMExecuteState`.
+
+ :return: a :class:`_engine.Result` object with ORM-level results.
+
+ .. seealso::
+
+ :ref:`do_orm_execute_re_executing` - background and examples on the
+ appropriate usage of :meth:`_orm.ORMExecuteState.invoke_statement`.
+
+
+ """
+
+ if statement is None:
+ statement = self.statement
+
+ _bind_arguments = dict(self.bind_arguments)
+ if bind_arguments:
+ _bind_arguments.update(bind_arguments)
+ _bind_arguments["_sa_skip_events"] = True
+
+ _params: Optional[_CoreAnyExecuteParams]
+ if params:
+ if self.is_executemany:
+ _params = []
+ exec_many_parameters = cast(
+ "List[Dict[str, Any]]", self.parameters
+ )
+ for _existing_params, _new_params in itertools.zip_longest(
+ exec_many_parameters,
+ cast("List[Dict[str, Any]]", params),
+ ):
+ if _existing_params is None or _new_params is None:
+ raise sa_exc.InvalidRequestError(
+ f"Can't apply executemany parameters to "
+ f"statement; number of parameter sets passed to "
+ f"Session.execute() ({len(exec_many_parameters)}) "
+ f"does not match number of parameter sets given "
+ f"to ORMExecuteState.invoke_statement() "
+ f"({len(params)})"
+ )
+ _existing_params = dict(_existing_params)
+ _existing_params.update(_new_params)
+ _params.append(_existing_params)
+ else:
+ _params = dict(cast("Dict[str, Any]", self.parameters))
+ _params.update(cast("Dict[str, Any]", params))
+ else:
+ _params = self.parameters
+
+ _execution_options = self.local_execution_options
+ if execution_options:
+ _execution_options = _execution_options.union(execution_options)
+
+ return self.session._execute_internal(
+ statement,
+ _params,
+ execution_options=_execution_options,
+ bind_arguments=_bind_arguments,
+ _parent_execute_state=self,
+ )
+
+ @property
+ def bind_mapper(self) -> Optional[Mapper[Any]]:
+ """Return the :class:`_orm.Mapper` that is the primary "bind" mapper.
+
+ For an :class:`_orm.ORMExecuteState` object invoking an ORM
+ statement, that is, the :attr:`_orm.ORMExecuteState.is_orm_statement`
+ attribute is ``True``, this attribute will return the
+ :class:`_orm.Mapper` that is considered to be the "primary" mapper
+ of the statement. The term "bind mapper" refers to the fact that
+ a :class:`_orm.Session` object may be "bound" to multiple
+ :class:`_engine.Engine` objects keyed to mapped classes, and the
+ "bind mapper" determines which of those :class:`_engine.Engine` objects
+ would be selected.
+
+ For a statement that is invoked against a single mapped class,
+ :attr:`_orm.ORMExecuteState.bind_mapper` is intended to be a reliable
+ way of getting this mapper.
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :attr:`_orm.ORMExecuteState.all_mappers`
+
+
+ """
+ mp: Optional[Mapper[Any]] = self.bind_arguments.get("mapper", None)
+ return mp
+
+ @property
+ def all_mappers(self) -> Sequence[Mapper[Any]]:
+ """Return a sequence of all :class:`_orm.Mapper` objects that are
+ involved at the top level of this statement.
+
+ By "top level" we mean those :class:`_orm.Mapper` objects that would
+ be represented in the result set rows for a :func:`_sql.select`
+ query, or for a :func:`_dml.update` or :func:`_dml.delete` query,
+ the mapper that is the main subject of the UPDATE or DELETE.
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :attr:`_orm.ORMExecuteState.bind_mapper`
+
+
+
+ """
+ if not self.is_orm_statement:
+ return []
+ elif isinstance(self.statement, (Select, FromStatement)):
+ result = []
+ seen = set()
+ for d in self.statement.column_descriptions:
+ ent = d["entity"]
+ if ent:
+ insp = inspect(ent, raiseerr=False)
+ if insp and insp.mapper and insp.mapper not in seen:
+ seen.add(insp.mapper)
+ result.append(insp.mapper)
+ return result
+ elif self.statement.is_dml and self.bind_mapper:
+ return [self.bind_mapper]
+ else:
+ return []
+
+ @property
+ def is_orm_statement(self) -> bool:
+ """return True if the operation is an ORM statement.
+
+ This indicates that the select(), insert(), update(), or delete()
+ being invoked contains ORM entities as subjects. For a statement
+ that does not have ORM entities and instead refers only to
+ :class:`.Table` metadata, it is invoked as a Core SQL statement
+ and no ORM-level automation takes place.
+
+ """
+ return self._compile_state_cls is not None
+
+ @property
+ def is_executemany(self) -> bool:
+ """return True if the parameters are a multi-element list of
+ dictionaries with more than one dictionary.
+
+ .. versionadded:: 2.0
+
+ """
+ return isinstance(self.parameters, list)
+
+ @property
+ def is_select(self) -> bool:
+ """return True if this is a SELECT operation."""
+ return self.statement.is_select
+
+ @property
+ def is_insert(self) -> bool:
+ """return True if this is an INSERT operation."""
+ return self.statement.is_dml and self.statement.is_insert
+
+ @property
+ def is_update(self) -> bool:
+ """return True if this is an UPDATE operation."""
+ return self.statement.is_dml and self.statement.is_update
+
+ @property
+ def is_delete(self) -> bool:
+ """return True if this is a DELETE operation."""
+ return self.statement.is_dml and self.statement.is_delete
+
+ @property
+ def _is_crud(self) -> bool:
+ return isinstance(self.statement, (dml.Update, dml.Delete))
+
+ def update_execution_options(self, **opts: Any) -> None:
+ """Update the local execution options with new values."""
+ self.local_execution_options = self.local_execution_options.union(opts)
+
+ def _orm_compile_options(
+ self,
+ ) -> Optional[
+ Union[
+ context.ORMCompileState.default_compile_options,
+ Type[context.ORMCompileState.default_compile_options],
+ ]
+ ]:
+ if not self.is_select:
+ return None
+ try:
+ opts = self.statement._compile_options
+ except AttributeError:
+ return None
+
+ if opts is not None and opts.isinstance(
+ context.ORMCompileState.default_compile_options
+ ):
+ return opts # type: ignore
+ else:
+ return None
+
+ @property
+ def lazy_loaded_from(self) -> Optional[InstanceState[Any]]:
+ """An :class:`.InstanceState` that is using this statement execution
+ for a lazy load operation.
+
+ The primary rationale for this attribute is to support the horizontal
+ sharding extension, where it is available within specific query
+ execution time hooks created by this extension. To that end, the
+ attribute is only intended to be meaningful at **query execution
+ time**, and importantly not any time prior to that, including query
+ compilation time.
+
+ """
+ return self.load_options._lazy_loaded_from
+
+ @property
+ def loader_strategy_path(self) -> Optional[PathRegistry]:
+ """Return the :class:`.PathRegistry` for the current load path.
+
+ This object represents the "path" in a query along relationships
+ when a particular object or collection is being loaded.
+
+ """
+ opts = self._orm_compile_options()
+ if opts is not None:
+ return opts._current_path
+ else:
+ return None
+
+ @property
+ def is_column_load(self) -> bool:
+ """Return True if the operation is refreshing column-oriented
+ attributes on an existing ORM object.
+
+ This occurs during operations such as :meth:`_orm.Session.refresh`,
+ as well as when an attribute deferred by :func:`_orm.defer` is
+ being loaded, or an attribute that was expired either directly
+ by :meth:`_orm.Session.expire` or via a commit operation is being
+ loaded.
+
+ Handlers will very likely not want to add any options to queries
+ when such an operation is occurring as the query should be a straight
+ primary key fetch which should not have any additional WHERE criteria,
+ and loader options travelling with the instance
+ will have already been added to the query.
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :attr:`_orm.ORMExecuteState.is_relationship_load`
+
+ """
+ opts = self._orm_compile_options()
+ return opts is not None and opts._for_refresh_state
+
+ @property
+ def is_relationship_load(self) -> bool:
+ """Return True if this load is loading objects on behalf of a
+ relationship.
+
+ This means, the loader in effect is either a LazyLoader,
+ SelectInLoader, SubqueryLoader, or similar, and the entire
+ SELECT statement being emitted is on behalf of a relationship
+ load.
+
+ Handlers will very likely not want to add any options to queries
+ when such an operation is occurring, as loader options are already
+ capable of being propagated to relationship loaders and should
+ be already present.
+
+ .. seealso::
+
+ :attr:`_orm.ORMExecuteState.is_column_load`
+
+ """
+ opts = self._orm_compile_options()
+ if opts is None:
+ return False
+ path = self.loader_strategy_path
+ return path is not None and not path.is_root
+
+ @property
+ def load_options(
+ self,
+ ) -> Union[
+ context.QueryContext.default_load_options,
+ Type[context.QueryContext.default_load_options],
+ ]:
+ """Return the load_options that will be used for this execution."""
+
+ if not self.is_select:
+ raise sa_exc.InvalidRequestError(
+ "This ORM execution is not against a SELECT statement "
+ "so there are no load options."
+ )
+
+ lo: Union[
+ context.QueryContext.default_load_options,
+ Type[context.QueryContext.default_load_options],
+ ] = self.execution_options.get(
+ "_sa_orm_load_options", context.QueryContext.default_load_options
+ )
+ return lo
+
+ @property
+ def update_delete_options(
+ self,
+ ) -> Union[
+ bulk_persistence.BulkUDCompileState.default_update_options,
+ Type[bulk_persistence.BulkUDCompileState.default_update_options],
+ ]:
+ """Return the update_delete_options that will be used for this
+ execution."""
+
+ if not self._is_crud:
+ raise sa_exc.InvalidRequestError(
+ "This ORM execution is not against an UPDATE or DELETE "
+ "statement so there are no update options."
+ )
+ uo: Union[
+ bulk_persistence.BulkUDCompileState.default_update_options,
+ Type[bulk_persistence.BulkUDCompileState.default_update_options],
+ ] = self.execution_options.get(
+ "_sa_orm_update_options",
+ bulk_persistence.BulkUDCompileState.default_update_options,
+ )
+ return uo
+
+ @property
+ def _non_compile_orm_options(self) -> Sequence[ORMOption]:
+ return [
+ opt
+ for opt in self.statement._with_options
+ if is_orm_option(opt) and not opt._is_compile_state
+ ]
+
+ @property
+ def user_defined_options(self) -> Sequence[UserDefinedOption]:
+ """The sequence of :class:`.UserDefinedOptions` that have been
+ associated with the statement being invoked.
+
+ """
+ return [
+ opt
+ for opt in self.statement._with_options
+ if is_user_defined_option(opt)
+ ]
+
+
+class SessionTransactionOrigin(Enum):
+ """indicates the origin of a :class:`.SessionTransaction`.
+
+ This enumeration is present on the
+ :attr:`.SessionTransaction.origin` attribute of any
+ :class:`.SessionTransaction` object.
+
+ .. versionadded:: 2.0
+
+ """
+
+ AUTOBEGIN = 0
+ """transaction were started by autobegin"""
+
+ BEGIN = 1
+ """transaction were started by calling :meth:`_orm.Session.begin`"""
+
+ BEGIN_NESTED = 2
+ """tranaction were started by :meth:`_orm.Session.begin_nested`"""
+
+ SUBTRANSACTION = 3
+ """transaction is an internal "subtransaction" """
+
+
+class SessionTransaction(_StateChange, TransactionalContext):
+ """A :class:`.Session`-level 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.
+
+ Documentation on interacting with :class:`_orm.SessionTransaction` is
+ at: :ref:`unitofwork_transaction`.
+
+
+ .. 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`
+
+ :meth:`.Session.rollback`
+
+ :meth:`.Session.commit`
+
+ :meth:`.Session.in_transaction`
+
+ :meth:`.Session.in_nested_transaction`
+
+ :meth:`.Session.get_transaction`
+
+ :meth:`.Session.get_nested_transaction`
+
+
+ """
+
+ _rollback_exception: Optional[BaseException] = None
+
+ _connections: Dict[
+ Union[Engine, Connection], Tuple[Connection, Transaction, bool, bool]
+ ]
+ session: Session
+ _parent: Optional[SessionTransaction]
+
+ _state: SessionTransactionState
+
+ _new: weakref.WeakKeyDictionary[InstanceState[Any], object]
+ _deleted: weakref.WeakKeyDictionary[InstanceState[Any], object]
+ _dirty: weakref.WeakKeyDictionary[InstanceState[Any], object]
+ _key_switches: weakref.WeakKeyDictionary[
+ InstanceState[Any], Tuple[Any, Any]
+ ]
+
+ origin: SessionTransactionOrigin
+ """Origin of this :class:`_orm.SessionTransaction`.
+
+ Refers to a :class:`.SessionTransactionOrigin` instance which is an
+ enumeration indicating the source event that led to constructing
+ this :class:`_orm.SessionTransaction`.
+
+ .. versionadded:: 2.0
+
+ """
+
+ nested: bool = False
+ """Indicates if this is a nested, or SAVEPOINT, transaction.
+
+ When :attr:`.SessionTransaction.nested` is True, it is expected
+ that :attr:`.SessionTransaction.parent` will be present as well,
+ linking to the enclosing :class:`.SessionTransaction`.
+
+ .. seealso::
+
+ :attr:`.SessionTransaction.origin`
+
+ """
+
+ def __init__(
+ self,
+ session: Session,
+ origin: SessionTransactionOrigin,
+ parent: Optional[SessionTransaction] = None,
+ ):
+ TransactionalContext._trans_ctx_check(session)
+
+ self.session = session
+ self._connections = {}
+ self._parent = parent
+ self.nested = nested = origin is SessionTransactionOrigin.BEGIN_NESTED
+ self.origin = origin
+
+ if session._close_state is _SessionCloseState.CLOSED:
+ raise sa_exc.InvalidRequestError(
+ "This Session has been permanently closed and is unable "
+ "to handle any more transaction requests."
+ )
+
+ if nested:
+ if not parent:
+ raise sa_exc.InvalidRequestError(
+ "Can't start a SAVEPOINT transaction when no existing "
+ "transaction is in progress"
+ )
+
+ self._previous_nested_transaction = session._nested_transaction
+ elif origin is SessionTransactionOrigin.SUBTRANSACTION:
+ assert parent is not None
+ else:
+ assert parent is None
+
+ self._state = SessionTransactionState.ACTIVE
+
+ self._take_snapshot()
+
+ # make sure transaction is assigned before we call the
+ # dispatch
+ self.session._transaction = self
+
+ self.session.dispatch.after_transaction_create(self.session, self)
+
+ def _raise_for_prerequisite_state(
+ self, operation_name: str, state: _StateChangeState
+ ) -> NoReturn:
+ if state is SessionTransactionState.DEACTIVE:
+ if self._rollback_exception:
+ raise sa_exc.PendingRollbackError(
+ "This Session's transaction has been rolled back "
+ "due to a previous exception during flush."
+ " To begin a new transaction with this Session, "
+ "first issue Session.rollback()."
+ f" Original exception was: {self._rollback_exception}",
+ code="7s2a",
+ )
+ else:
+ raise sa_exc.InvalidRequestError(
+ "This session is in 'inactive' state, due to the "
+ "SQL transaction being rolled back; no further SQL "
+ "can be emitted within this transaction."
+ )
+ elif state is SessionTransactionState.CLOSED:
+ raise sa_exc.ResourceClosedError("This transaction is closed")
+ elif state is SessionTransactionState.PROVISIONING_CONNECTION:
+ raise sa_exc.InvalidRequestError(
+ "This session is provisioning a new connection; concurrent "
+ "operations are not permitted",
+ code="isce",
+ )
+ else:
+ raise sa_exc.InvalidRequestError(
+ f"This session is in '{state.name.lower()}' state; no "
+ "further SQL can be emitted within this transaction."
+ )
+
+ @property
+ def parent(self) -> Optional[SessionTransaction]:
+ """The parent :class:`.SessionTransaction` of this
+ :class:`.SessionTransaction`.
+
+ If this attribute is ``None``, indicates this
+ :class:`.SessionTransaction` is at the top of the stack, and
+ corresponds to a real "COMMIT"/"ROLLBACK"
+ block. If non-``None``, then this is either a "subtransaction"
+ (an internal marker object used by the flush process) or a
+ "nested" / SAVEPOINT transaction. If the
+ :attr:`.SessionTransaction.nested` attribute is ``True``, then
+ this is a SAVEPOINT, and if ``False``, indicates this a subtransaction.
+
+ """
+ return self._parent
+
+ @property
+ def is_active(self) -> bool:
+ return (
+ self.session is not None
+ and self._state is SessionTransactionState.ACTIVE
+ )
+
+ @property
+ def _is_transaction_boundary(self) -> bool:
+ return self.nested or not self._parent
+
+ @_StateChange.declare_states(
+ (SessionTransactionState.ACTIVE,), _StateChangeStates.NO_CHANGE
+ )
+ def connection(
+ self,
+ bindkey: Optional[Mapper[Any]],
+ execution_options: Optional[_ExecuteOptions] = None,
+ **kwargs: Any,
+ ) -> Connection:
+ bind = self.session.get_bind(bindkey, **kwargs)
+ return self._connection_for_bind(bind, execution_options)
+
+ @_StateChange.declare_states(
+ (SessionTransactionState.ACTIVE,), _StateChangeStates.NO_CHANGE
+ )
+ def _begin(self, nested: bool = False) -> SessionTransaction:
+ return SessionTransaction(
+ self.session,
+ (
+ SessionTransactionOrigin.BEGIN_NESTED
+ if nested
+ else SessionTransactionOrigin.SUBTRANSACTION
+ ),
+ self,
+ )
+
+ def _iterate_self_and_parents(
+ self, upto: Optional[SessionTransaction] = None
+ ) -> Iterable[SessionTransaction]:
+ current = self
+ result: Tuple[SessionTransaction, ...] = ()
+ while current:
+ result += (current,)
+ if current._parent is upto:
+ break
+ elif current._parent is None:
+ raise sa_exc.InvalidRequestError(
+ "Transaction %s is not on the active transaction list"
+ % (upto)
+ )
+ else:
+ current = current._parent
+
+ return result
+
+ def _take_snapshot(self) -> None:
+ if not self._is_transaction_boundary:
+ parent = self._parent
+ assert parent is not None
+ self._new = parent._new
+ self._deleted = parent._deleted
+ self._dirty = parent._dirty
+ self._key_switches = parent._key_switches
+ return
+
+ is_begin = self.origin in (
+ SessionTransactionOrigin.BEGIN,
+ SessionTransactionOrigin.AUTOBEGIN,
+ )
+ if not is_begin and not self.session._flushing:
+ self.session.flush()
+
+ self._new = weakref.WeakKeyDictionary()
+ self._deleted = weakref.WeakKeyDictionary()
+ self._dirty = weakref.WeakKeyDictionary()
+ self._key_switches = weakref.WeakKeyDictionary()
+
+ def _restore_snapshot(self, dirty_only: bool = False) -> None:
+ """Restore the restoration state taken before a transaction began.
+
+ Corresponds to a rollback.
+
+ """
+ assert self._is_transaction_boundary
+
+ to_expunge = set(self._new).union(self.session._new)
+ self.session._expunge_states(to_expunge, to_transient=True)
+
+ for s, (oldkey, newkey) in self._key_switches.items():
+ # we probably can do this conditionally based on
+ # if we expunged or not, but safe_discard does that anyway
+ self.session.identity_map.safe_discard(s)
+
+ # restore the old key
+ s.key = oldkey
+
+ # now restore the object, but only if we didn't expunge
+ if s not in to_expunge:
+ self.session.identity_map.replace(s)
+
+ for s in set(self._deleted).union(self.session._deleted):
+ self.session._update_impl(s, revert_deletion=True)
+
+ assert not self.session._deleted
+
+ for s in self.session.identity_map.all_states():
+ if not dirty_only or s.modified or s in self._dirty:
+ s._expire(s.dict, self.session.identity_map._modified)
+
+ def _remove_snapshot(self) -> None:
+ """Remove the restoration state taken before a transaction began.
+
+ Corresponds to a commit.
+
+ """
+ assert self._is_transaction_boundary
+
+ if not self.nested and self.session.expire_on_commit:
+ for s in self.session.identity_map.all_states():
+ s._expire(s.dict, self.session.identity_map._modified)
+
+ statelib.InstanceState._detach_states(
+ list(self._deleted), self.session
+ )
+ self._deleted.clear()
+ elif self.nested:
+ parent = self._parent
+ assert parent is not None
+ parent._new.update(self._new)
+ parent._dirty.update(self._dirty)
+ parent._deleted.update(self._deleted)
+ parent._key_switches.update(self._key_switches)
+
+ @_StateChange.declare_states(
+ (SessionTransactionState.ACTIVE,), _StateChangeStates.NO_CHANGE
+ )
+ def _connection_for_bind(
+ self,
+ bind: _SessionBind,
+ execution_options: Optional[CoreExecuteOptionsParameter],
+ ) -> Connection:
+ if bind in self._connections:
+ if execution_options:
+ util.warn(
+ "Connection is already established for the "
+ "given bind; execution_options ignored"
+ )
+ return self._connections[bind][0]
+
+ self._state = SessionTransactionState.PROVISIONING_CONNECTION
+
+ local_connect = False
+ should_commit = True
+
+ try:
+ if self._parent:
+ conn = self._parent._connection_for_bind(
+ bind, execution_options
+ )
+ if not self.nested:
+ return conn
+ else:
+ if isinstance(bind, engine.Connection):
+ conn = bind
+ if conn.engine in self._connections:
+ raise sa_exc.InvalidRequestError(
+ "Session already has a Connection associated "
+ "for the given Connection's Engine"
+ )
+ else:
+ conn = bind.connect()
+ local_connect = True
+
+ try:
+ if execution_options:
+ conn = conn.execution_options(**execution_options)
+
+ transaction: Transaction
+ if self.session.twophase and self._parent is None:
+ # TODO: shouldn't we only be here if not
+ # conn.in_transaction() ?
+ # if twophase is set and conn.in_transaction(), validate
+ # that it is in fact twophase.
+ transaction = conn.begin_twophase()
+ elif self.nested:
+ transaction = conn.begin_nested()
+ elif conn.in_transaction():
+ join_transaction_mode = self.session.join_transaction_mode
+
+ if join_transaction_mode == "conditional_savepoint":
+ if conn.in_nested_transaction():
+ join_transaction_mode = "create_savepoint"
+ else:
+ join_transaction_mode = "rollback_only"
+
+ if join_transaction_mode in (
+ "control_fully",
+ "rollback_only",
+ ):
+ if conn.in_nested_transaction():
+ transaction = (
+ conn._get_required_nested_transaction()
+ )
+ else:
+ transaction = conn._get_required_transaction()
+ if join_transaction_mode == "rollback_only":
+ should_commit = False
+ elif join_transaction_mode == "create_savepoint":
+ transaction = conn.begin_nested()
+ else:
+ assert False, join_transaction_mode
+ 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
+ if local_connect:
+ conn.close()
+ raise
+ else:
+ bind_is_connection = isinstance(bind, engine.Connection)
+
+ self._connections[conn] = self._connections[conn.engine] = (
+ conn,
+ transaction,
+ should_commit,
+ not bind_is_connection,
+ )
+ self.session.dispatch.after_begin(self.session, self, conn)
+ return conn
+ finally:
+ self._state = SessionTransactionState.ACTIVE
+
+ def prepare(self) -> None:
+ if self._parent is not None or not self.session.twophase:
+ raise sa_exc.InvalidRequestError(
+ "'twophase' mode not enabled, or not root transaction; "
+ "can't prepare."
+ )
+ self._prepare_impl()
+
+ @_StateChange.declare_states(
+ (SessionTransactionState.ACTIVE,), SessionTransactionState.PREPARED
+ )
+ def _prepare_impl(self) -> None:
+ if self._parent is None or self.nested:
+ self.session.dispatch.before_commit(self.session)
+
+ stx = self.session._transaction
+ assert stx is not None
+ if stx is not self:
+ for subtransaction in stx._iterate_self_and_parents(upto=self):
+ subtransaction.commit()
+
+ if not self.session._flushing:
+ for _flush_guard in range(100):
+ if self.session._is_clean():
+ break
+ self.session.flush()
+ else:
+ raise exc.FlushError(
+ "Over 100 subsequent flushes have occurred within "
+ "session.commit() - is an after_flush() hook "
+ "creating new objects?"
+ )
+
+ if self._parent is None and self.session.twophase:
+ try:
+ for t in set(self._connections.values()):
+ cast("TwoPhaseTransaction", t[1]).prepare()
+ except:
+ with util.safe_reraise():
+ self.rollback()
+
+ self._state = SessionTransactionState.PREPARED
+
+ @_StateChange.declare_states(
+ (SessionTransactionState.ACTIVE, SessionTransactionState.PREPARED),
+ SessionTransactionState.CLOSED,
+ )
+ def commit(self, _to_root: bool = False) -> None:
+ if self._state is not SessionTransactionState.PREPARED:
+ with self._expect_state(SessionTransactionState.PREPARED):
+ self._prepare_impl()
+
+ if self._parent is None or self.nested:
+ for conn, trans, should_commit, autoclose in set(
+ self._connections.values()
+ ):
+ if should_commit:
+ trans.commit()
+
+ self._state = SessionTransactionState.COMMITTED
+ self.session.dispatch.after_commit(self.session)
+
+ self._remove_snapshot()
+
+ with self._expect_state(SessionTransactionState.CLOSED):
+ self.close()
+
+ if _to_root and self._parent:
+ self._parent.commit(_to_root=True)
+
+ @_StateChange.declare_states(
+ (
+ SessionTransactionState.ACTIVE,
+ SessionTransactionState.DEACTIVE,
+ SessionTransactionState.PREPARED,
+ ),
+ SessionTransactionState.CLOSED,
+ )
+ def rollback(
+ self, _capture_exception: bool = False, _to_root: bool = False
+ ) -> None:
+ stx = self.session._transaction
+ assert stx is not None
+ if stx is not self:
+ for subtransaction in stx._iterate_self_and_parents(upto=self):
+ subtransaction.close()
+
+ boundary = self
+ rollback_err = None
+ if self._state in (
+ SessionTransactionState.ACTIVE,
+ SessionTransactionState.PREPARED,
+ ):
+ for transaction in self._iterate_self_and_parents():
+ if transaction._parent is None or transaction.nested:
+ try:
+ for t in set(transaction._connections.values()):
+ t[1].rollback()
+
+ transaction._state = SessionTransactionState.DEACTIVE
+ self.session.dispatch.after_rollback(self.session)
+ except:
+ rollback_err = sys.exc_info()
+ finally:
+ transaction._state = SessionTransactionState.DEACTIVE
+ transaction._restore_snapshot(
+ dirty_only=transaction.nested
+ )
+ boundary = transaction
+ break
+ else:
+ transaction._state = SessionTransactionState.DEACTIVE
+
+ sess = self.session
+
+ if not rollback_err and not sess._is_clean():
+ # if items were added, deleted, or mutated
+ # here, we need to re-restore the snapshot
+ util.warn(
+ "Session's state has been changed on "
+ "a non-active transaction - this state "
+ "will be discarded."
+ )
+ boundary._restore_snapshot(dirty_only=boundary.nested)
+
+ with self._expect_state(SessionTransactionState.CLOSED):
+ self.close()
+
+ if self._parent and _capture_exception:
+ self._parent._rollback_exception = sys.exc_info()[1]
+
+ if rollback_err and rollback_err[1]:
+ raise rollback_err[1].with_traceback(rollback_err[2])
+
+ sess.dispatch.after_soft_rollback(sess, self)
+
+ if _to_root and self._parent:
+ self._parent.rollback(_to_root=True)
+
+ @_StateChange.declare_states(
+ _StateChangeStates.ANY, SessionTransactionState.CLOSED
+ )
+ def close(self, invalidate: bool = False) -> None:
+ if self.nested:
+ self.session._nested_transaction = (
+ self._previous_nested_transaction
+ )
+
+ self.session._transaction = self._parent
+
+ for connection, transaction, should_commit, autoclose in set(
+ self._connections.values()
+ ):
+ if invalidate and self._parent is None:
+ connection.invalidate()
+ if should_commit and transaction.is_active:
+ transaction.close()
+ if autoclose and self._parent is None:
+ connection.close()
+
+ self._state = SessionTransactionState.CLOSED
+ sess = self.session
+
+ # TODO: these two None sets were historically after the
+ # event hook below, and in 2.0 I changed it this way for some reason,
+ # and I remember there being a reason, but not what it was.
+ # Why do we need to get rid of them at all? test_memusage::CycleTest
+ # passes with these commented out.
+ # self.session = None # type: ignore
+ # self._connections = None # type: ignore
+
+ sess.dispatch.after_transaction_end(sess, self)
+
+ def _get_subject(self) -> Session:
+ return self.session
+
+ def _transaction_is_active(self) -> bool:
+ return self._state is SessionTransactionState.ACTIVE
+
+ def _transaction_is_closed(self) -> bool:
+ return self._state is SessionTransactionState.CLOSED
+
+ def _rollback_can_be_called(self) -> bool:
+ return self._state not in (COMMITTED, CLOSED)
+
+
+class _SessionCloseState(Enum):
+ ACTIVE = 1
+ CLOSED = 2
+ CLOSE_IS_RESET = 3
+
+
+class Session(_SessionClassMethods, EventTarget):
+ """Manages persistence operations for ORM-mapped objects.
+
+ The :class:`_orm.Session` is **not safe for use in concurrent threads.**.
+ See :ref:`session_faq_threadsafe` for background.
+
+ The Session's usage paradigm is described at :doc:`/orm/session`.
+
+
+ """
+
+ _is_asyncio = False
+
+ dispatch: dispatcher[Session]
+
+ identity_map: IdentityMap
+ """A mapping of object identities to objects themselves.
+
+ Iterating through ``Session.identity_map.values()`` provides
+ access to the full set of persistent objects (i.e., those
+ that have row identity) currently in the session.
+
+ .. seealso::
+
+ :func:`.identity_key` - helper function to produce the keys used
+ in this dictionary.
+
+ """
+
+ _new: Dict[InstanceState[Any], Any]
+ _deleted: Dict[InstanceState[Any], Any]
+ bind: Optional[Union[Engine, Connection]]
+ __binds: Dict[_SessionBindKey, _SessionBind]
+ _flushing: bool
+ _warn_on_events: bool
+ _transaction: Optional[SessionTransaction]
+ _nested_transaction: Optional[SessionTransaction]
+ hash_key: int
+ autoflush: bool
+ expire_on_commit: bool
+ enable_baked_queries: bool
+ twophase: bool
+ join_transaction_mode: JoinTransactionMode
+ _query_cls: Type[Query[Any]]
+ _close_state: _SessionCloseState
+
+ def __init__(
+ self,
+ bind: Optional[_SessionBind] = None,
+ *,
+ autoflush: bool = True,
+ future: Literal[True] = True,
+ expire_on_commit: bool = True,
+ autobegin: bool = True,
+ twophase: bool = False,
+ binds: Optional[Dict[_SessionBindKey, _SessionBind]] = None,
+ enable_baked_queries: bool = True,
+ info: Optional[_InfoType] = None,
+ query_cls: Optional[Type[Query[Any]]] = None,
+ autocommit: Literal[False] = False,
+ join_transaction_mode: JoinTransactionMode = "conditional_savepoint",
+ close_resets_only: Union[bool, _NoArg] = _NoArg.NO_ARG,
+ ):
+ r"""Construct a new :class:`_orm.Session`.
+
+ See also the :class:`.sessionmaker` function which is used to
+ generate a :class:`.Session`-producing callable with a given
+ set of arguments.
+
+ :param autoflush: When ``True``, all query operations will issue a
+ :meth:`~.Session.flush` call to this ``Session`` before proceeding.
+ This is a convenience feature so that :meth:`~.Session.flush` need
+ not be called repeatedly in order for database queries to retrieve
+ results.
+
+ .. seealso::
+
+ :ref:`session_flushing` - additional background on autoflush
+
+ :param autobegin: Automatically start transactions (i.e. equivalent to
+ invoking :meth:`_orm.Session.begin`) when database access is
+ requested by an operation. Defaults to ``True``. Set to
+ ``False`` to prevent a :class:`_orm.Session` from implicitly
+ beginning transactions after construction, as well as after any of
+ the :meth:`_orm.Session.rollback`, :meth:`_orm.Session.commit`,
+ or :meth:`_orm.Session.close` methods are called.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`session_autobegin_disable`
+
+ :param bind: An optional :class:`_engine.Engine` or
+ :class:`_engine.Connection` to
+ which this ``Session`` should be bound. When specified, all SQL
+ operations performed by this session will execute via this
+ connectable.
+
+ :param binds: A dictionary which may specify any number of
+ :class:`_engine.Engine` or :class:`_engine.Connection`
+ objects as the source of
+ connectivity for SQL operations on a per-entity basis. The keys
+ of the dictionary consist of any series of mapped classes,
+ arbitrary Python classes that are bases for mapped classes,
+ :class:`_schema.Table` objects and :class:`_orm.Mapper` objects.
+ The
+ values of the dictionary are then instances of
+ :class:`_engine.Engine`
+ or less commonly :class:`_engine.Connection` objects.
+ Operations which
+ proceed relative to a particular mapped class will consult this
+ dictionary for the closest matching entity in order to determine
+ which :class:`_engine.Engine` should be used for a particular SQL
+ operation. The complete heuristics for resolution are
+ described at :meth:`.Session.get_bind`. Usage looks like::
+
+ Session = sessionmaker(binds={
+ SomeMappedClass: create_engine('postgresql+psycopg2://engine1'),
+ SomeDeclarativeBase: create_engine('postgresql+psycopg2://engine2'),
+ some_mapper: create_engine('postgresql+psycopg2://engine3'),
+ some_table: create_engine('postgresql+psycopg2://engine4'),
+ })
+
+ .. seealso::
+
+ :ref:`session_partitioning`
+
+ :meth:`.Session.bind_mapper`
+
+ :meth:`.Session.bind_table`
+
+ :meth:`.Session.get_bind`
+
+
+ :param \class_: Specify an alternate class other than
+ ``sqlalchemy.orm.session.Session`` which should be used by the
+ returned class. This is the only argument that is local to the
+ :class:`.sessionmaker` function, and is not sent directly to the
+ constructor for ``Session``.
+
+ :param enable_baked_queries: legacy; defaults to ``True``.
+ A parameter consumed
+ by the :mod:`sqlalchemy.ext.baked` extension to determine if
+ "baked queries" should be cached, as is the normal operation
+ of this extension. When set to ``False``, caching as used by
+ this particular extension is disabled.
+
+ .. versionchanged:: 1.4 The ``sqlalchemy.ext.baked`` extension is
+ legacy and is not used by any of SQLAlchemy's internals. This
+ flag therefore only affects applications that are making explicit
+ use of this extension within their own code.
+
+ :param expire_on_commit: Defaults to ``True``. When ``True``, all
+ instances will be fully expired after each :meth:`~.commit`,
+ so that all attribute/object access subsequent to a completed
+ transaction will load from the most recent database state.
+
+ .. seealso::
+
+ :ref:`session_committing`
+
+ :param future: Deprecated; this flag is always True.
+
+ .. seealso::
+
+ :ref:`migration_20_toplevel`
+
+ :param info: optional dictionary of arbitrary data to be associated
+ with this :class:`.Session`. Is available via the
+ :attr:`.Session.info` attribute. Note the dictionary is copied at
+ construction time so that modifications to the per-
+ :class:`.Session` dictionary will be local to that
+ :class:`.Session`.
+
+ :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`.
+
+ :param twophase: When ``True``, all transactions will be started as
+ a "two phase" transaction, i.e. using the "two phase" semantics
+ of the database in use along with an XID. During a
+ :meth:`~.commit`, after :meth:`~.flush` has been issued for all
+ attached databases, the :meth:`~.TwoPhaseTransaction.prepare`
+ method on each database's :class:`.TwoPhaseTransaction` will be
+ called. This allows each database to roll back the entire
+ transaction, before each transaction is committed.
+
+ :param autocommit: the "autocommit" keyword is present for backwards
+ compatibility but must remain at its default value of ``False``.
+
+ :param join_transaction_mode: Describes the transactional behavior to
+ take when a given bind is a :class:`_engine.Connection` that
+ has already begun a transaction outside the scope of this
+ :class:`_orm.Session`; in other words the
+ :meth:`_engine.Connection.in_transaction()` method returns True.
+
+ The following behaviors only take effect when the :class:`_orm.Session`
+ **actually makes use of the connection given**; that is, a method
+ such as :meth:`_orm.Session.execute`, :meth:`_orm.Session.connection`,
+ etc. are actually invoked:
+
+ * ``"conditional_savepoint"`` - this is the default. if the given
+ :class:`_engine.Connection` is begun within a transaction but
+ does not have a SAVEPOINT, then ``"rollback_only"`` is used.
+ If the :class:`_engine.Connection` is additionally within
+ a SAVEPOINT, in other words
+ :meth:`_engine.Connection.in_nested_transaction()` method returns
+ True, then ``"create_savepoint"`` is used.
+
+ ``"conditional_savepoint"`` behavior attempts to make use of
+ savepoints in order to keep the state of the existing transaction
+ unchanged, but only if there is already a savepoint in progress;
+ otherwise, it is not assumed that the backend in use has adequate
+ support for SAVEPOINT, as availability of this feature varies.
+ ``"conditional_savepoint"`` also seeks to establish approximate
+ backwards compatibility with previous :class:`_orm.Session`
+ behavior, for applications that are not setting a specific mode. It
+ is recommended that one of the explicit settings be used.
+
+ * ``"create_savepoint"`` - the :class:`_orm.Session` will use
+ :meth:`_engine.Connection.begin_nested()` in all cases to create
+ its own transaction. This transaction by its nature rides
+ "on top" of any existing transaction that's opened on the given
+ :class:`_engine.Connection`; if the underlying database and
+ the driver in use has full, non-broken support for SAVEPOINT, the
+ external transaction will remain unaffected throughout the
+ lifespan of the :class:`_orm.Session`.
+
+ The ``"create_savepoint"`` mode is the most useful for integrating
+ a :class:`_orm.Session` into a test suite where an externally
+ initiated transaction should remain unaffected; however, it relies
+ on proper SAVEPOINT support from the underlying driver and
+ database.
+
+ .. tip:: When using SQLite, the SQLite driver included through
+ Python 3.11 does not handle SAVEPOINTs correctly in all cases
+ without workarounds. See the sections
+ :ref:`pysqlite_serializable` and :ref:`aiosqlite_serializable`
+ for details on current workarounds.
+
+ * ``"control_fully"`` - the :class:`_orm.Session` will take
+ control of the given transaction as its own;
+ :meth:`_orm.Session.commit` will call ``.commit()`` on the
+ transaction, :meth:`_orm.Session.rollback` will call
+ ``.rollback()`` on the transaction, :meth:`_orm.Session.close` will
+ call ``.rollback`` on the transaction.
+
+ .. tip:: This mode of use is equivalent to how SQLAlchemy 1.4 would
+ handle a :class:`_engine.Connection` given with an existing
+ SAVEPOINT (i.e. :meth:`_engine.Connection.begin_nested`); the
+ :class:`_orm.Session` would take full control of the existing
+ SAVEPOINT.
+
+ * ``"rollback_only"`` - the :class:`_orm.Session` will take control
+ of the given transaction for ``.rollback()`` calls only;
+ ``.commit()`` calls will not be propagated to the given
+ transaction. ``.close()`` calls will have no effect on the
+ given transaction.
+
+ .. tip:: This mode of use is equivalent to how SQLAlchemy 1.4 would
+ handle a :class:`_engine.Connection` given with an existing
+ regular database transaction (i.e.
+ :meth:`_engine.Connection.begin`); the :class:`_orm.Session`
+ would propagate :meth:`_orm.Session.rollback` calls to the
+ underlying transaction, but not :meth:`_orm.Session.commit` or
+ :meth:`_orm.Session.close` calls.
+
+ .. versionadded:: 2.0.0rc1
+
+ :param close_resets_only: Defaults to ``True``. Determines if
+ the session should reset itself after calling ``.close()``
+ or should pass in a no longer usable state, disabling re-use.
+
+ .. versionadded:: 2.0.22 added flag ``close_resets_only``.
+ A future SQLAlchemy version may change the default value of
+ this flag to ``False``.
+
+ .. seealso::
+
+ :ref:`session_closing` - Detail on the semantics of
+ :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
+
+ """ # noqa
+
+ # considering allowing the "autocommit" keyword to still be accepted
+ # as long as it's False, so that external test suites, oslo.db etc
+ # continue to function as the argument appears to be passed in lots
+ # of cases including in our own test suite
+ if autocommit:
+ raise sa_exc.ArgumentError(
+ "autocommit=True is no longer supported"
+ )
+ self.identity_map = identity.WeakInstanceDict()
+
+ if not future:
+ raise sa_exc.ArgumentError(
+ "The 'future' parameter passed to "
+ "Session() may only be set to True."
+ )
+
+ self._new = {} # InstanceState->object, strong refs object
+ self._deleted = {} # same
+ self.bind = bind
+ self.__binds = {}
+ self._flushing = False
+ self._warn_on_events = False
+ self._transaction = None
+ self._nested_transaction = None
+ self.hash_key = _new_sessionid()
+ self.autobegin = autobegin
+ self.autoflush = autoflush
+ self.expire_on_commit = expire_on_commit
+ self.enable_baked_queries = enable_baked_queries
+
+ # the idea is that at some point NO_ARG will warn that in the future
+ # the default will switch to close_resets_only=False.
+ if close_resets_only or close_resets_only is _NoArg.NO_ARG:
+ self._close_state = _SessionCloseState.CLOSE_IS_RESET
+ else:
+ self._close_state = _SessionCloseState.ACTIVE
+ if (
+ join_transaction_mode
+ and join_transaction_mode
+ not in JoinTransactionMode.__args__ # type: ignore
+ ):
+ raise sa_exc.ArgumentError(
+ f"invalid selection for join_transaction_mode: "
+ f'"{join_transaction_mode}"'
+ )
+ self.join_transaction_mode = join_transaction_mode
+
+ self.twophase = twophase
+ self._query_cls = query_cls if query_cls else query.Query
+ if info:
+ self.info.update(info)
+
+ if binds is not None:
+ for key, bind in binds.items():
+ self._add_bind(key, bind)
+
+ _sessions[self.hash_key] = self
+
+ # used by sqlalchemy.engine.util.TransactionalContext
+ _trans_context_manager: Optional[TransactionalContext] = None
+
+ connection_callable: Optional[_ConnectionCallableProto] = None
+
+ def __enter__(self: _S) -> _S:
+ return self
+
+ def __exit__(self, type_: Any, value: Any, traceback: Any) -> None:
+ self.close()
+
+ @contextlib.contextmanager
+ def _maker_context_manager(self: _S) -> Iterator[_S]:
+ with self:
+ with self.begin():
+ yield self
+
+ def in_transaction(self) -> bool:
+ """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) -> bool:
+ """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) -> Optional[SessionTransaction]:
+ """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) -> Optional[SessionTransaction]:
+ """Return the current nested transaction in progress, if any.
+
+ .. versionadded:: 1.4
+
+ """
+
+ return self._nested_transaction
+
+ @util.memoized_property
+ def info(self) -> _InfoType:
+ """A user-modifiable dictionary.
+
+ The initial value of this dictionary can be populated using the
+ ``info`` argument to the :class:`.Session` constructor or
+ :class:`.sessionmaker` constructor or factory methods. The dictionary
+ here is always local to this :class:`.Session` and can be modified
+ independently of all other :class:`.Session` objects.
+
+ """
+ return {}
+
+ def _autobegin_t(self, begin: bool = False) -> SessionTransaction:
+ if self._transaction is None:
+ if not begin and not self.autobegin:
+ raise sa_exc.InvalidRequestError(
+ "Autobegin is disabled on this Session; please call "
+ "session.begin() to start a new transaction"
+ )
+ trans = SessionTransaction(
+ self,
+ (
+ SessionTransactionOrigin.BEGIN
+ if begin
+ else SessionTransactionOrigin.AUTOBEGIN
+ ),
+ )
+ assert self._transaction is trans
+ return trans
+
+ return self._transaction
+
+ def begin(self, nested: bool = False) -> SessionTransaction:
+ """Begin a transaction, or nested transaction,
+ on this :class:`.Session`, if one is not already begun.
+
+ The :class:`_orm.Session` object features **autobegin** behavior,
+ so that normally it is not necessary to call the
+ :meth:`_orm.Session.begin`
+ method explicitly. However, it may be used in order to control
+ the scope of when the transactional state is begun.
+
+ When used to begin the outermost transaction, an error is raised
+ if this :class:`.Session` is already inside of a transaction.
+
+ :param nested: if True, begins a SAVEPOINT transaction and is
+ equivalent to calling :meth:`~.Session.begin_nested`. For
+ documentation on SAVEPOINT transactions, please see
+ :ref:`session_begin_nested`.
+
+ :return: the :class:`.SessionTransaction` object. Note that
+ :class:`.SessionTransaction`
+ acts as a Python context manager, allowing :meth:`.Session.begin`
+ to be used in a "with" block. See :ref:`session_explicit_begin` for
+ an example.
+
+ .. seealso::
+
+ :ref:`session_autobegin`
+
+ :ref:`unitofwork_transaction`
+
+ :meth:`.Session.begin_nested`
+
+
+ """
+
+ trans = self._transaction
+ if trans is None:
+ trans = self._autobegin_t(begin=True)
+
+ if not nested:
+ return trans
+
+ assert trans is not None
+
+ if nested:
+ trans = trans._begin(nested=nested)
+ assert self._transaction is trans
+ self._nested_transaction = trans
+ else:
+ raise sa_exc.InvalidRequestError(
+ "A transaction is already begun on this Session."
+ )
+
+ return trans # needed for __enter__/__exit__ hook
+
+ def begin_nested(self) -> SessionTransaction:
+ """Begin a "nested" transaction on this Session, e.g. SAVEPOINT.
+
+ The target database(s) and associated drivers must support SQL
+ SAVEPOINT for this method to function correctly.
+
+ For documentation on SAVEPOINT
+ transactions, please see :ref:`session_begin_nested`.
+
+ :return: the :class:`.SessionTransaction` object. Note that
+ :class:`.SessionTransaction` acts as a context manager, allowing
+ :meth:`.Session.begin_nested` to be used in a "with" block.
+ See :ref:`session_begin_nested` for a usage example.
+
+ .. seealso::
+
+ :ref:`session_begin_nested`
+
+ :ref:`pysqlite_serializable` - special workarounds required
+ with the SQLite driver in order for SAVEPOINT to work
+ correctly. For asyncio use cases, see the section
+ :ref:`aiosqlite_serializable`.
+
+ """
+ return self.begin(nested=True)
+
+ def rollback(self) -> None:
+ """Rollback the current transaction in progress.
+
+ If no transaction is in progress, this method is a pass-through.
+
+ The method always rolls back
+ the topmost database transaction, discarding any nested
+ transactions that may be in progress.
+
+ .. seealso::
+
+ :ref:`session_rollback`
+
+ :ref:`unitofwork_transaction`
+
+ """
+ if self._transaction is None:
+ pass
+ else:
+ self._transaction.rollback(_to_root=True)
+
+ def commit(self) -> None:
+ """Flush pending changes and commit the current transaction.
+
+ When the COMMIT operation is complete, all objects are fully
+ :term:`expired`, erasing their internal contents, which will be
+ automatically re-loaded when the objects are next accessed. In the
+ interim, these objects are in an expired state and will not function if
+ they are :term:`detached` from the :class:`.Session`. Additionally,
+ this re-load operation is not supported when using asyncio-oriented
+ APIs. The :paramref:`.Session.expire_on_commit` parameter may be used
+ to disable this behavior.
+
+ When there is no transaction in place for the :class:`.Session`,
+ indicating that no operations were invoked on this :class:`.Session`
+ since the previous call to :meth:`.Session.commit`, the method will
+ begin and commit an internal-only "logical" transaction, that does not
+ normally affect the database unless pending flush changes were
+ detected, but will still invoke event handlers and object expiration
+ rules.
+
+ The outermost database transaction is committed unconditionally,
+ automatically releasing any SAVEPOINTs in effect.
+
+ .. seealso::
+
+ :ref:`session_committing`
+
+ :ref:`unitofwork_transaction`
+
+ :ref:`asyncio_orm_avoid_lazyloads`
+
+ """
+ trans = self._transaction
+ if trans is None:
+ trans = self._autobegin_t()
+
+ trans.commit(_to_root=True)
+
+ def prepare(self) -> None:
+ """Prepare the current transaction in progress for two phase commit.
+
+ If no transaction is in progress, this method raises an
+ :exc:`~sqlalchemy.exc.InvalidRequestError`.
+
+ Only root transactions of two phase sessions can be prepared. If the
+ current transaction is not such, an
+ :exc:`~sqlalchemy.exc.InvalidRequestError` is raised.
+
+ """
+ trans = self._transaction
+ if trans is None:
+ trans = self._autobegin_t()
+
+ trans.prepare()
+
+ def connection(
+ self,
+ bind_arguments: Optional[_BindArguments] = None,
+ execution_options: Optional[CoreExecuteOptionsParameter] = None,
+ ) -> Connection:
+ r"""Return a :class:`_engine.Connection` object corresponding to this
+ :class:`.Session` object's transactional state.
+
+ Either the :class:`_engine.Connection` corresponding to the current
+ transaction is returned, or if no transaction is in progress, a new
+ one is begun and the :class:`_engine.Connection`
+ returned (note that no
+ transactional state is established with the DBAPI until the first
+ SQL statement is emitted).
+
+ Ambiguity in multi-bind or unbound :class:`.Session` objects can be
+ resolved through any of the optional keyword arguments. This
+ ultimately makes usage of the :meth:`.get_bind` method for resolution.
+
+ :param bind_arguments: dictionary of bind arguments. May include
+ "mapper", "bind", "clause", other custom arguments that are passed
+ to :meth:`.Session.get_bind`.
+
+ :param execution_options: a dictionary of execution options that will
+ be passed to :meth:`_engine.Connection.execution_options`, **when the
+ connection is first procured only**. If the connection is already
+ present within the :class:`.Session`, a warning is emitted and
+ the arguments are ignored.
+
+ .. seealso::
+
+ :ref:`session_transaction_isolation`
+
+ """
+
+ if bind_arguments:
+ bind = bind_arguments.pop("bind", None)
+
+ if bind is None:
+ bind = self.get_bind(**bind_arguments)
+ else:
+ bind = self.get_bind()
+
+ return self._connection_for_bind(
+ bind,
+ execution_options=execution_options,
+ )
+
+ def _connection_for_bind(
+ self,
+ engine: _SessionBind,
+ execution_options: Optional[CoreExecuteOptionsParameter] = None,
+ **kw: Any,
+ ) -> Connection:
+ TransactionalContext._trans_ctx_check(self)
+
+ trans = self._transaction
+ if trans is None:
+ trans = self._autobegin_t()
+ return trans._connection_for_bind(engine, execution_options)
+
+ @overload
+ def _execute_internal(
+ self,
+ statement: Executable,
+ params: Optional[_CoreSingleExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ _scalar_result: Literal[True] = ...,
+ ) -> Any: ...
+
+ @overload
+ def _execute_internal(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ _scalar_result: bool = ...,
+ ) -> Result[Any]: ...
+
+ def _execute_internal(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ _scalar_result: bool = False,
+ ) -> Any:
+ statement = coercions.expect(roles.StatementRole, statement)
+
+ if not bind_arguments:
+ bind_arguments = {}
+ else:
+ bind_arguments = dict(bind_arguments)
+
+ if (
+ statement._propagate_attrs.get("compile_state_plugin", None)
+ == "orm"
+ ):
+ compile_state_cls = CompileState._get_plugin_class_for_plugin(
+ statement, "orm"
+ )
+ if TYPE_CHECKING:
+ assert isinstance(
+ compile_state_cls, context.AbstractORMCompileState
+ )
+ else:
+ compile_state_cls = None
+ bind_arguments.setdefault("clause", statement)
+
+ execution_options = util.coerce_to_immutabledict(execution_options)
+
+ if _parent_execute_state:
+ events_todo = _parent_execute_state._remaining_events()
+ else:
+ events_todo = self.dispatch.do_orm_execute
+ if _add_event:
+ events_todo = list(events_todo) + [_add_event]
+
+ if events_todo:
+ if compile_state_cls is not None:
+ # for event handlers, do the orm_pre_session_exec
+ # pass ahead of the event handlers, so that things like
+ # .load_options, .update_delete_options etc. are populated.
+ # is_pre_event=True allows the hook to hold off on things
+ # it doesn't want to do twice, including autoflush as well
+ # as "pre fetch" for DML, etc.
+ (
+ statement,
+ execution_options,
+ ) = compile_state_cls.orm_pre_session_exec(
+ self,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ True,
+ )
+
+ orm_exec_state = ORMExecuteState(
+ self,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ compile_state_cls,
+ events_todo,
+ )
+ for idx, fn in enumerate(events_todo):
+ orm_exec_state._starting_event_idx = idx
+ fn_result: Optional[Result[Any]] = fn(orm_exec_state)
+ if fn_result:
+ if _scalar_result:
+ return fn_result.scalar()
+ else:
+ return fn_result
+
+ statement = orm_exec_state.statement
+ execution_options = orm_exec_state.local_execution_options
+
+ if compile_state_cls is not None:
+ # now run orm_pre_session_exec() "for real". if there were
+ # event hooks, this will re-run the steps that interpret
+ # new execution_options into load_options / update_delete_options,
+ # which we assume the event hook might have updated.
+ # autoflush will also be invoked in this step if enabled.
+ (
+ statement,
+ execution_options,
+ ) = compile_state_cls.orm_pre_session_exec(
+ self,
+ statement,
+ params,
+ execution_options,
+ bind_arguments,
+ False,
+ )
+
+ bind = self.get_bind(**bind_arguments)
+
+ conn = self._connection_for_bind(bind)
+
+ if _scalar_result and not compile_state_cls:
+ if TYPE_CHECKING:
+ params = cast(_CoreSingleExecuteParams, params)
+ return conn.scalar(
+ statement, params or {}, execution_options=execution_options
+ )
+
+ if compile_state_cls:
+ result: Result[Any] = compile_state_cls.orm_execute_statement(
+ self,
+ statement,
+ params or {},
+ execution_options,
+ bind_arguments,
+ conn,
+ )
+ else:
+ result = conn.execute(
+ statement, params or {}, execution_options=execution_options
+ )
+
+ if _scalar_result:
+ return result.scalar()
+ else:
+ return result
+
+ @overload
+ def execute(
+ self,
+ statement: TypedReturnsRows[_T],
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ ) -> Result[_T]: ...
+
+ @overload
+ def execute(
+ self,
+ statement: UpdateBase,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ ) -> CursorResult[Any]: ...
+
+ @overload
+ def execute(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ ) -> Result[Any]: ...
+
+ def execute(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ _parent_execute_state: Optional[Any] = None,
+ _add_event: Optional[Any] = None,
+ ) -> Result[Any]:
+ r"""Execute a SQL expression construct.
+
+ Returns a :class:`_engine.Result` object representing
+ results of the statement execution.
+
+ E.g.::
+
+ from sqlalchemy import select
+ result = session.execute(
+ select(User).where(User.id == 5)
+ )
+
+ The API contract of :meth:`_orm.Session.execute` is similar to that
+ of :meth:`_engine.Connection.execute`, the :term:`2.0 style` version
+ of :class:`_engine.Connection`.
+
+ .. versionchanged:: 1.4 the :meth:`_orm.Session.execute` method is
+ now the primary point of ORM statement execution when using
+ :term:`2.0 style` ORM usage.
+
+ :param statement:
+ An executable statement (i.e. an :class:`.Executable` expression
+ such as :func:`_expression.select`).
+
+ :param params:
+ Optional dictionary, or list of dictionaries, containing
+ bound parameter values. If a single dictionary, single-row
+ execution occurs; if a list of dictionaries, an
+ "executemany" will be invoked. The keys in each dictionary
+ must correspond to parameter names present in the statement.
+
+ :param execution_options: optional dictionary of execution options,
+ which will be associated with the statement execution. This
+ dictionary can provide a subset of the options that are accepted
+ by :meth:`_engine.Connection.execution_options`, and may also
+ provide additional options understood only in an ORM context.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_execution_options` - ORM-specific execution
+ options
+
+ :param bind_arguments: dictionary of additional arguments to determine
+ the bind. May include "mapper", "bind", or other custom arguments.
+ Contents of this dictionary are passed to the
+ :meth:`.Session.get_bind` method.
+
+ :return: a :class:`_engine.Result` object.
+
+
+ """
+ return self._execute_internal(
+ statement,
+ params,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ _parent_execute_state=_parent_execute_state,
+ _add_event=_add_event,
+ )
+
+ @overload
+ def scalar(
+ self,
+ statement: TypedReturnsRows[Tuple[_T]],
+ params: Optional[_CoreSingleExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> Optional[_T]: ...
+
+ @overload
+ def scalar(
+ self,
+ statement: Executable,
+ params: Optional[_CoreSingleExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> Any: ...
+
+ def scalar(
+ self,
+ statement: Executable,
+ params: Optional[_CoreSingleExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> Any:
+ """Execute a statement and return a scalar result.
+
+ Usage and parameters are the same as that of
+ :meth:`_orm.Session.execute`; the return result is a scalar Python
+ value.
+
+ """
+
+ return self._execute_internal(
+ statement,
+ params,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ _scalar_result=True,
+ **kw,
+ )
+
+ @overload
+ def scalars(
+ self,
+ statement: TypedReturnsRows[Tuple[_T]],
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> ScalarResult[_T]: ...
+
+ @overload
+ def scalars(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> ScalarResult[Any]: ...
+
+ def scalars(
+ self,
+ statement: Executable,
+ params: Optional[_CoreAnyExecuteParams] = None,
+ *,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ **kw: Any,
+ ) -> ScalarResult[Any]:
+ """Execute a statement and return the results as scalars.
+
+ Usage and parameters are the same as that of
+ :meth:`_orm.Session.execute`; the return result is a
+ :class:`_result.ScalarResult` filtering object which
+ will return single elements rather than :class:`_row.Row` objects.
+
+ :return: a :class:`_result.ScalarResult` object
+
+ .. versionadded:: 1.4.24 Added :meth:`_orm.Session.scalars`
+
+ .. versionadded:: 1.4.26 Added :meth:`_orm.scoped_session.scalars`
+
+ .. seealso::
+
+ :ref:`orm_queryguide_select_orm_entities` - contrasts the behavior
+ of :meth:`_orm.Session.execute` to :meth:`_orm.Session.scalars`
+
+ """
+
+ return self._execute_internal(
+ statement,
+ params=params,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ _scalar_result=False, # mypy appreciates this
+ **kw,
+ ).scalars()
+
+ def close(self) -> None:
+ """Close out the transactional resources and ORM objects used by this
+ :class:`_orm.Session`.
+
+ This expunges all ORM objects associated with this
+ :class:`_orm.Session`, ends any transaction in progress and
+ :term:`releases` any :class:`_engine.Connection` objects which this
+ :class:`_orm.Session` itself has checked out from associated
+ :class:`_engine.Engine` objects. The operation then leaves the
+ :class:`_orm.Session` in a state which it may be used again.
+
+ .. tip::
+
+ In the default running mode the :meth:`_orm.Session.close`
+ method **does not prevent the Session from being used again**.
+ The :class:`_orm.Session` itself does not actually have a
+ distinct "closed" state; it merely means
+ the :class:`_orm.Session` will release all database connections
+ and ORM objects.
+
+ Setting the parameter :paramref:`_orm.Session.close_resets_only`
+ to ``False`` will instead make the ``close`` final, meaning that
+ any further action on the session will be forbidden.
+
+ .. versionchanged:: 1.4 The :meth:`.Session.close` method does not
+ immediately create a new :class:`.SessionTransaction` object;
+ instead, the new :class:`.SessionTransaction` is created only if
+ the :class:`.Session` is used again for a database operation.
+
+ .. seealso::
+
+ :ref:`session_closing` - detail on the semantics of
+ :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
+
+ :meth:`_orm.Session.reset` - a similar method that behaves like
+ ``close()`` with the parameter
+ :paramref:`_orm.Session.close_resets_only` set to ``True``.
+
+ """
+ self._close_impl(invalidate=False)
+
+ def reset(self) -> None:
+ """Close out the transactional resources and ORM objects used by this
+ :class:`_orm.Session`, resetting the session to its initial state.
+
+ This method provides for same "reset-only" behavior that the
+ :meth:`_orm.Session.close` method has provided historically, where the
+ state of the :class:`_orm.Session` is reset as though the object were
+ brand new, and ready to be used again.
+ This method may then be useful for :class:`_orm.Session` objects
+ which set :paramref:`_orm.Session.close_resets_only` to ``False``,
+ so that "reset only" behavior is still available.
+
+ .. versionadded:: 2.0.22
+
+ .. seealso::
+
+ :ref:`session_closing` - detail on the semantics of
+ :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
+
+ :meth:`_orm.Session.close` - a similar method will additionally
+ prevent re-use of the Session when the parameter
+ :paramref:`_orm.Session.close_resets_only` is set to ``False``.
+ """
+ self._close_impl(invalidate=False, is_reset=True)
+
+ def invalidate(self) -> None:
+ """Close this Session, using connection invalidation.
+
+ This is a variant of :meth:`.Session.close` that will additionally
+ ensure that the :meth:`_engine.Connection.invalidate`
+ 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).
+
+ 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
+ <https://www.gevent.org/>`_, which can produce ``Timeout`` exceptions
+ that may mean the underlying connection should be discarded::
+
+ import gevent
+
+ try:
+ sess = Session()
+ sess.add(User())
+ sess.commit()
+ except gevent.Timeout:
+ sess.invalidate()
+ raise
+ except:
+ sess.rollback()
+ raise
+
+ The method additionally does everything that :meth:`_orm.Session.close`
+ does, including that all ORM objects are expunged.
+
+ """
+ self._close_impl(invalidate=True)
+
+ def _close_impl(self, invalidate: bool, is_reset: bool = False) -> None:
+ if not is_reset and self._close_state is _SessionCloseState.ACTIVE:
+ self._close_state = _SessionCloseState.CLOSED
+ self.expunge_all()
+ if self._transaction is not None:
+ for transaction in self._transaction._iterate_self_and_parents():
+ transaction.close(invalidate)
+
+ def expunge_all(self) -> None:
+ """Remove all object instances from this ``Session``.
+
+ This is equivalent to calling ``expunge(obj)`` on all objects in this
+ ``Session``.
+
+ """
+
+ all_states = self.identity_map.all_states() + list(self._new)
+ self.identity_map._kill()
+ self.identity_map = identity.WeakInstanceDict()
+ self._new = {}
+ self._deleted = {}
+
+ statelib.InstanceState._detach_states(all_states, self)
+
+ def _add_bind(self, key: _SessionBindKey, bind: _SessionBind) -> None:
+ try:
+ insp = inspect(key)
+ except sa_exc.NoInspectionAvailable as err:
+ if not isinstance(key, type):
+ raise sa_exc.ArgumentError(
+ "Not an acceptable bind target: %s" % key
+ ) from err
+ else:
+ self.__binds[key] = bind
+ else:
+ if TYPE_CHECKING:
+ assert isinstance(insp, Inspectable)
+
+ if isinstance(insp, TableClause):
+ self.__binds[insp] = bind
+ elif insp_is_mapper(insp):
+ self.__binds[insp.class_] = bind
+ for _selectable in insp._all_tables:
+ self.__binds[_selectable] = bind
+ else:
+ raise sa_exc.ArgumentError(
+ "Not an acceptable bind target: %s" % key
+ )
+
+ def bind_mapper(
+ self, mapper: _EntityBindKey[_O], bind: _SessionBind
+ ) -> None:
+ """Associate a :class:`_orm.Mapper` or arbitrary Python class with a
+ "bind", e.g. an :class:`_engine.Engine` or
+ :class:`_engine.Connection`.
+
+ The given entity is added to a lookup used by the
+ :meth:`.Session.get_bind` method.
+
+ :param mapper: a :class:`_orm.Mapper` object,
+ or an instance of a mapped
+ class, or any Python class that is the base of a set of mapped
+ classes.
+
+ :param bind: an :class:`_engine.Engine` or :class:`_engine.Connection`
+ object.
+
+ .. seealso::
+
+ :ref:`session_partitioning`
+
+ :paramref:`.Session.binds`
+
+ :meth:`.Session.bind_table`
+
+
+ """
+ self._add_bind(mapper, bind)
+
+ def bind_table(self, table: TableClause, bind: _SessionBind) -> None:
+ """Associate a :class:`_schema.Table` with a "bind", e.g. an
+ :class:`_engine.Engine`
+ or :class:`_engine.Connection`.
+
+ The given :class:`_schema.Table` is added to a lookup used by the
+ :meth:`.Session.get_bind` method.
+
+ :param table: a :class:`_schema.Table` object,
+ which is typically the target
+ of an ORM mapping, or is present within a selectable that is
+ mapped.
+
+ :param bind: an :class:`_engine.Engine` or :class:`_engine.Connection`
+ object.
+
+ .. seealso::
+
+ :ref:`session_partitioning`
+
+ :paramref:`.Session.binds`
+
+ :meth:`.Session.bind_mapper`
+
+
+ """
+ self._add_bind(table, bind)
+
+ def get_bind(
+ self,
+ mapper: Optional[_EntityBindKey[_O]] = None,
+ *,
+ clause: Optional[ClauseElement] = None,
+ bind: Optional[_SessionBind] = None,
+ _sa_skip_events: Optional[bool] = None,
+ _sa_skip_for_implicit_returning: bool = False,
+ **kw: Any,
+ ) -> Union[Engine, Connection]:
+ """Return a "bind" to which this :class:`.Session` is bound.
+
+ The "bind" is usually an instance of :class:`_engine.Engine`,
+ except in the case where the :class:`.Session` has been
+ explicitly bound directly to a :class:`_engine.Connection`.
+
+ For a multiply-bound or unbound :class:`.Session`, the
+ ``mapper`` or ``clause`` arguments are used to determine the
+ appropriate bind to return.
+
+ Note that the "mapper" argument is usually present
+ when :meth:`.Session.get_bind` is called via an ORM
+ operation such as a :meth:`.Session.query`, each
+ individual INSERT/UPDATE/DELETE operation within a
+ :meth:`.Session.flush`, call, etc.
+
+ The order of resolution is:
+
+ 1. if mapper given and :paramref:`.Session.binds` is present,
+ locate a bind based first on the mapper in use, then
+ on the mapped class in use, then on any base classes that are
+ present in the ``__mro__`` of the mapped class, from more specific
+ superclasses to more general.
+ 2. if clause given and ``Session.binds`` is present,
+ locate a bind based on :class:`_schema.Table` objects
+ found in the given clause present in ``Session.binds``.
+ 3. if ``Session.binds`` is present, return that.
+ 4. if clause given, attempt to return a bind
+ linked to the :class:`_schema.MetaData` ultimately
+ associated with the clause.
+ 5. if mapper given, attempt to return a bind
+ linked to the :class:`_schema.MetaData` ultimately
+ associated with the :class:`_schema.Table` or other
+ selectable to which the mapper is mapped.
+ 6. No bind can be found, :exc:`~sqlalchemy.exc.UnboundExecutionError`
+ is raised.
+
+ Note that the :meth:`.Session.get_bind` method can be overridden on
+ a user-defined subclass of :class:`.Session` to provide any kind
+ of bind resolution scheme. See the example at
+ :ref:`session_custom_partitioning`.
+
+ :param mapper:
+ Optional mapped class or corresponding :class:`_orm.Mapper` instance.
+ The bind can be derived from a :class:`_orm.Mapper` first by
+ consulting the "binds" map associated with this :class:`.Session`,
+ and secondly by consulting the :class:`_schema.MetaData` associated
+ with the :class:`_schema.Table` to which the :class:`_orm.Mapper` is
+ mapped for a bind.
+
+ :param clause:
+ A :class:`_expression.ClauseElement` (i.e.
+ :func:`_expression.select`,
+ :func:`_expression.text`,
+ etc.). If the ``mapper`` argument is not present or could not
+ produce a bind, the given expression construct will be searched
+ for a bound element, typically a :class:`_schema.Table`
+ associated with
+ bound :class:`_schema.MetaData`.
+
+ .. seealso::
+
+ :ref:`session_partitioning`
+
+ :paramref:`.Session.binds`
+
+ :meth:`.Session.bind_mapper`
+
+ :meth:`.Session.bind_table`
+
+ """
+
+ # this function is documented as a subclassing hook, so we have
+ # to call this method even if the return is simple
+ if bind:
+ return bind
+ elif not self.__binds and self.bind:
+ # simplest and most common case, we have a bind and no
+ # per-mapper/table binds, we're done
+ return self.bind
+
+ # we don't have self.bind and either have self.__binds
+ # or we don't have self.__binds (which is legacy). Look at the
+ # mapper and the clause
+ if mapper is None and clause is None:
+ if self.bind:
+ return self.bind
+ else:
+ raise sa_exc.UnboundExecutionError(
+ "This session is not bound to a single Engine or "
+ "Connection, and no context was provided to locate "
+ "a binding."
+ )
+
+ # look more closely at the mapper.
+ if mapper is not None:
+ try:
+ inspected_mapper = inspect(mapper)
+ except sa_exc.NoInspectionAvailable as err:
+ if isinstance(mapper, type):
+ raise exc.UnmappedClassError(mapper) from err
+ else:
+ raise
+ else:
+ inspected_mapper = None
+
+ # match up the mapper or clause in the __binds
+ if self.__binds:
+ # matching mappers and selectables to entries in the
+ # binds dictionary; supported use case.
+ if inspected_mapper:
+ for cls in inspected_mapper.class_.__mro__:
+ if cls in self.__binds:
+ return self.__binds[cls]
+ if clause is None:
+ clause = inspected_mapper.persist_selectable
+
+ if clause is not None:
+ plugin_subject = clause._propagate_attrs.get(
+ "plugin_subject", None
+ )
+
+ if plugin_subject is not None:
+ for cls in plugin_subject.mapper.class_.__mro__:
+ if cls in self.__binds:
+ return self.__binds[cls]
+
+ for obj in visitors.iterate(clause):
+ if obj in self.__binds:
+ if TYPE_CHECKING:
+ assert isinstance(obj, Table)
+ return self.__binds[obj]
+
+ # none of the __binds matched, but we have a fallback bind.
+ # return that
+ if self.bind:
+ return self.bind
+
+ context = []
+ if inspected_mapper is not None:
+ context.append(f"mapper {inspected_mapper}")
+ if clause is not None:
+ context.append("SQL expression")
+
+ raise sa_exc.UnboundExecutionError(
+ f"Could not locate a bind configured on "
+ f'{", ".join(context)} or this Session.'
+ )
+
+ @overload
+ def query(self, _entity: _EntityType[_O]) -> Query[_O]: ...
+
+ @overload
+ def query(
+ self, _colexpr: TypedColumnsClauseRole[_T]
+ ) -> RowReturningQuery[Tuple[_T]]: ...
+
+ # START OVERLOADED FUNCTIONS self.query RowReturningQuery 2-8
+
+ # code within this block is **programmatically,
+ # statically generated** by tools/generate_tuple_map_overloads.py
+
+ @overload
+ def query(
+ self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1]
+ ) -> RowReturningQuery[Tuple[_T0, _T1]]: ...
+
+ @overload
+ def query(
+ self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2]
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ __ent5: _TCCA[_T5],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ __ent5: _TCCA[_T5],
+ __ent6: _TCCA[_T6],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: ...
+
+ @overload
+ def query(
+ self,
+ __ent0: _TCCA[_T0],
+ __ent1: _TCCA[_T1],
+ __ent2: _TCCA[_T2],
+ __ent3: _TCCA[_T3],
+ __ent4: _TCCA[_T4],
+ __ent5: _TCCA[_T5],
+ __ent6: _TCCA[_T6],
+ __ent7: _TCCA[_T7],
+ ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: ...
+
+ # END OVERLOADED FUNCTIONS self.query
+
+ @overload
+ def query(
+ self, *entities: _ColumnsClauseArgument[Any], **kwargs: Any
+ ) -> Query[Any]: ...
+
+ def query(
+ self, *entities: _ColumnsClauseArgument[Any], **kwargs: Any
+ ) -> Query[Any]:
+ """Return a new :class:`_query.Query` object corresponding to this
+ :class:`_orm.Session`.
+
+ Note that the :class:`_query.Query` object is legacy as of
+ SQLAlchemy 2.0; the :func:`_sql.select` construct is now used
+ to construct ORM queries.
+
+ .. seealso::
+
+ :ref:`unified_tutorial`
+
+ :ref:`queryguide_toplevel`
+
+ :ref:`query_api_toplevel` - legacy API doc
+
+ """
+
+ return self._query_cls(entities, self, **kwargs)
+
+ def _identity_lookup(
+ self,
+ mapper: Mapper[_O],
+ primary_key_identity: Union[Any, Tuple[Any, ...]],
+ identity_token: Any = None,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ lazy_loaded_from: Optional[InstanceState[Any]] = None,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ ) -> Union[Optional[_O], LoaderCallableStatus]:
+ """Locate an object in the identity map.
+
+ Given a primary key identity, constructs an identity key and then
+ looks in the session's identity map. If present, the object may
+ be run through unexpiration rules (e.g. load unloaded attributes,
+ check if was deleted).
+
+ e.g.::
+
+ obj = session._identity_lookup(inspect(SomeClass), (1, ))
+
+ :param mapper: mapper in use
+ :param primary_key_identity: the primary key we are searching for, as
+ a tuple.
+ :param identity_token: identity token that should be used to create
+ the identity key. Used as is, however overriding subclasses can
+ repurpose this in order to interpret the value in a special way,
+ such as if None then look among multiple target tokens.
+ :param passive: passive load flag passed to
+ :func:`.loading.get_from_identity`, which impacts the behavior if
+ the object is found; the object may be validated and/or unexpired
+ if the flag allows for SQL to be emitted.
+ :param lazy_loaded_from: an :class:`.InstanceState` that is
+ specifically asking for this identity as a related identity. Used
+ for sharding schemes where there is a correspondence between an object
+ and a related object being lazy-loaded (or otherwise
+ relationship-loaded).
+
+ :return: None if the object is not found in the identity map, *or*
+ if the object was unexpired and found to have been deleted.
+ if passive flags disallow SQL and the object is expired, returns
+ PASSIVE_NO_RESULT. In all other cases the instance is returned.
+
+ .. versionchanged:: 1.4.0 - the :meth:`.Session._identity_lookup`
+ method was moved from :class:`_query.Query` to
+ :class:`.Session`, to avoid having to instantiate the
+ :class:`_query.Query` object.
+
+
+ """
+
+ key = mapper.identity_key_from_primary_key(
+ primary_key_identity, identity_token=identity_token
+ )
+
+ # work around: https://github.com/python/typing/discussions/1143
+ return_value = loading.get_from_identity(self, mapper, key, passive)
+ return return_value
+
+ @util.non_memoized_property
+ @contextlib.contextmanager
+ def no_autoflush(self) -> Iterator[Session]:
+ """Return a context manager that disables autoflush.
+
+ e.g.::
+
+ with session.no_autoflush:
+
+ some_object = SomeClass()
+ session.add(some_object)
+ # won't autoflush
+ some_object.related_thing = session.query(SomeRelated).first()
+
+ Operations that proceed within the ``with:`` block
+ will not be subject to flushes occurring upon query
+ access. This is useful when initializing a series
+ of objects which involve existing database queries,
+ where the uncompleted object should not yet be flushed.
+
+ """
+ autoflush = self.autoflush
+ self.autoflush = False
+ try:
+ yield self
+ finally:
+ self.autoflush = autoflush
+
+ @util.langhelpers.tag_method_for_warnings(
+ "This warning originated from the Session 'autoflush' process, "
+ "which was invoked automatically in response to a user-initiated "
+ "operation.",
+ sa_exc.SAWarning,
+ )
+ def _autoflush(self) -> None:
+ if self.autoflush and not self._flushing:
+ try:
+ self.flush()
+ except sa_exc.StatementError as e:
+ # note we are reraising StatementError as opposed to
+ # raising FlushError with "chaining" to remain compatible
+ # with code that catches StatementError, IntegrityError,
+ # etc.
+ e.add_detail(
+ "raised as a result of Query-invoked autoflush; "
+ "consider using a session.no_autoflush block if this "
+ "flush is occurring prematurely"
+ )
+ raise e.with_traceback(sys.exc_info()[2])
+
+ def refresh(
+ self,
+ instance: object,
+ attribute_names: Optional[Iterable[str]] = None,
+ with_for_update: ForUpdateParameter = None,
+ ) -> None:
+ """Expire and refresh attributes on the given instance.
+
+ The selected attributes will first be expired as they would when using
+ :meth:`_orm.Session.expire`; then a SELECT statement will be issued to
+ the database to refresh column-oriented attributes with the current
+ value available in the current transaction.
+
+ :func:`_orm.relationship` oriented attributes will also be immediately
+ loaded if they were already eagerly loaded on the object, using the
+ same eager loading strategy that they were loaded with originally.
+
+ .. versionadded:: 1.4 - the :meth:`_orm.Session.refresh` method
+ can also refresh eagerly loaded attributes.
+
+ :func:`_orm.relationship` oriented attributes that would normally
+ load using the ``select`` (or "lazy") loader strategy will also
+ load **if they are named explicitly in the attribute_names
+ collection**, emitting a SELECT statement for the attribute using the
+ ``immediate`` loader strategy. If lazy-loaded relationships are not
+ named in :paramref:`_orm.Session.refresh.attribute_names`, then
+ they remain as "lazy loaded" attributes and are not implicitly
+ refreshed.
+
+ .. versionchanged:: 2.0.4 The :meth:`_orm.Session.refresh` method
+ will now refresh lazy-loaded :func:`_orm.relationship` oriented
+ attributes for those which are named explicitly in the
+ :paramref:`_orm.Session.refresh.attribute_names` collection.
+
+ .. tip::
+
+ While the :meth:`_orm.Session.refresh` method is capable of
+ refreshing both column and relationship oriented attributes, its
+ primary focus is on refreshing of local column-oriented attributes
+ on a single instance. For more open ended "refresh" functionality,
+ including the ability to refresh the attributes on many objects at
+ once while having explicit control over relationship loader
+ strategies, use the
+ :ref:`populate existing <orm_queryguide_populate_existing>` feature
+ instead.
+
+ Note that a highly isolated transaction will return the same values as
+ were previously read in that same transaction, regardless of changes
+ in database state outside of that transaction. Refreshing
+ attributes usually only makes sense at the start of a transaction
+ where database rows have not yet been accessed.
+
+ :param attribute_names: optional. An iterable collection of
+ string attribute names indicating a subset of attributes to
+ be refreshed.
+
+ :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.
+
+ .. seealso::
+
+ :ref:`session_expire` - introductory material
+
+ :meth:`.Session.expire`
+
+ :meth:`.Session.expire_all`
+
+ :ref:`orm_queryguide_populate_existing` - allows any ORM query
+ to refresh objects as they would be loaded normally.
+
+ """
+ try:
+ state = attributes.instance_state(instance)
+ except exc.NO_STATE as err:
+ raise exc.UnmappedInstanceError(instance) from err
+
+ self._expire_state(state, attribute_names)
+
+ # this autoflush previously used to occur as a secondary effect
+ # of the load_on_ident below. Meaning we'd organize the SELECT
+ # based on current DB pks, then flush, then if pks changed in that
+ # flush, crash. this was unticketed but discovered as part of
+ # #8703. So here, autoflush up front, dont autoflush inside
+ # load_on_ident.
+ self._autoflush()
+
+ if with_for_update == {}:
+ raise sa_exc.ArgumentError(
+ "with_for_update should be the boolean value "
+ "True, or a dictionary with options. "
+ "A blank dictionary is ambiguous."
+ )
+
+ with_for_update = ForUpdateArg._from_argument(with_for_update)
+
+ stmt: Select[Any] = sql.select(object_mapper(instance))
+ if (
+ loading.load_on_ident(
+ self,
+ stmt,
+ state.key,
+ refresh_state=state,
+ with_for_update=with_for_update,
+ only_load_props=attribute_names,
+ require_pk_cols=True,
+ # technically unnecessary as we just did autoflush
+ # above, however removes the additional unnecessary
+ # call to _autoflush()
+ no_autoflush=True,
+ is_user_refresh=True,
+ )
+ is None
+ ):
+ raise sa_exc.InvalidRequestError(
+ "Could not refresh instance '%s'" % instance_str(instance)
+ )
+
+ def expire_all(self) -> None:
+ """Expires all persistent instances within this Session.
+
+ When any attributes on a persistent instance is next accessed,
+ a query will be issued using the
+ :class:`.Session` object's current transactional context in order to
+ load all expired attributes for the given instance. Note that
+ a highly isolated transaction will return the same values as were
+ previously read in that same transaction, regardless of changes
+ in database state outside of that transaction.
+
+ To expire individual objects and individual attributes
+ on those objects, use :meth:`Session.expire`.
+
+ The :class:`.Session` object's default behavior is to
+ expire all state whenever the :meth:`Session.rollback`
+ or :meth:`Session.commit` methods are called, so that new
+ state can be loaded for the new transaction. For this reason,
+ calling :meth:`Session.expire_all` is not usually needed,
+ assuming the transaction is isolated.
+
+ .. seealso::
+
+ :ref:`session_expire` - introductory material
+
+ :meth:`.Session.expire`
+
+ :meth:`.Session.refresh`
+
+ :meth:`_orm.Query.populate_existing`
+
+ """
+ for state in self.identity_map.all_states():
+ state._expire(state.dict, self.identity_map._modified)
+
+ def expire(
+ self, instance: object, attribute_names: Optional[Iterable[str]] = None
+ ) -> None:
+ """Expire the attributes on an instance.
+
+ Marks the attributes of an instance as out of date. When an expired
+ attribute is next accessed, a query will be issued to the
+ :class:`.Session` object's current transactional context in order to
+ load all expired attributes for the given instance. Note that
+ a highly isolated transaction will return the same values as were
+ previously read in that same transaction, regardless of changes
+ in database state outside of that transaction.
+
+ To expire all objects in the :class:`.Session` simultaneously,
+ use :meth:`Session.expire_all`.
+
+ The :class:`.Session` object's default behavior is to
+ expire all state whenever the :meth:`Session.rollback`
+ or :meth:`Session.commit` methods are called, so that new
+ state can be loaded for the new transaction. For this reason,
+ calling :meth:`Session.expire` only makes sense for the specific
+ case that a non-ORM SQL statement was emitted in the current
+ transaction.
+
+ :param instance: The instance to be refreshed.
+ :param attribute_names: optional list of string attribute names
+ indicating a subset of attributes to be expired.
+
+ .. seealso::
+
+ :ref:`session_expire` - introductory material
+
+ :meth:`.Session.expire`
+
+ :meth:`.Session.refresh`
+
+ :meth:`_orm.Query.populate_existing`
+
+ """
+ try:
+ state = attributes.instance_state(instance)
+ except exc.NO_STATE as err:
+ raise exc.UnmappedInstanceError(instance) from err
+ self._expire_state(state, attribute_names)
+
+ def _expire_state(
+ self,
+ state: InstanceState[Any],
+ attribute_names: Optional[Iterable[str]],
+ ) -> None:
+ self._validate_persistent(state)
+ if attribute_names:
+ state._expire_attributes(state.dict, attribute_names)
+ else:
+ # pre-fetch the full cascade since the expire is going to
+ # remove associations
+ cascaded = list(
+ state.manager.mapper.cascade_iterator("refresh-expire", state)
+ )
+ self._conditional_expire(state)
+ for o, m, st_, dct_ in cascaded:
+ self._conditional_expire(st_)
+
+ def _conditional_expire(
+ self, state: InstanceState[Any], autoflush: Optional[bool] = None
+ ) -> None:
+ """Expire a state if persistent, else expunge if pending"""
+
+ if state.key:
+ state._expire(state.dict, self.identity_map._modified)
+ elif state in self._new:
+ self._new.pop(state)
+ state._detach(self)
+
+ def expunge(self, instance: object) -> None:
+ """Remove the `instance` from this ``Session``.
+
+ This will free all internal references to the instance. Cascading
+ will be applied according to the *expunge* cascade rule.
+
+ """
+ try:
+ state = attributes.instance_state(instance)
+ except exc.NO_STATE as err:
+ raise exc.UnmappedInstanceError(instance) from err
+ if state.session_id is not self.hash_key:
+ raise sa_exc.InvalidRequestError(
+ "Instance %s is not present in this Session" % state_str(state)
+ )
+
+ cascaded = list(
+ state.manager.mapper.cascade_iterator("expunge", state)
+ )
+ self._expunge_states([state] + [st_ for o, m, st_, dct_ in cascaded])
+
+ def _expunge_states(
+ self, states: Iterable[InstanceState[Any]], to_transient: bool = False
+ ) -> None:
+ for state in states:
+ if state in self._new:
+ self._new.pop(state)
+ elif self.identity_map.contains_state(state):
+ self.identity_map.safe_discard(state)
+ self._deleted.pop(state, None)
+ elif self._transaction:
+ # state is "detached" from being deleted, but still present
+ # in the transaction snapshot
+ self._transaction._deleted.pop(state, None)
+ statelib.InstanceState._detach_states(
+ states, self, to_transient=to_transient
+ )
+
+ def _register_persistent(self, states: Set[InstanceState[Any]]) -> None:
+ """Register all persistent objects from a flush.
+
+ This is used both for pending objects moving to the persistent
+ state as well as already persistent objects.
+
+ """
+
+ pending_to_persistent = self.dispatch.pending_to_persistent or None
+ for state in states:
+ mapper = _state_mapper(state)
+
+ # prevent against last minute dereferences of the object
+ obj = state.obj()
+ if obj is not None:
+ instance_key = mapper._identity_key_from_state(state)
+
+ if (
+ _none_set.intersection(instance_key[1])
+ and not mapper.allow_partial_pks
+ or _none_set.issuperset(instance_key[1])
+ ):
+ raise exc.FlushError(
+ "Instance %s has a NULL identity key. If this is an "
+ "auto-generated value, check that the database table "
+ "allows generation of new primary key values, and "
+ "that the mapped Column object is configured to "
+ "expect these generated values. Ensure also that "
+ "this flush() is not occurring at an inappropriate "
+ "time, such as within a load() event."
+ % state_str(state)
+ )
+
+ if state.key is None:
+ state.key = instance_key
+ elif state.key != instance_key:
+ # primary key switch. use safe_discard() in case another
+ # state has already replaced this one in the identity
+ # map (see test/orm/test_naturalpks.py ReversePKsTest)
+ self.identity_map.safe_discard(state)
+ trans = self._transaction
+ assert trans is not None
+ if state in trans._key_switches:
+ orig_key = trans._key_switches[state][0]
+ else:
+ orig_key = state.key
+ trans._key_switches[state] = (
+ orig_key,
+ instance_key,
+ )
+ state.key = instance_key
+
+ # there can be an existing state in the identity map
+ # that is replaced when the primary keys of two instances
+ # are swapped; see test/orm/test_naturalpks.py -> test_reverse
+ old = self.identity_map.replace(state)
+ if (
+ old is not None
+ and mapper._identity_key_from_state(old) == instance_key
+ and old.obj() is not None
+ ):
+ util.warn(
+ "Identity map already had an identity for %s, "
+ "replacing it with newly flushed object. Are there "
+ "load operations occurring inside of an event handler "
+ "within the flush?" % (instance_key,)
+ )
+ state._orphaned_outside_of_session = False
+
+ statelib.InstanceState._commit_all_states(
+ ((state, state.dict) for state in states), self.identity_map
+ )
+
+ self._register_altered(states)
+
+ if pending_to_persistent is not None:
+ for state in states.intersection(self._new):
+ pending_to_persistent(self, state)
+
+ # remove from new last, might be the last strong ref
+ for state in set(states).intersection(self._new):
+ self._new.pop(state)
+
+ def _register_altered(self, states: Iterable[InstanceState[Any]]) -> None:
+ if self._transaction:
+ for state in states:
+ if state in self._new:
+ self._transaction._new[state] = True
+ else:
+ self._transaction._dirty[state] = True
+
+ def _remove_newly_deleted(
+ self, states: Iterable[InstanceState[Any]]
+ ) -> None:
+ persistent_to_deleted = self.dispatch.persistent_to_deleted or None
+ for state in states:
+ if self._transaction:
+ self._transaction._deleted[state] = True
+
+ if persistent_to_deleted is not None:
+ # get a strong reference before we pop out of
+ # self._deleted
+ obj = state.obj() # noqa
+
+ self.identity_map.safe_discard(state)
+ self._deleted.pop(state, None)
+ state._deleted = True
+ # can't call state._detach() here, because this state
+ # is still in the transaction snapshot and needs to be
+ # tracked as part of that
+ if persistent_to_deleted is not None:
+ persistent_to_deleted(self, state)
+
+ def add(self, instance: object, _warn: bool = True) -> None:
+ """Place an object into this :class:`_orm.Session`.
+
+ Objects that are in the :term:`transient` state when passed to the
+ :meth:`_orm.Session.add` method will move to the
+ :term:`pending` state, until the next flush, at which point they
+ will move to the :term:`persistent` state.
+
+ Objects that are in the :term:`detached` state when passed to the
+ :meth:`_orm.Session.add` method will move to the :term:`persistent`
+ state directly.
+
+ If the transaction used by the :class:`_orm.Session` is rolled back,
+ objects which were transient when they were passed to
+ :meth:`_orm.Session.add` will be moved back to the
+ :term:`transient` state, and will no longer be present within this
+ :class:`_orm.Session`.
+
+ .. seealso::
+
+ :meth:`_orm.Session.add_all`
+
+ :ref:`session_adding` - at :ref:`session_basics`
+
+ """
+ if _warn and self._warn_on_events:
+ self._flush_warning("Session.add()")
+
+ try:
+ state = attributes.instance_state(instance)
+ except exc.NO_STATE as err:
+ raise exc.UnmappedInstanceError(instance) from err
+
+ self._save_or_update_state(state)
+
+ def add_all(self, instances: Iterable[object]) -> None:
+ """Add the given collection of instances to this :class:`_orm.Session`.
+
+ See the documentation for :meth:`_orm.Session.add` for a general
+ behavioral description.
+
+ .. seealso::
+
+ :meth:`_orm.Session.add`
+
+ :ref:`session_adding` - at :ref:`session_basics`
+
+ """
+
+ if self._warn_on_events:
+ self._flush_warning("Session.add_all()")
+
+ for instance in instances:
+ self.add(instance, _warn=False)
+
+ def _save_or_update_state(self, state: InstanceState[Any]) -> None:
+ state._orphaned_outside_of_session = False
+ self._save_or_update_impl(state)
+
+ mapper = _state_mapper(state)
+ for o, m, st_, dct_ in mapper.cascade_iterator(
+ "save-update", state, halt_on=self._contains_state
+ ):
+ self._save_or_update_impl(st_)
+
+ def delete(self, instance: object) -> None:
+ """Mark an instance as deleted.
+
+ The object is assumed to be either :term:`persistent` or
+ :term:`detached` when passed; after the method is called, the
+ object will remain in the :term:`persistent` state until the next
+ flush proceeds. During this time, the object will also be a member
+ of the :attr:`_orm.Session.deleted` collection.
+
+ When the next flush proceeds, the object will move to the
+ :term:`deleted` state, indicating a ``DELETE`` statement was emitted
+ for its row within the current transaction. When the transaction
+ is successfully committed,
+ the deleted object is moved to the :term:`detached` state and is
+ no longer present within this :class:`_orm.Session`.
+
+ .. seealso::
+
+ :ref:`session_deleting` - at :ref:`session_basics`
+
+ """
+ if self._warn_on_events:
+ self._flush_warning("Session.delete()")
+
+ try:
+ state = attributes.instance_state(instance)
+ except exc.NO_STATE as err:
+ raise exc.UnmappedInstanceError(instance) from err
+
+ self._delete_impl(state, instance, head=True)
+
+ def _delete_impl(
+ self, state: InstanceState[Any], obj: object, head: bool
+ ) -> None:
+ if state.key is None:
+ if head:
+ raise sa_exc.InvalidRequestError(
+ "Instance '%s' is not persisted" % state_str(state)
+ )
+ else:
+ return
+
+ to_attach = self._before_attach(state, obj)
+
+ if state in self._deleted:
+ return
+
+ self.identity_map.add(state)
+
+ if to_attach:
+ self._after_attach(state, obj)
+
+ if head:
+ # grab the cascades before adding the item to the deleted list
+ # so that autoflush does not delete the item
+ # the strong reference to the instance itself is significant here
+ cascade_states = list(
+ state.manager.mapper.cascade_iterator("delete", state)
+ )
+ else:
+ cascade_states = None
+
+ self._deleted[state] = obj
+
+ if head:
+ if TYPE_CHECKING:
+ assert cascade_states is not None
+ for o, m, st_, dct_ in cascade_states:
+ self._delete_impl(st_, o, False)
+
+ def get(
+ self,
+ entity: _EntityBindKey[_O],
+ ident: _PKIdentityArgument,
+ *,
+ options: Optional[Sequence[ORMOption]] = None,
+ populate_existing: bool = False,
+ with_for_update: ForUpdateParameter = None,
+ identity_token: Optional[Any] = None,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ ) -> Optional[_O]:
+ """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 legacy :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.
+
+ :param execution_options: optional dictionary of execution options,
+ which will be associated with the query execution if one is emitted.
+ This dictionary can provide a subset of the options that are
+ accepted by :meth:`_engine.Connection.execution_options`, and may
+ also provide additional options understood only in an ORM context.
+
+ .. versionadded:: 1.4.29
+
+ .. seealso::
+
+ :ref:`orm_queryguide_execution_options` - ORM-specific execution
+ options
+
+ :param bind_arguments: dictionary of additional arguments to determine
+ the bind. May include "mapper", "bind", or other custom arguments.
+ Contents of this dictionary are passed to the
+ :meth:`.Session.get_bind` method.
+
+ .. versionadded: 2.0.0rc1
+
+ :return: The object instance, or ``None``.
+
+ """
+ return self._get_impl(
+ entity,
+ ident,
+ loading.load_on_pk_identity,
+ options=options,
+ populate_existing=populate_existing,
+ with_for_update=with_for_update,
+ identity_token=identity_token,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ )
+
+ def get_one(
+ self,
+ entity: _EntityBindKey[_O],
+ ident: _PKIdentityArgument,
+ *,
+ options: Optional[Sequence[ORMOption]] = None,
+ populate_existing: bool = False,
+ with_for_update: ForUpdateParameter = None,
+ identity_token: Optional[Any] = None,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ ) -> _O:
+ """Return exactly one instance based on the given primary key
+ identifier, or raise an exception if not found.
+
+ Raises ``sqlalchemy.orm.exc.NoResultFound`` if the query
+ selects no rows.
+
+ For a detailed documentation of the arguments see the
+ method :meth:`.Session.get`.
+
+ .. versionadded:: 2.0.22
+
+ :return: The object instance.
+
+ .. seealso::
+
+ :meth:`.Session.get` - equivalent method that instead
+ returns ``None`` if no row was found with the provided primary
+ key
+
+ """
+
+ instance = self.get(
+ entity,
+ ident,
+ options=options,
+ populate_existing=populate_existing,
+ with_for_update=with_for_update,
+ identity_token=identity_token,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ )
+
+ if instance is None:
+ raise sa_exc.NoResultFound(
+ "No row was found when one was required"
+ )
+
+ return instance
+
+ def _get_impl(
+ self,
+ entity: _EntityBindKey[_O],
+ primary_key_identity: _PKIdentityArgument,
+ db_load_fn: Callable[..., _O],
+ *,
+ options: Optional[Sequence[ExecutableOption]] = None,
+ populate_existing: bool = False,
+ with_for_update: ForUpdateParameter = None,
+ identity_token: Optional[Any] = None,
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
+ bind_arguments: Optional[_BindArguments] = None,
+ ) -> Optional[_O]:
+ # convert composite types to individual args
+ if (
+ is_composite_class(primary_key_identity)
+ and type(primary_key_identity)
+ in descriptor_props._composite_getters
+ ):
+ getter = descriptor_props._composite_getters[
+ type(primary_key_identity)
+ ]
+ primary_key_identity = getter(primary_key_identity)
+
+ mapper: Optional[Mapper[_O]] = inspect(entity)
+
+ if mapper is None or not mapper.is_mapper:
+ raise sa_exc.ArgumentError(
+ "Expected mapped class or mapper, got: %r" % 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 session.get(); primary key columns "
+ "are %s" % ",".join("'%s'" % c for c in mapper.primary_key)
+ )
+
+ if is_dict:
+ pk_synonyms = mapper._pk_synonyms
+
+ if pk_synonyms:
+ correct_keys = set(pk_synonyms).intersection(
+ primary_key_identity
+ )
+
+ if correct_keys:
+ primary_key_identity = dict(primary_key_identity)
+ for k in correct_keys:
+ primary_key_identity[pk_synonyms[k]] = (
+ primary_key_identity[k]
+ )
+
+ try:
+ primary_key_identity = list(
+ primary_key_identity[prop.key]
+ for prop in mapper._identity_key_props
+ )
+
+ except KeyError as err:
+ raise sa_exc.InvalidRequestError(
+ "Incorrect names of values in identifier to formulate "
+ "primary key for session.get(); primary key attribute "
+ "names are %s (synonym names are also accepted)"
+ % ",".join(
+ "'%s'" % prop.key
+ for prop in mapper._identity_key_props
+ )
+ ) from 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,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ )
+
+ if instance is not None:
+ # reject calls for id in identity map but class
+ # mismatch.
+ if not isinstance(instance, mapper.class_):
+ return None
+ return instance
+
+ # TODO: this was being tested before, but this is not possible
+ assert instance is not LoaderCallableStatus.PASSIVE_CLASS_MISMATCH
+
+ # set_label_style() 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).set_label_style(
+ LABEL_STYLE_TABLENAME_PLUS_COL
+ )
+ if with_for_update is not None:
+ statement._for_update_arg = ForUpdateArg._from_argument(
+ with_for_update
+ )
+
+ if options:
+ statement = statement.options(*options)
+ return db_load_fn(
+ self,
+ statement,
+ primary_key_identity,
+ load_options=load_options,
+ identity_token=identity_token,
+ execution_options=execution_options,
+ bind_arguments=bind_arguments,
+ )
+
+ def merge(
+ self,
+ instance: _O,
+ *,
+ load: bool = True,
+ options: Optional[Sequence[ORMOption]] = None,
+ ) -> _O:
+ """Copy the state of a given instance into a corresponding instance
+ within this :class:`.Session`.
+
+ :meth:`.Session.merge` examines the primary key attributes of the
+ source instance, and attempts to reconcile it with an instance of the
+ same primary key in the session. If not found locally, it attempts
+ to load the object from the database based on primary key, and if
+ none can be located, creates a new instance. The state of each
+ attribute on the source instance is then copied to the target
+ instance. The resulting target instance is then returned by the
+ method; the original source instance is left unmodified, and
+ un-associated with the :class:`.Session` if not already.
+
+ This operation cascades to associated instances if the association is
+ mapped with ``cascade="merge"``.
+
+ See :ref:`unitofwork_merging` for a detailed discussion of merging.
+
+ :param instance: Instance to be merged.
+ :param load: Boolean, when False, :meth:`.merge` switches into
+ a "high performance" mode which causes it to forego emitting history
+ events as well as all database access. This flag is used for
+ cases such as transferring graphs of objects into a :class:`.Session`
+ from a second level cache, or to transfer just-loaded objects
+ into the :class:`.Session` owned by a worker thread or process
+ without re-querying the database.
+
+ The ``load=False`` use case adds the caveat that the given
+ object has to be in a "clean" state, that is, has no pending changes
+ to be flushed - even if the incoming object is detached from any
+ :class:`.Session`. This is so that when
+ the merge operation populates local attributes and
+ cascades to related objects and
+ collections, the values can be "stamped" onto the
+ target object as is, without generating any history or attribute
+ events, and without the need to reconcile the incoming data with
+ any existing related objects or collections that might not
+ be loaded. The resulting objects from ``load=False`` are always
+ produced as "clean", so it is only appropriate that the given objects
+ should be "clean" as well, else this suggests a mis-use of the
+ method.
+ :param options: optional sequence of loader options which will be
+ applied to the :meth:`_orm.Session.get` method when the merge
+ operation loads the existing version of the object from the database.
+
+ .. versionadded:: 1.4.24
+
+
+ .. seealso::
+
+ :func:`.make_transient_to_detached` - provides for an alternative
+ means of "merging" a single object into the :class:`.Session`
+
+ """
+
+ if self._warn_on_events:
+ self._flush_warning("Session.merge()")
+
+ _recursive: Dict[InstanceState[Any], object] = {}
+ _resolve_conflict_map: Dict[_IdentityKeyType[Any], object] = {}
+
+ if load:
+ # flush current contents if we expect to load data
+ self._autoflush()
+
+ object_mapper(instance) # verify mapped
+ autoflush = self.autoflush
+ try:
+ self.autoflush = False
+ return self._merge(
+ attributes.instance_state(instance),
+ attributes.instance_dict(instance),
+ load=load,
+ options=options,
+ _recursive=_recursive,
+ _resolve_conflict_map=_resolve_conflict_map,
+ )
+ finally:
+ self.autoflush = autoflush
+
+ def _merge(
+ self,
+ state: InstanceState[_O],
+ state_dict: _InstanceDict,
+ *,
+ options: Optional[Sequence[ORMOption]] = None,
+ load: bool,
+ _recursive: Dict[Any, object],
+ _resolve_conflict_map: Dict[_IdentityKeyType[Any], object],
+ ) -> _O:
+ mapper: Mapper[_O] = _state_mapper(state)
+ if state in _recursive:
+ return cast(_O, _recursive[state])
+
+ new_instance = False
+ key = state.key
+
+ merged: Optional[_O]
+
+ if key is None:
+ if state in self._new:
+ util.warn(
+ "Instance %s is already pending in this Session yet is "
+ "being merged again; this is probably not what you want "
+ "to do" % state_str(state)
+ )
+
+ if not load:
+ raise sa_exc.InvalidRequestError(
+ "merge() with load=False option does not support "
+ "objects transient (i.e. unpersisted) objects. flush() "
+ "all changes on mapped instances before merging with "
+ "load=False."
+ )
+ key = mapper._identity_key_from_state(state)
+ key_is_persistent = LoaderCallableStatus.NEVER_SET not in key[
+ 1
+ ] and (
+ not _none_set.intersection(key[1])
+ or (
+ mapper.allow_partial_pks
+ and not _none_set.issuperset(key[1])
+ )
+ )
+ else:
+ key_is_persistent = True
+
+ if key in self.identity_map:
+ try:
+ merged = self.identity_map[key]
+ except KeyError:
+ # object was GC'ed right as we checked for it
+ merged = None
+ else:
+ merged = None
+
+ if merged is None:
+ if key_is_persistent and key in _resolve_conflict_map:
+ merged = cast(_O, _resolve_conflict_map[key])
+
+ elif not load:
+ if state.modified:
+ raise sa_exc.InvalidRequestError(
+ "merge() with load=False option does not support "
+ "objects marked as 'dirty'. flush() all changes on "
+ "mapped instances before merging with load=False."
+ )
+ merged = mapper.class_manager.new_instance()
+ merged_state = attributes.instance_state(merged)
+ merged_state.key = key
+ self._update_impl(merged_state)
+ new_instance = True
+
+ elif key_is_persistent:
+ merged = self.get(
+ mapper.class_,
+ key[1],
+ identity_token=key[2],
+ options=options,
+ )
+
+ if merged is None:
+ merged = mapper.class_manager.new_instance()
+ merged_state = attributes.instance_state(merged)
+ merged_dict = attributes.instance_dict(merged)
+ new_instance = True
+ self._save_or_update_state(merged_state)
+ else:
+ merged_state = attributes.instance_state(merged)
+ merged_dict = attributes.instance_dict(merged)
+
+ _recursive[state] = merged
+ _resolve_conflict_map[key] = merged
+
+ # check that we didn't just pull the exact same
+ # state out.
+ if state is not merged_state:
+ # version check if applicable
+ if mapper.version_id_col is not None:
+ existing_version = mapper._get_state_attr_by_column(
+ state,
+ state_dict,
+ mapper.version_id_col,
+ passive=PassiveFlag.PASSIVE_NO_INITIALIZE,
+ )
+
+ merged_version = mapper._get_state_attr_by_column(
+ merged_state,
+ merged_dict,
+ mapper.version_id_col,
+ passive=PassiveFlag.PASSIVE_NO_INITIALIZE,
+ )
+
+ if (
+ existing_version
+ is not LoaderCallableStatus.PASSIVE_NO_RESULT
+ and merged_version
+ is not LoaderCallableStatus.PASSIVE_NO_RESULT
+ and existing_version != merged_version
+ ):
+ raise exc.StaleDataError(
+ "Version id '%s' on merged state %s "
+ "does not match existing version '%s'. "
+ "Leave the version attribute unset when "
+ "merging to update the most recent version."
+ % (
+ existing_version,
+ state_str(merged_state),
+ merged_version,
+ )
+ )
+
+ merged_state.load_path = state.load_path
+ merged_state.load_options = state.load_options
+
+ # since we are copying load_options, we need to copy
+ # the callables_ that would have been generated by those
+ # load_options.
+ # assumes that the callables we put in state.callables_
+ # are not instance-specific (which they should not be)
+ merged_state._copy_callables(state)
+
+ for prop in mapper.iterate_properties:
+ prop.merge(
+ self,
+ state,
+ state_dict,
+ merged_state,
+ merged_dict,
+ load,
+ _recursive,
+ _resolve_conflict_map,
+ )
+
+ if not load:
+ # remove any history
+ merged_state._commit_all(merged_dict, self.identity_map)
+ merged_state.manager.dispatch._sa_event_merge_wo_load(
+ merged_state, None
+ )
+
+ if new_instance:
+ merged_state.manager.dispatch.load(merged_state, None)
+
+ return merged
+
+ def _validate_persistent(self, state: InstanceState[Any]) -> None:
+ if not self.identity_map.contains_state(state):
+ raise sa_exc.InvalidRequestError(
+ "Instance '%s' is not persistent within this Session"
+ % state_str(state)
+ )
+
+ def _save_impl(self, state: InstanceState[Any]) -> None:
+ if state.key is not None:
+ raise sa_exc.InvalidRequestError(
+ "Object '%s' already has an identity - "
+ "it can't be registered as pending" % state_str(state)
+ )
+
+ obj = state.obj()
+ to_attach = self._before_attach(state, obj)
+ if state not in self._new:
+ self._new[state] = obj
+ state.insert_order = len(self._new)
+ if to_attach:
+ self._after_attach(state, obj)
+
+ def _update_impl(
+ self, state: InstanceState[Any], revert_deletion: bool = False
+ ) -> None:
+ if state.key is None:
+ raise sa_exc.InvalidRequestError(
+ "Instance '%s' is not persisted" % state_str(state)
+ )
+
+ if state._deleted:
+ if revert_deletion:
+ if not state._attached:
+ return
+ del state._deleted
+ else:
+ raise sa_exc.InvalidRequestError(
+ "Instance '%s' has been deleted. "
+ "Use the make_transient() "
+ "function to send this object back "
+ "to the transient state." % state_str(state)
+ )
+
+ obj = state.obj()
+
+ # check for late gc
+ if obj is None:
+ return
+
+ to_attach = self._before_attach(state, obj)
+
+ self._deleted.pop(state, None)
+ if revert_deletion:
+ self.identity_map.replace(state)
+ else:
+ self.identity_map.add(state)
+
+ if to_attach:
+ self._after_attach(state, obj)
+ elif revert_deletion:
+ self.dispatch.deleted_to_persistent(self, state)
+
+ def _save_or_update_impl(self, state: InstanceState[Any]) -> None:
+ if state.key is None:
+ self._save_impl(state)
+ else:
+ self._update_impl(state)
+
+ def enable_relationship_loading(self, obj: object) -> None:
+ """Associate an object with this :class:`.Session` for related
+ object loading.
+
+ .. warning::
+
+ :meth:`.enable_relationship_loading` exists to serve special
+ use cases and is not recommended for general use.
+
+ Accesses of attributes mapped with :func:`_orm.relationship`
+ will attempt to load a value from the database using this
+ :class:`.Session` as the source of connectivity. The values
+ will be loaded based on foreign key and primary key values
+ present on this object - if not present, then those relationships
+ will be unavailable.
+
+ The object will be attached to this session, but will
+ **not** participate in any persistence operations; its state
+ for almost all purposes will remain either "transient" or
+ "detached", except for the case of relationship loading.
+
+ Also note that backrefs will often not work as expected.
+ Altering a relationship-bound attribute on the target object
+ may not fire off a backref event, if the effective value
+ is what was already loaded from a foreign-key-holding value.
+
+ The :meth:`.Session.enable_relationship_loading` method is
+ similar to the ``load_on_pending`` flag on :func:`_orm.relationship`.
+ Unlike that flag, :meth:`.Session.enable_relationship_loading` allows
+ an object to remain transient while still being able to load
+ related items.
+
+ To make a transient object associated with a :class:`.Session`
+ via :meth:`.Session.enable_relationship_loading` pending, add
+ it to the :class:`.Session` using :meth:`.Session.add` normally.
+ If the object instead represents an existing identity in the database,
+ it should be merged using :meth:`.Session.merge`.
+
+ :meth:`.Session.enable_relationship_loading` does not improve
+ behavior when the ORM is used normally - object references should be
+ constructed at the object level, not at the foreign key level, so
+ that they are present in an ordinary way before flush()
+ proceeds. This method is not intended for general use.
+
+ .. seealso::
+
+ :paramref:`_orm.relationship.load_on_pending` - this flag
+ allows per-relationship loading of many-to-ones on items that
+ are pending.
+
+ :func:`.make_transient_to_detached` - allows for an object to
+ be added to a :class:`.Session` without SQL emitted, which then
+ will unexpire attributes on access.
+
+ """
+ try:
+ state = attributes.instance_state(obj)
+ except exc.NO_STATE as err:
+ raise exc.UnmappedInstanceError(obj) from err
+
+ to_attach = self._before_attach(state, obj)
+ state._load_pending = True
+ if to_attach:
+ self._after_attach(state, obj)
+
+ def _before_attach(self, state: InstanceState[Any], obj: object) -> bool:
+ self._autobegin_t()
+
+ if state.session_id == self.hash_key:
+ return False
+
+ if state.session_id and state.session_id in _sessions:
+ raise sa_exc.InvalidRequestError(
+ "Object '%s' is already attached to session '%s' "
+ "(this is '%s')"
+ % (state_str(state), state.session_id, self.hash_key)
+ )
+
+ self.dispatch.before_attach(self, state)
+
+ return True
+
+ def _after_attach(self, state: InstanceState[Any], obj: object) -> None:
+ state.session_id = self.hash_key
+ if state.modified and state._strong_obj is None:
+ state._strong_obj = obj
+ self.dispatch.after_attach(self, state)
+
+ if state.key:
+ self.dispatch.detached_to_persistent(self, state)
+ else:
+ self.dispatch.transient_to_pending(self, state)
+
+ def __contains__(self, instance: object) -> bool:
+ """Return True if the instance is associated with this session.
+
+ The instance may be pending or persistent within the Session for a
+ result of True.
+
+ """
+ try:
+ state = attributes.instance_state(instance)
+ except exc.NO_STATE as err:
+ raise exc.UnmappedInstanceError(instance) from err
+ return self._contains_state(state)
+
+ def __iter__(self) -> Iterator[object]:
+ """Iterate over all pending or persistent instances within this
+ Session.
+
+ """
+ return iter(
+ list(self._new.values()) + list(self.identity_map.values())
+ )
+
+ def _contains_state(self, state: InstanceState[Any]) -> bool:
+ return state in self._new or self.identity_map.contains_state(state)
+
+ def flush(self, objects: Optional[Sequence[Any]] = None) -> None:
+ """Flush all the object changes to the database.
+
+ Writes out all pending object creations, deletions and modifications
+ to the database as INSERTs, DELETEs, UPDATEs, etc. Operations are
+ automatically ordered by the Session's unit of work dependency
+ solver.
+
+ Database operations will be issued in the current transactional
+ context and do not affect the state of the transaction, unless an
+ error occurs, in which case the entire transaction is rolled back.
+ You may flush() as often as you like within a transaction to move
+ changes from Python to the database's transaction buffer.
+
+ :param objects: Optional; restricts the flush operation to operate
+ only on elements that are in the given collection.
+
+ This feature is for an extremely narrow set of use cases where
+ particular objects may need to be operated upon before the
+ full flush() occurs. It is not intended for general use.
+
+ """
+
+ if self._flushing:
+ raise sa_exc.InvalidRequestError("Session is already flushing")
+
+ if self._is_clean():
+ return
+ try:
+ self._flushing = True
+ self._flush(objects)
+ finally:
+ self._flushing = False
+
+ def _flush_warning(self, method: Any) -> None:
+ util.warn(
+ "Usage of the '%s' operation is not currently supported "
+ "within the execution stage of the flush process. "
+ "Results may not be consistent. Consider using alternative "
+ "event listeners or connection-level operations instead." % method
+ )
+
+ def _is_clean(self) -> bool:
+ return (
+ not self.identity_map.check_modified()
+ and not self._deleted
+ and not self._new
+ )
+
+ def _flush(self, objects: Optional[Sequence[object]] = None) -> None:
+ dirty = self._dirty_states
+ if not dirty and not self._deleted and not self._new:
+ self.identity_map._modified.clear()
+ return
+
+ flush_context = UOWTransaction(self)
+
+ if self.dispatch.before_flush:
+ self.dispatch.before_flush(self, flush_context, objects)
+ # re-establish "dirty states" in case the listeners
+ # added
+ dirty = self._dirty_states
+
+ deleted = set(self._deleted)
+ new = set(self._new)
+
+ dirty = set(dirty).difference(deleted)
+
+ # create the set of all objects we want to operate upon
+ if objects:
+ # specific list passed in
+ objset = set()
+ for o in objects:
+ try:
+ state = attributes.instance_state(o)
+
+ except exc.NO_STATE as err:
+ raise exc.UnmappedInstanceError(o) from err
+ objset.add(state)
+ else:
+ objset = None
+
+ # store objects whose fate has been decided
+ processed = set()
+
+ # put all saves/updates into the flush context. detect top-level
+ # orphans and throw them into deleted.
+ if objset:
+ proc = new.union(dirty).intersection(objset).difference(deleted)
+ else:
+ proc = new.union(dirty).difference(deleted)
+
+ for state in proc:
+ is_orphan = _state_mapper(state)._is_orphan(state)
+
+ is_persistent_orphan = is_orphan and state.has_identity
+
+ if (
+ is_orphan
+ and not is_persistent_orphan
+ and state._orphaned_outside_of_session
+ ):
+ self._expunge_states([state])
+ else:
+ _reg = flush_context.register_object(
+ state, isdelete=is_persistent_orphan
+ )
+ assert _reg, "Failed to add object to the flush context!"
+ processed.add(state)
+
+ # put all remaining deletes into the flush context.
+ if objset:
+ proc = deleted.intersection(objset).difference(processed)
+ else:
+ proc = deleted.difference(processed)
+ for state in proc:
+ _reg = flush_context.register_object(state, isdelete=True)
+ assert _reg, "Failed to add object to the flush context!"
+
+ if not flush_context.has_work:
+ return
+
+ flush_context.transaction = transaction = self._autobegin_t()._begin()
+ try:
+ self._warn_on_events = True
+ try:
+ flush_context.execute()
+ finally:
+ self._warn_on_events = False
+
+ self.dispatch.after_flush(self, flush_context)
+
+ flush_context.finalize_flush_changes()
+
+ if not objects and self.identity_map._modified:
+ len_ = len(self.identity_map._modified)
+
+ statelib.InstanceState._commit_all_states(
+ [
+ (state, state.dict)
+ for state in self.identity_map._modified
+ ],
+ instance_dict=self.identity_map,
+ )
+ util.warn(
+ "Attribute history events accumulated on %d "
+ "previously clean instances "
+ "within inner-flush event handlers have been "
+ "reset, and will not result in database updates. "
+ "Consider using set_committed_value() within "
+ "inner-flush event handlers to avoid this warning." % len_
+ )
+
+ # useful assertions:
+ # if not objects:
+ # assert not self.identity_map._modified
+ # else:
+ # assert self.identity_map._modified == \
+ # self.identity_map._modified.difference(objects)
+
+ self.dispatch.after_flush_postexec(self, flush_context)
+
+ transaction.commit()
+
+ except:
+ with util.safe_reraise():
+ transaction.rollback(_capture_exception=True)
+
+ def bulk_save_objects(
+ self,
+ objects: Iterable[object],
+ return_defaults: bool = False,
+ update_changed_only: bool = True,
+ preserve_order: bool = True,
+ ) -> None:
+ """Perform a bulk save of the given list of objects.
+
+ .. legacy::
+
+ This method is a legacy feature as of the 2.0 series of
+ SQLAlchemy. For modern bulk INSERT and UPDATE, see
+ the sections :ref:`orm_queryguide_bulk_insert` and
+ :ref:`orm_queryguide_bulk_update`.
+
+ For general INSERT and UPDATE of existing ORM mapped objects,
+ prefer standard :term:`unit of work` data management patterns,
+ introduced in the :ref:`unified_tutorial` at
+ :ref:`tutorial_orm_data_manipulation`. SQLAlchemy 2.0
+ now uses :ref:`engine_insertmanyvalues` with modern dialects
+ which solves previous issues of bulk INSERT slowness.
+
+ :param objects: a sequence of mapped object instances. The mapped
+ objects are persisted as is, and are **not** associated with the
+ :class:`.Session` afterwards.
+
+ For each object, whether the object is sent as an INSERT or an
+ UPDATE is dependent on the same rules used by the :class:`.Session`
+ in traditional operation; if the object has the
+ :attr:`.InstanceState.key`
+ attribute set, then the object is assumed to be "detached" and
+ will result in an UPDATE. Otherwise, an INSERT is used.
+
+ In the case of an UPDATE, statements are grouped based on which
+ attributes have changed, and are thus to be the subject of each
+ SET clause. If ``update_changed_only`` is False, then all
+ attributes present within each object are applied to the UPDATE
+ statement, which may help in allowing the statements to be grouped
+ together into a larger executemany(), and will also reduce the
+ overhead of checking history on attributes.
+
+ :param return_defaults: when True, rows that are missing values which
+ generate defaults, namely integer primary key defaults and sequences,
+ will be inserted **one at a time**, so that the primary key value
+ is available. In particular this will allow joined-inheritance
+ and other multi-table mappings to insert correctly without the need
+ to provide primary key values ahead of time; however,
+ :paramref:`.Session.bulk_save_objects.return_defaults` **greatly
+ reduces the performance gains** of the method overall. It is strongly
+ advised to please use the standard :meth:`_orm.Session.add_all`
+ approach.
+
+ :param update_changed_only: when True, UPDATE statements are rendered
+ based on those attributes in each state that have logged changes.
+ When False, all attributes present are rendered into the SET clause
+ with the exception of primary key attributes.
+
+ :param preserve_order: when True, the order of inserts and updates
+ matches exactly the order in which the objects are given. When
+ False, common types of objects are grouped into inserts
+ and updates, to allow for more batching opportunities.
+
+ .. seealso::
+
+ :doc:`queryguide/dml`
+
+ :meth:`.Session.bulk_insert_mappings`
+
+ :meth:`.Session.bulk_update_mappings`
+
+ """
+
+ obj_states: Iterable[InstanceState[Any]]
+
+ obj_states = (attributes.instance_state(obj) for obj in objects)
+
+ if not preserve_order:
+ # the purpose of this sort is just so that common mappers
+ # and persistence states are grouped together, so that groupby
+ # will return a single group for a particular type of mapper.
+ # it's not trying to be deterministic beyond that.
+ obj_states = sorted(
+ obj_states,
+ key=lambda state: (id(state.mapper), state.key is not None),
+ )
+
+ def grouping_key(
+ state: InstanceState[_O],
+ ) -> Tuple[Mapper[_O], bool]:
+ return (state.mapper, state.key is not None)
+
+ for (mapper, isupdate), states in itertools.groupby(
+ obj_states, grouping_key
+ ):
+ self._bulk_save_mappings(
+ mapper,
+ states,
+ isupdate,
+ True,
+ return_defaults,
+ update_changed_only,
+ False,
+ )
+
+ def bulk_insert_mappings(
+ self,
+ mapper: Mapper[Any],
+ mappings: Iterable[Dict[str, Any]],
+ return_defaults: bool = False,
+ render_nulls: bool = False,
+ ) -> None:
+ """Perform a bulk insert of the given list of mapping dictionaries.
+
+ .. legacy::
+
+ This method is a legacy feature as of the 2.0 series of
+ SQLAlchemy. For modern bulk INSERT and UPDATE, see
+ the sections :ref:`orm_queryguide_bulk_insert` and
+ :ref:`orm_queryguide_bulk_update`. The 2.0 API shares
+ implementation details with this method and adds new features
+ as well.
+
+ :param mapper: a mapped class, or the actual :class:`_orm.Mapper`
+ object,
+ representing the single kind of object represented within the mapping
+ list.
+
+ :param mappings: a sequence of dictionaries, each one containing the
+ state of the mapped row to be inserted, in terms of the attribute
+ names on the mapped class. If the mapping refers to multiple tables,
+ such as a joined-inheritance mapping, each dictionary must contain all
+ keys to be populated into all tables.
+
+ :param return_defaults: when True, the INSERT process will be altered
+ to ensure that newly generated primary key values will be fetched.
+ The rationale for this parameter is typically to enable
+ :ref:`Joined Table Inheritance <joined_inheritance>` mappings to
+ be bulk inserted.
+
+ .. note:: for backends that don't support RETURNING, the
+ :paramref:`_orm.Session.bulk_insert_mappings.return_defaults`
+ parameter can significantly decrease performance as INSERT
+ statements can no longer be batched. See
+ :ref:`engine_insertmanyvalues`
+ for background on which backends are affected.
+
+ :param render_nulls: When True, a value of ``None`` will result
+ in a NULL value being included in the INSERT statement, rather
+ than the column being omitted from the INSERT. This allows all
+ the rows being INSERTed to have the identical set of columns which
+ allows the full set of rows to be batched to the DBAPI. Normally,
+ each column-set that contains a different combination of NULL values
+ than the previous row must omit a different series of columns from
+ the rendered INSERT statement, which means it must be emitted as a
+ separate statement. By passing this flag, the full set of rows
+ are guaranteed to be batchable into one batch; the cost however is
+ that server-side defaults which are invoked by an omitted column will
+ be skipped, so care must be taken to ensure that these are not
+ necessary.
+
+ .. warning::
+
+ When this flag is set, **server side default SQL values will
+ not be invoked** for those columns that are inserted as NULL;
+ the NULL value will be sent explicitly. Care must be taken
+ to ensure that no server-side default functions need to be
+ invoked for the operation as a whole.
+
+ .. seealso::
+
+ :doc:`queryguide/dml`
+
+ :meth:`.Session.bulk_save_objects`
+
+ :meth:`.Session.bulk_update_mappings`
+
+ """
+ self._bulk_save_mappings(
+ mapper,
+ mappings,
+ False,
+ False,
+ return_defaults,
+ False,
+ render_nulls,
+ )
+
+ def bulk_update_mappings(
+ self, mapper: Mapper[Any], mappings: Iterable[Dict[str, Any]]
+ ) -> None:
+ """Perform a bulk update of the given list of mapping dictionaries.
+
+ .. legacy::
+
+ This method is a legacy feature as of the 2.0 series of
+ SQLAlchemy. For modern bulk INSERT and UPDATE, see
+ the sections :ref:`orm_queryguide_bulk_insert` and
+ :ref:`orm_queryguide_bulk_update`. The 2.0 API shares
+ implementation details with this method and adds new features
+ as well.
+
+ :param mapper: a mapped class, or the actual :class:`_orm.Mapper`
+ object,
+ representing the single kind of object represented within the mapping
+ list.
+
+ :param mappings: a sequence of dictionaries, each one containing the
+ state of the mapped row to be updated, in terms of the attribute names
+ on the mapped class. If the mapping refers to multiple tables, such
+ as a joined-inheritance mapping, each dictionary may contain keys
+ corresponding to all tables. All those keys which are present and
+ are not part of the primary key are applied to the SET clause of the
+ UPDATE statement; the primary key values, which are required, are
+ applied to the WHERE clause.
+
+
+ .. seealso::
+
+ :doc:`queryguide/dml`
+
+ :meth:`.Session.bulk_insert_mappings`
+
+ :meth:`.Session.bulk_save_objects`
+
+ """
+ self._bulk_save_mappings(
+ mapper, mappings, True, False, False, False, False
+ )
+
+ def _bulk_save_mappings(
+ self,
+ mapper: Mapper[_O],
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
+ isupdate: bool,
+ isstates: bool,
+ return_defaults: bool,
+ update_changed_only: bool,
+ render_nulls: bool,
+ ) -> None:
+ mapper = _class_to_mapper(mapper)
+ self._flushing = True
+
+ transaction = self._autobegin_t()._begin()
+ try:
+ if isupdate:
+ bulk_persistence._bulk_update(
+ mapper,
+ mappings,
+ transaction,
+ isstates,
+ update_changed_only,
+ )
+ else:
+ bulk_persistence._bulk_insert(
+ mapper,
+ mappings,
+ transaction,
+ isstates,
+ return_defaults,
+ render_nulls,
+ )
+ transaction.commit()
+
+ except:
+ with util.safe_reraise():
+ transaction.rollback(_capture_exception=True)
+ finally:
+ self._flushing = False
+
+ def is_modified(
+ self, instance: object, include_collections: bool = True
+ ) -> bool:
+ r"""Return ``True`` if the given instance has locally
+ modified attributes.
+
+ This method retrieves the history for each instrumented
+ attribute on the instance and performs a comparison of the current
+ value to its previously committed value, if any.
+
+ It is in effect a more expensive and accurate
+ version of checking for the given instance in the
+ :attr:`.Session.dirty` collection; a full test for
+ each attribute's net "dirty" status is performed.
+
+ E.g.::
+
+ return session.is_modified(someobject)
+
+ A few caveats to this method apply:
+
+ * Instances present in the :attr:`.Session.dirty` collection may
+ report ``False`` when tested with this method. This is because
+ the object may have received change events via attribute mutation,
+ thus placing it in :attr:`.Session.dirty`, but ultimately the state
+ is the same as that loaded from the database, resulting in no net
+ change here.
+ * Scalar attributes may not have recorded the previously set
+ value when a new value was applied, if the attribute was not loaded,
+ or was expired, at the time the new value was received - in these
+ cases, the attribute is assumed to have a change, even if there is
+ ultimately no net change against its database value. SQLAlchemy in
+ most cases does not need the "old" value when a set event occurs, so
+ it skips the expense of a SQL call if the old value isn't present,
+ based on the assumption that an UPDATE of the scalar value is
+ usually needed, and in those few cases where it isn't, is less
+ expensive on average than issuing a defensive SELECT.
+
+ The "old" value is fetched unconditionally upon set only if the
+ attribute container has the ``active_history`` flag set to ``True``.
+ This flag is set typically for primary key attributes and scalar
+ object references that are not a simple many-to-one. To set this
+ flag for any arbitrary mapped column, use the ``active_history``
+ argument with :func:`.column_property`.
+
+ :param instance: mapped instance to be tested for pending changes.
+ :param include_collections: Indicates if multivalued collections
+ should be included in the operation. Setting this to ``False`` is a
+ way to detect only local-column based properties (i.e. scalar columns
+ or many-to-one foreign keys) that would result in an UPDATE for this
+ instance upon flush.
+
+ """
+ state = object_state(instance)
+
+ if not state.modified:
+ return False
+
+ dict_ = state.dict
+
+ for attr in state.manager.attributes:
+ if (
+ not include_collections
+ and hasattr(attr.impl, "get_collection")
+ ) or not hasattr(attr.impl, "get_history"):
+ continue
+
+ (added, unchanged, deleted) = attr.impl.get_history(
+ state, dict_, passive=PassiveFlag.NO_CHANGE
+ )
+
+ if added or deleted:
+ return True
+ else:
+ return False
+
+ @property
+ def is_active(self) -> bool:
+ """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`
+
+ """
+ return self._transaction is None or self._transaction.is_active
+
+ @property
+ def _dirty_states(self) -> Iterable[InstanceState[Any]]:
+ """The set of all persistent states considered dirty.
+
+ This method returns all states that were modified including
+ those that were possibly deleted.
+
+ """
+ return self.identity_map._dirty_states()
+
+ @property
+ def dirty(self) -> IdentitySet:
+ """The set of all persistent instances considered dirty.
+
+ E.g.::
+
+ some_mapped_object in session.dirty
+
+ Instances are considered dirty when they were modified but not
+ deleted.
+
+ Note that this 'dirty' calculation is 'optimistic'; most
+ attribute-setting or collection modification operations will
+ mark an instance as 'dirty' and place it in this set, even if
+ there is no net change to the attribute's value. At flush
+ time, the value of each attribute is compared to its
+ previously saved value, and if there's no net change, no SQL
+ operation will occur (this is a more expensive operation so
+ it's only done at flush time).
+
+ To check if an instance has actionable net changes to its
+ attributes, use the :meth:`.Session.is_modified` method.
+
+ """
+ return IdentitySet(
+ [
+ state.obj()
+ for state in self._dirty_states
+ if state not in self._deleted
+ ]
+ )
+
+ @property
+ def deleted(self) -> IdentitySet:
+ "The set of all instances marked as 'deleted' within this ``Session``"
+
+ return util.IdentitySet(list(self._deleted.values()))
+
+ @property
+ def new(self) -> IdentitySet:
+ "The set of all instances marked as 'new' within this ``Session``."
+
+ return util.IdentitySet(list(self._new.values()))
+
+
+_S = TypeVar("_S", bound="Session")
+
+
+class sessionmaker(_SessionClassMethods, Generic[_S]):
+ """A configurable :class:`.Session` factory.
+
+ The :class:`.sessionmaker` factory generates new
+ :class:`.Session` objects when called, creating them given
+ the configurational arguments established here.
+
+ e.g.::
+
+ from sqlalchemy import create_engine
+ from sqlalchemy.orm import sessionmaker
+
+ # an Engine, which the Session will use for connection
+ # resources
+ engine = create_engine('postgresql+psycopg2://scott:tiger@localhost/')
+
+ Session = sessionmaker(engine)
+
+ 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)
+
+ with 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
+
+ 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, sessionmaker does not have
+ # an engine bound yet
+ Session = sessionmaker()
+
+ # ... 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::
+
+ :ref:`session_getting` - introductory text on creating
+ sessions using :class:`.sessionmaker`.
+
+ """
+
+ class_: Type[_S]
+
+ @overload
+ def __init__(
+ self,
+ bind: Optional[_SessionBind] = ...,
+ *,
+ class_: Type[_S],
+ autoflush: bool = ...,
+ expire_on_commit: bool = ...,
+ info: Optional[_InfoType] = ...,
+ **kw: Any,
+ ): ...
+
+ @overload
+ def __init__(
+ self: "sessionmaker[Session]",
+ bind: Optional[_SessionBind] = ...,
+ *,
+ autoflush: bool = ...,
+ expire_on_commit: bool = ...,
+ info: Optional[_InfoType] = ...,
+ **kw: Any,
+ ): ...
+
+ def __init__(
+ self,
+ bind: Optional[_SessionBind] = None,
+ *,
+ class_: Type[_S] = Session, # type: ignore
+ autoflush: bool = True,
+ expire_on_commit: bool = True,
+ info: Optional[_InfoType] = None,
+ **kw: Any,
+ ):
+ r"""Construct a new :class:`.sessionmaker`.
+
+ All arguments here except for ``class_`` correspond to arguments
+ accepted by :class:`.Session` directly. See the
+ :meth:`.Session.__init__` docstring for more details on parameters.
+
+ :param bind: a :class:`_engine.Engine` or other :class:`.Connectable`
+ with
+ which newly created :class:`.Session` objects will be associated.
+ :param class\_: class to use in order to create new :class:`.Session`
+ objects. Defaults to :class:`.Session`.
+ :param autoflush: The autoflush setting to use with newly created
+ :class:`.Session` objects.
+
+ .. seealso::
+
+ :ref:`session_flushing` - additional background on autoflush
+
+ :param expire_on_commit=True: the
+ :paramref:`_orm.Session.expire_on_commit` setting to use
+ with newly created :class:`.Session` objects.
+
+ :param info: optional dictionary of information that will be available
+ via :attr:`.Session.info`. Note this dictionary is *updated*, not
+ replaced, when the ``info`` parameter is specified to the specific
+ :class:`.Session` construction operation.
+
+ :param \**kw: all other keyword arguments are passed to the
+ constructor of newly created :class:`.Session` objects.
+
+ """
+ kw["bind"] = bind
+ kw["autoflush"] = autoflush
+ kw["expire_on_commit"] = expire_on_commit
+ if info is not None:
+ kw["info"] = info
+ self.kw = kw
+ # make our own subclass of the given class, so that
+ # events can be associated with it specifically.
+ self.class_ = type(class_.__name__, (class_,), {})
+
+ def begin(self) -> contextlib.AbstractContextManager[_S]:
+ """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
+
+
+ """
+
+ session = self()
+ return session._maker_context_manager()
+
+ def __call__(self, **local_kw: Any) -> _S:
+ """Produce a new :class:`.Session` object using the configuration
+ established in this :class:`.sessionmaker`.
+
+ In Python, the ``__call__`` method is invoked on an object when
+ it is "called" in the same way as a function::
+
+ Session = sessionmaker(some_engine)
+ session = Session() # invokes sessionmaker.__call__()
+
+ """
+ for k, v in self.kw.items():
+ if k == "info" and "info" in local_kw:
+ d = v.copy()
+ d.update(local_kw["info"])
+ local_kw["info"] = d
+ else:
+ local_kw.setdefault(k, v)
+ return self.class_(**local_kw)
+
+ def configure(self, **new_kw: Any) -> None:
+ """(Re)configure the arguments for this sessionmaker.
+
+ e.g.::
+
+ Session = sessionmaker()
+
+ Session.configure(bind=create_engine('sqlite://'))
+ """
+ self.kw.update(new_kw)
+
+ def __repr__(self) -> str:
+ return "%s(class_=%r, %s)" % (
+ self.__class__.__name__,
+ self.class_.__name__,
+ ", ".join("%s=%r" % (k, v) for k, v in self.kw.items()),
+ )
+
+
+def close_all_sessions() -> None:
+ """Close all sessions in memory.
+
+ This function consults a global registry of all :class:`.Session` objects
+ and calls :meth:`.Session.close` on them, which resets them to a clean
+ state.
+
+ This function is not for general use but may be useful for test suites
+ within the teardown scheme.
+
+ .. versionadded:: 1.3
+
+ """
+
+ for sess in _sessions.values():
+ sess.close()
+
+
+def make_transient(instance: object) -> None:
+ """Alter the state of the given instance so that it is :term:`transient`.
+
+ .. note::
+
+ :func:`.make_transient` is a special-case function for
+ advanced use cases only.
+
+ The given mapped instance is assumed to be in the :term:`persistent` or
+ :term:`detached` state. The function will remove its association with any
+ :class:`.Session` as well as its :attr:`.InstanceState.identity`. The
+ effect is that the object will behave as though it were newly constructed,
+ except retaining any attribute / collection values that were loaded at the
+ time of the call. The :attr:`.InstanceState.deleted` flag is also reset
+ if this object had been deleted as a result of using
+ :meth:`.Session.delete`.
+
+ .. warning::
+
+ :func:`.make_transient` does **not** "unexpire" or otherwise eagerly
+ load ORM-mapped attributes that are not currently loaded at the time
+ the function is called. This includes attributes which:
+
+ * were expired via :meth:`.Session.expire`
+
+ * were expired as the natural effect of committing a session
+ transaction, e.g. :meth:`.Session.commit`
+
+ * are normally :term:`lazy loaded` but are not currently loaded
+
+ * are "deferred" (see :ref:`orm_queryguide_column_deferral`) and are
+ not yet loaded
+
+ * were not present in the query which loaded this object, such as that
+ which is common in joined table inheritance and other scenarios.
+
+ After :func:`.make_transient` is called, unloaded attributes such
+ as those above will normally resolve to the value ``None`` when
+ accessed, or an empty collection for a collection-oriented attribute.
+ As the object is transient and un-associated with any database
+ identity, it will no longer retrieve these values.
+
+ .. seealso::
+
+ :func:`.make_transient_to_detached`
+
+ """
+ state = attributes.instance_state(instance)
+ s = _state_session(state)
+ if s:
+ s._expunge_states([state])
+
+ # remove expired state
+ state.expired_attributes.clear()
+
+ # remove deferred callables
+ if state.callables:
+ del state.callables
+
+ if state.key:
+ del state.key
+ if state._deleted:
+ del state._deleted
+
+
+def make_transient_to_detached(instance: object) -> None:
+ """Make the given transient instance :term:`detached`.
+
+ .. note::
+
+ :func:`.make_transient_to_detached` is a special-case function for
+ advanced use cases only.
+
+ All attribute history on the given instance
+ will be reset as though the instance were freshly loaded
+ from a query. Missing attributes will be marked as expired.
+ The primary key attributes of the object, which are required, will be made
+ into the "key" of the instance.
+
+ The object can then be added to a session, or merged
+ possibly with the load=False flag, at which point it will look
+ as if it were loaded that way, without emitting SQL.
+
+ This is a special use case function that differs from a normal
+ call to :meth:`.Session.merge` in that a given persistent state
+ can be manufactured without any SQL calls.
+
+ .. seealso::
+
+ :func:`.make_transient`
+
+ :meth:`.Session.enable_relationship_loading`
+
+ """
+ state = attributes.instance_state(instance)
+ if state.session_id or state.key:
+ raise sa_exc.InvalidRequestError("Given object must be transient")
+ state.key = state.mapper._identity_key_from_state(state)
+ if state._deleted:
+ del state._deleted
+ state._commit_all(state.dict)
+ state._expire_attributes(state.dict, state.unloaded)
+
+
+def object_session(instance: object) -> Optional[Session]:
+ """Return the :class:`.Session` to which the given instance belongs.
+
+ This is essentially the same as the :attr:`.InstanceState.session`
+ accessor. See that attribute for details.
+
+ """
+
+ try:
+ state = attributes.instance_state(instance)
+ except exc.NO_STATE as err:
+ raise exc.UnmappedInstanceError(instance) from err
+ else:
+ return _state_session(state)
+
+
+_new_sessionid = util.counter()
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/state.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/state.py
new file mode 100644
index 0000000..03b81f9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/state.py
@@ -0,0 +1,1136 @@
+# orm/state.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""Defines instrumentation of instances.
+
+This module is usually not directly visible to user applications, but
+defines a large part of the ORM's interactivity.
+
+"""
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Generic
+from typing import Iterable
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+import weakref
+
+from . import base
+from . import exc as orm_exc
+from . import interfaces
+from ._typing import _O
+from ._typing import is_collection_impl
+from .base import ATTR_WAS_SET
+from .base import INIT_OK
+from .base import LoaderCallableStatus
+from .base import NEVER_SET
+from .base import NO_VALUE
+from .base import PASSIVE_NO_INITIALIZE
+from .base import PASSIVE_NO_RESULT
+from .base import PASSIVE_OFF
+from .base import SQL_OK
+from .path_registry import PathRegistry
+from .. import exc as sa_exc
+from .. import inspection
+from .. import util
+from ..util.typing import Literal
+from ..util.typing import Protocol
+
+if TYPE_CHECKING:
+ from ._typing import _IdentityKeyType
+ from ._typing import _InstanceDict
+ from ._typing import _LoaderCallable
+ from .attributes import AttributeImpl
+ from .attributes import History
+ from .base import PassiveFlag
+ from .collections import _AdaptedCollectionProtocol
+ from .identity import IdentityMap
+ from .instrumentation import ClassManager
+ from .interfaces import ORMOption
+ from .mapper import Mapper
+ from .session import Session
+ from ..engine import Row
+ from ..ext.asyncio.session import async_session as _async_provider
+ from ..ext.asyncio.session import AsyncSession
+
+if TYPE_CHECKING:
+ _sessions: weakref.WeakValueDictionary[int, Session]
+else:
+ # late-populated by session.py
+ _sessions = None
+
+
+if not TYPE_CHECKING:
+ # optionally late-provided by sqlalchemy.ext.asyncio.session
+
+ _async_provider = None # noqa
+
+
+class _InstanceDictProto(Protocol):
+ def __call__(self) -> Optional[IdentityMap]: ...
+
+
+class _InstallLoaderCallableProto(Protocol[_O]):
+ """used at result loading time to install a _LoaderCallable callable
+ upon a specific InstanceState, which will be used to populate an
+ attribute when that attribute is accessed.
+
+ Concrete examples are per-instance deferred column loaders and
+ relationship lazy loaders.
+
+ """
+
+ def __call__(
+ self, state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any]
+ ) -> None: ...
+
+
+@inspection._self_inspects
+class InstanceState(interfaces.InspectionAttrInfo, Generic[_O]):
+ """tracks state information at the instance level.
+
+ The :class:`.InstanceState` is a key object used by the
+ SQLAlchemy ORM in order to track the state of an object;
+ it is created the moment an object is instantiated, typically
+ as a result of :term:`instrumentation` which SQLAlchemy applies
+ to the ``__init__()`` method of the class.
+
+ :class:`.InstanceState` is also a semi-public object,
+ available for runtime inspection as to the state of a
+ mapped instance, including information such as its current
+ status within a particular :class:`.Session` and details
+ about data on individual attributes. The public API
+ in order to acquire a :class:`.InstanceState` object
+ is to use the :func:`_sa.inspect` system::
+
+ >>> from sqlalchemy import inspect
+ >>> insp = inspect(some_mapped_object)
+ >>> insp.attrs.nickname.history
+ History(added=['new nickname'], unchanged=(), deleted=['nickname'])
+
+ .. seealso::
+
+ :ref:`orm_mapper_inspection_instancestate`
+
+ """
+
+ __slots__ = (
+ "__dict__",
+ "__weakref__",
+ "class_",
+ "manager",
+ "obj",
+ "committed_state",
+ "expired_attributes",
+ )
+
+ manager: ClassManager[_O]
+ session_id: Optional[int] = None
+ key: Optional[_IdentityKeyType[_O]] = None
+ runid: Optional[int] = None
+ load_options: Tuple[ORMOption, ...] = ()
+ load_path: PathRegistry = PathRegistry.root
+ insert_order: Optional[int] = None
+ _strong_obj: Optional[object] = None
+ obj: weakref.ref[_O]
+
+ committed_state: Dict[str, Any]
+
+ modified: bool = False
+ expired: bool = False
+ _deleted: bool = False
+ _load_pending: bool = False
+ _orphaned_outside_of_session: bool = False
+ is_instance: bool = True
+ identity_token: object = None
+ _last_known_values: Optional[Dict[str, Any]] = None
+
+ _instance_dict: _InstanceDictProto
+ """A weak reference, or in the default case a plain callable, that
+ returns a reference to the current :class:`.IdentityMap`, if any.
+
+ """
+ if not TYPE_CHECKING:
+
+ def _instance_dict(self):
+ """default 'weak reference' for _instance_dict"""
+ return None
+
+ expired_attributes: Set[str]
+ """The set of keys which are 'expired' to be loaded by
+ the manager's deferred scalar loader, assuming no pending
+ changes.
+
+ see also the ``unmodified`` collection which is intersected
+ against this set when a refresh operation occurs."""
+
+ callables: Dict[str, Callable[[InstanceState[_O], PassiveFlag], Any]]
+ """A namespace where a per-state loader callable can be associated.
+
+ In SQLAlchemy 1.0, this is only used for lazy loaders / deferred
+ loaders that were set up via query option.
+
+ Previously, callables was used also to indicate expired attributes
+ by storing a link to the InstanceState itself in this dictionary.
+ This role is now handled by the expired_attributes set.
+
+ """
+
+ if not TYPE_CHECKING:
+ callables = util.EMPTY_DICT
+
+ def __init__(self, obj: _O, manager: ClassManager[_O]):
+ self.class_ = obj.__class__
+ self.manager = manager
+ self.obj = weakref.ref(obj, self._cleanup)
+ self.committed_state = {}
+ self.expired_attributes = set()
+
+ @util.memoized_property
+ def attrs(self) -> util.ReadOnlyProperties[AttributeState]:
+ """Return a namespace representing each attribute on
+ the mapped object, including its current value
+ and history.
+
+ The returned object is an instance of :class:`.AttributeState`.
+ This object allows inspection of the current data
+ within an attribute as well as attribute history
+ since the last flush.
+
+ """
+ return util.ReadOnlyProperties(
+ {key: AttributeState(self, key) for key in self.manager}
+ )
+
+ @property
+ def transient(self) -> bool:
+ """Return ``True`` if the object is :term:`transient`.
+
+ .. seealso::
+
+ :ref:`session_object_states`
+
+ """
+ return self.key is None and not self._attached
+
+ @property
+ def pending(self) -> bool:
+ """Return ``True`` if the object is :term:`pending`.
+
+
+ .. seealso::
+
+ :ref:`session_object_states`
+
+ """
+ return self.key is None and self._attached
+
+ @property
+ def deleted(self) -> bool:
+ """Return ``True`` if the object is :term:`deleted`.
+
+ An object that is in the deleted state is guaranteed to
+ not be within the :attr:`.Session.identity_map` of its parent
+ :class:`.Session`; however if the session's transaction is rolled
+ back, the object will be restored to the persistent state and
+ the identity map.
+
+ .. note::
+
+ The :attr:`.InstanceState.deleted` attribute refers to a specific
+ state of the object that occurs between the "persistent" and
+ "detached" states; once the object is :term:`detached`, the
+ :attr:`.InstanceState.deleted` attribute **no longer returns
+ True**; in order to detect that a state was deleted, regardless
+ of whether or not the object is associated with a
+ :class:`.Session`, use the :attr:`.InstanceState.was_deleted`
+ accessor.
+
+ .. versionadded: 1.1
+
+ .. seealso::
+
+ :ref:`session_object_states`
+
+ """
+ return self.key is not None and self._attached and self._deleted
+
+ @property
+ def was_deleted(self) -> bool:
+ """Return True if this object is or was previously in the
+ "deleted" state and has not been reverted to persistent.
+
+ This flag returns True once the object was deleted in flush.
+ When the object is expunged from the session either explicitly
+ or via transaction commit and enters the "detached" state,
+ this flag will continue to report True.
+
+ .. seealso::
+
+ :attr:`.InstanceState.deleted` - refers to the "deleted" state
+
+ :func:`.orm.util.was_deleted` - standalone function
+
+ :ref:`session_object_states`
+
+ """
+ return self._deleted
+
+ @property
+ def persistent(self) -> bool:
+ """Return ``True`` if the object is :term:`persistent`.
+
+ An object that is in the persistent state is guaranteed to
+ be within the :attr:`.Session.identity_map` of its parent
+ :class:`.Session`.
+
+ .. seealso::
+
+ :ref:`session_object_states`
+
+ """
+ return self.key is not None and self._attached and not self._deleted
+
+ @property
+ def detached(self) -> bool:
+ """Return ``True`` if the object is :term:`detached`.
+
+ .. seealso::
+
+ :ref:`session_object_states`
+
+ """
+ return self.key is not None and not self._attached
+
+ @util.non_memoized_property
+ @util.preload_module("sqlalchemy.orm.session")
+ def _attached(self) -> bool:
+ return (
+ self.session_id is not None
+ and self.session_id in util.preloaded.orm_session._sessions
+ )
+
+ def _track_last_known_value(self, key: str) -> None:
+ """Track the last known value of a particular key after expiration
+ operations.
+
+ .. versionadded:: 1.3
+
+ """
+
+ lkv = self._last_known_values
+ if lkv is None:
+ self._last_known_values = lkv = {}
+ if key not in lkv:
+ lkv[key] = NO_VALUE
+
+ @property
+ def session(self) -> Optional[Session]:
+ """Return the owning :class:`.Session` for this instance,
+ or ``None`` if none available.
+
+ Note that the result here can in some cases be *different*
+ from that of ``obj in session``; an object that's been deleted
+ will report as not ``in session``, however if the transaction is
+ still in progress, this attribute will still refer to that session.
+ Only when the transaction is completed does the object become
+ fully detached under normal circumstances.
+
+ .. seealso::
+
+ :attr:`_orm.InstanceState.async_session`
+
+ """
+ if self.session_id:
+ try:
+ return _sessions[self.session_id]
+ except KeyError:
+ pass
+ return None
+
+ @property
+ def async_session(self) -> Optional[AsyncSession]:
+ """Return the owning :class:`_asyncio.AsyncSession` for this instance,
+ or ``None`` if none available.
+
+ This attribute is only non-None when the :mod:`sqlalchemy.ext.asyncio`
+ API is in use for this ORM object. The returned
+ :class:`_asyncio.AsyncSession` object will be a proxy for the
+ :class:`_orm.Session` object that would be returned from the
+ :attr:`_orm.InstanceState.session` attribute for this
+ :class:`_orm.InstanceState`.
+
+ .. versionadded:: 1.4.18
+
+ .. seealso::
+
+ :ref:`asyncio_toplevel`
+
+ """
+ if _async_provider is None:
+ return None
+
+ sess = self.session
+ if sess is not None:
+ return _async_provider(sess)
+ else:
+ return None
+
+ @property
+ def object(self) -> Optional[_O]:
+ """Return the mapped object represented by this
+ :class:`.InstanceState`.
+
+ Returns None if the object has been garbage collected
+
+ """
+ return self.obj()
+
+ @property
+ def identity(self) -> Optional[Tuple[Any, ...]]:
+ """Return the mapped identity of the mapped object.
+ This is the primary key identity as persisted by the ORM
+ which can always be passed directly to
+ :meth:`_query.Query.get`.
+
+ Returns ``None`` if the object has no primary key identity.
+
+ .. note::
+ An object which is :term:`transient` or :term:`pending`
+ does **not** have a mapped identity until it is flushed,
+ even if its attributes include primary key values.
+
+ """
+ if self.key is None:
+ return None
+ else:
+ return self.key[1]
+
+ @property
+ def identity_key(self) -> Optional[_IdentityKeyType[_O]]:
+ """Return the identity key for the mapped object.
+
+ This is the key used to locate the object within
+ the :attr:`.Session.identity_map` mapping. It contains
+ the identity as returned by :attr:`.identity` within it.
+
+
+ """
+ return self.key
+
+ @util.memoized_property
+ def parents(self) -> Dict[int, Union[Literal[False], InstanceState[Any]]]:
+ return {}
+
+ @util.memoized_property
+ def _pending_mutations(self) -> Dict[str, PendingCollection]:
+ return {}
+
+ @util.memoized_property
+ def _empty_collections(self) -> Dict[str, _AdaptedCollectionProtocol]:
+ return {}
+
+ @util.memoized_property
+ def mapper(self) -> Mapper[_O]:
+ """Return the :class:`_orm.Mapper` used for this mapped object."""
+ return self.manager.mapper
+
+ @property
+ def has_identity(self) -> bool:
+ """Return ``True`` if this object has an identity key.
+
+ This should always have the same value as the
+ expression ``state.persistent`` or ``state.detached``.
+
+ """
+ return bool(self.key)
+
+ @classmethod
+ def _detach_states(
+ self,
+ states: Iterable[InstanceState[_O]],
+ session: Session,
+ to_transient: bool = False,
+ ) -> None:
+ persistent_to_detached = (
+ session.dispatch.persistent_to_detached or None
+ )
+ deleted_to_detached = session.dispatch.deleted_to_detached or None
+ pending_to_transient = session.dispatch.pending_to_transient or None
+ persistent_to_transient = (
+ session.dispatch.persistent_to_transient or None
+ )
+
+ for state in states:
+ deleted = state._deleted
+ pending = state.key is None
+ persistent = not pending and not deleted
+
+ state.session_id = None
+
+ if to_transient and state.key:
+ del state.key
+ if persistent:
+ if to_transient:
+ if persistent_to_transient is not None:
+ persistent_to_transient(session, state)
+ elif persistent_to_detached is not None:
+ persistent_to_detached(session, state)
+ elif deleted and deleted_to_detached is not None:
+ deleted_to_detached(session, state)
+ elif pending and pending_to_transient is not None:
+ pending_to_transient(session, state)
+
+ state._strong_obj = None
+
+ def _detach(self, session: Optional[Session] = None) -> None:
+ if session:
+ InstanceState._detach_states([self], session)
+ else:
+ self.session_id = self._strong_obj = None
+
+ def _dispose(self) -> None:
+ # used by the test suite, apparently
+ self._detach()
+
+ def _cleanup(self, ref: weakref.ref[_O]) -> None:
+ """Weakref callback cleanup.
+
+ This callable cleans out the state when it is being garbage
+ collected.
+
+ this _cleanup **assumes** that there are no strong refs to us!
+ Will not work otherwise!
+
+ """
+
+ # Python builtins become undefined during interpreter shutdown.
+ # Guard against exceptions during this phase, as the method cannot
+ # proceed in any case if builtins have been undefined.
+ if dict is None:
+ return
+
+ instance_dict = self._instance_dict()
+ if instance_dict is not None:
+ instance_dict._fast_discard(self)
+ del self._instance_dict
+
+ # we can't possibly be in instance_dict._modified
+ # b.c. this is weakref cleanup only, that set
+ # is strong referencing!
+ # assert self not in instance_dict._modified
+
+ self.session_id = self._strong_obj = None
+
+ @property
+ def dict(self) -> _InstanceDict:
+ """Return the instance dict used by the object.
+
+ Under normal circumstances, this is always synonymous
+ with the ``__dict__`` attribute of the mapped object,
+ unless an alternative instrumentation system has been
+ configured.
+
+ In the case that the actual object has been garbage
+ collected, this accessor returns a blank dictionary.
+
+ """
+ o = self.obj()
+ if o is not None:
+ return base.instance_dict(o)
+ else:
+ return {}
+
+ def _initialize_instance(*mixed: Any, **kwargs: Any) -> None:
+ self, instance, args = mixed[0], mixed[1], mixed[2:] # noqa
+ manager = self.manager
+
+ manager.dispatch.init(self, args, kwargs)
+
+ try:
+ manager.original_init(*mixed[1:], **kwargs)
+ except:
+ with util.safe_reraise():
+ manager.dispatch.init_failure(self, args, kwargs)
+
+ def get_history(self, key: str, passive: PassiveFlag) -> History:
+ return self.manager[key].impl.get_history(self, self.dict, passive)
+
+ def get_impl(self, key: str) -> AttributeImpl:
+ return self.manager[key].impl
+
+ def _get_pending_mutation(self, key: str) -> PendingCollection:
+ if key not in self._pending_mutations:
+ self._pending_mutations[key] = PendingCollection()
+ return self._pending_mutations[key]
+
+ def __getstate__(self) -> Dict[str, Any]:
+ state_dict: Dict[str, Any] = {
+ "instance": self.obj(),
+ "class_": self.class_,
+ "committed_state": self.committed_state,
+ "expired_attributes": self.expired_attributes,
+ }
+ state_dict.update(
+ (k, self.__dict__[k])
+ for k in (
+ "_pending_mutations",
+ "modified",
+ "expired",
+ "callables",
+ "key",
+ "parents",
+ "load_options",
+ "class_",
+ "expired_attributes",
+ "info",
+ )
+ if k in self.__dict__
+ )
+ if self.load_path:
+ state_dict["load_path"] = self.load_path.serialize()
+
+ state_dict["manager"] = self.manager._serialize(self, state_dict)
+
+ return state_dict
+
+ def __setstate__(self, state_dict: Dict[str, Any]) -> None:
+ inst = state_dict["instance"]
+ if inst is not None:
+ self.obj = weakref.ref(inst, self._cleanup)
+ self.class_ = inst.__class__
+ else:
+ self.obj = lambda: None # type: ignore
+ self.class_ = state_dict["class_"]
+
+ self.committed_state = state_dict.get("committed_state", {})
+ self._pending_mutations = state_dict.get("_pending_mutations", {})
+ self.parents = state_dict.get("parents", {})
+ self.modified = state_dict.get("modified", False)
+ self.expired = state_dict.get("expired", False)
+ if "info" in state_dict:
+ self.info.update(state_dict["info"])
+ if "callables" in state_dict:
+ self.callables = state_dict["callables"]
+
+ self.expired_attributes = state_dict["expired_attributes"]
+ else:
+ if "expired_attributes" in state_dict:
+ self.expired_attributes = state_dict["expired_attributes"]
+ else:
+ self.expired_attributes = set()
+
+ self.__dict__.update(
+ [
+ (k, state_dict[k])
+ for k in ("key", "load_options")
+ if k in state_dict
+ ]
+ )
+ if self.key:
+ self.identity_token = self.key[2]
+
+ if "load_path" in state_dict:
+ self.load_path = PathRegistry.deserialize(state_dict["load_path"])
+
+ state_dict["manager"](self, inst, state_dict)
+
+ def _reset(self, dict_: _InstanceDict, key: str) -> None:
+ """Remove the given attribute and any
+ callables associated with it."""
+
+ old = dict_.pop(key, None)
+ manager_impl = self.manager[key].impl
+ if old is not None and is_collection_impl(manager_impl):
+ manager_impl._invalidate_collection(old)
+ self.expired_attributes.discard(key)
+ if self.callables:
+ self.callables.pop(key, None)
+
+ def _copy_callables(self, from_: InstanceState[Any]) -> None:
+ if "callables" in from_.__dict__:
+ self.callables = dict(from_.callables)
+
+ @classmethod
+ def _instance_level_callable_processor(
+ cls, manager: ClassManager[_O], fn: _LoaderCallable, key: Any
+ ) -> _InstallLoaderCallableProto[_O]:
+ impl = manager[key].impl
+ if is_collection_impl(impl):
+ fixed_impl = impl
+
+ def _set_callable(
+ state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any]
+ ) -> None:
+ if "callables" not in state.__dict__:
+ state.callables = {}
+ old = dict_.pop(key, None)
+ if old is not None:
+ fixed_impl._invalidate_collection(old)
+ state.callables[key] = fn
+
+ else:
+
+ def _set_callable(
+ state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any]
+ ) -> None:
+ if "callables" not in state.__dict__:
+ state.callables = {}
+ state.callables[key] = fn
+
+ return _set_callable
+
+ def _expire(
+ self, dict_: _InstanceDict, modified_set: Set[InstanceState[Any]]
+ ) -> None:
+ self.expired = True
+ if self.modified:
+ modified_set.discard(self)
+ self.committed_state.clear()
+ self.modified = False
+
+ self._strong_obj = None
+
+ if "_pending_mutations" in self.__dict__:
+ del self.__dict__["_pending_mutations"]
+
+ if "parents" in self.__dict__:
+ del self.__dict__["parents"]
+
+ self.expired_attributes.update(
+ [impl.key for impl in self.manager._loader_impls]
+ )
+
+ if self.callables:
+ # the per state loader callables we can remove here are
+ # LoadDeferredColumns, which undefers a column at the instance
+ # level that is mapped with deferred, and LoadLazyAttribute,
+ # which lazy loads a relationship at the instance level that
+ # is mapped with "noload" or perhaps "immediateload".
+ # Before 1.4, only column-based
+ # attributes could be considered to be "expired", so here they
+ # were the only ones "unexpired", which means to make them deferred
+ # again. For the moment, as of 1.4 we also apply the same
+ # treatment relationships now, that is, an instance level lazy
+ # loader is reset in the same way as a column loader.
+ for k in self.expired_attributes.intersection(self.callables):
+ del self.callables[k]
+
+ for k in self.manager._collection_impl_keys.intersection(dict_):
+ collection = dict_.pop(k)
+ collection._sa_adapter.invalidated = True
+
+ if self._last_known_values:
+ self._last_known_values.update(
+ {k: dict_[k] for k in self._last_known_values if k in dict_}
+ )
+
+ for key in self.manager._all_key_set.intersection(dict_):
+ del dict_[key]
+
+ self.manager.dispatch.expire(self, None)
+
+ def _expire_attributes(
+ self,
+ dict_: _InstanceDict,
+ attribute_names: Iterable[str],
+ no_loader: bool = False,
+ ) -> None:
+ pending = self.__dict__.get("_pending_mutations", None)
+
+ callables = self.callables
+
+ for key in attribute_names:
+ impl = self.manager[key].impl
+ if impl.accepts_scalar_loader:
+ if no_loader and (impl.callable_ or key in callables):
+ continue
+
+ self.expired_attributes.add(key)
+ if callables and key in callables:
+ del callables[key]
+ old = dict_.pop(key, NO_VALUE)
+ if is_collection_impl(impl) and old is not NO_VALUE:
+ impl._invalidate_collection(old)
+
+ lkv = self._last_known_values
+ if lkv is not None and key in lkv and old is not NO_VALUE:
+ lkv[key] = old
+
+ self.committed_state.pop(key, None)
+ if pending:
+ pending.pop(key, None)
+
+ self.manager.dispatch.expire(self, attribute_names)
+
+ def _load_expired(
+ self, state: InstanceState[_O], passive: PassiveFlag
+ ) -> LoaderCallableStatus:
+ """__call__ allows the InstanceState to act as a deferred
+ callable for loading expired attributes, which is also
+ serializable (picklable).
+
+ """
+
+ if not passive & SQL_OK:
+ return PASSIVE_NO_RESULT
+
+ toload = self.expired_attributes.intersection(self.unmodified)
+ toload = toload.difference(
+ attr
+ for attr in toload
+ if not self.manager[attr].impl.load_on_unexpire
+ )
+
+ self.manager.expired_attribute_loader(self, toload, passive)
+
+ # if the loader failed, or this
+ # instance state didn't have an identity,
+ # the attributes still might be in the callables
+ # dict. ensure they are removed.
+ self.expired_attributes.clear()
+
+ return ATTR_WAS_SET
+
+ @property
+ def unmodified(self) -> Set[str]:
+ """Return the set of keys which have no uncommitted changes"""
+
+ return set(self.manager).difference(self.committed_state)
+
+ def unmodified_intersection(self, keys: Iterable[str]) -> Set[str]:
+ """Return self.unmodified.intersection(keys)."""
+
+ return (
+ set(keys)
+ .intersection(self.manager)
+ .difference(self.committed_state)
+ )
+
+ @property
+ def unloaded(self) -> Set[str]:
+ """Return the set of keys which do not have a loaded value.
+
+ This includes expired attributes and any other attribute that was never
+ populated or modified.
+
+ """
+ return (
+ set(self.manager)
+ .difference(self.committed_state)
+ .difference(self.dict)
+ )
+
+ @property
+ @util.deprecated(
+ "2.0",
+ "The :attr:`.InstanceState.unloaded_expirable` attribute is "
+ "deprecated. Please use :attr:`.InstanceState.unloaded`.",
+ )
+ def unloaded_expirable(self) -> Set[str]:
+ """Synonymous with :attr:`.InstanceState.unloaded`.
+
+ This attribute was added as an implementation-specific detail at some
+ point and should be considered to be private.
+
+ """
+ return self.unloaded
+
+ @property
+ def _unloaded_non_object(self) -> Set[str]:
+ return self.unloaded.intersection(
+ attr
+ for attr in self.manager
+ if self.manager[attr].impl.accepts_scalar_loader
+ )
+
+ def _modified_event(
+ self,
+ dict_: _InstanceDict,
+ attr: Optional[AttributeImpl],
+ previous: Any,
+ collection: bool = False,
+ is_userland: bool = False,
+ ) -> None:
+ if attr:
+ if not attr.send_modified_events:
+ return
+ if is_userland and attr.key not in dict_:
+ raise sa_exc.InvalidRequestError(
+ "Can't flag attribute '%s' modified; it's not present in "
+ "the object state" % attr.key
+ )
+ if attr.key not in self.committed_state or is_userland:
+ if collection:
+ if TYPE_CHECKING:
+ assert is_collection_impl(attr)
+ if previous is NEVER_SET:
+ if attr.key in dict_:
+ previous = dict_[attr.key]
+
+ if previous not in (None, NO_VALUE, NEVER_SET):
+ previous = attr.copy(previous)
+ self.committed_state[attr.key] = previous
+
+ lkv = self._last_known_values
+ if lkv is not None and attr.key in lkv:
+ lkv[attr.key] = NO_VALUE
+
+ # assert self._strong_obj is None or self.modified
+
+ if (self.session_id and self._strong_obj is None) or not self.modified:
+ self.modified = True
+ instance_dict = self._instance_dict()
+ if instance_dict:
+ has_modified = bool(instance_dict._modified)
+ instance_dict._modified.add(self)
+ else:
+ has_modified = False
+
+ # only create _strong_obj link if attached
+ # to a session
+
+ inst = self.obj()
+ if self.session_id:
+ self._strong_obj = inst
+
+ # if identity map already had modified objects,
+ # assume autobegin already occurred, else check
+ # for autobegin
+ if not has_modified:
+ # inline of autobegin, to ensure session transaction
+ # snapshot is established
+ try:
+ session = _sessions[self.session_id]
+ except KeyError:
+ pass
+ else:
+ if session._transaction is None:
+ session._autobegin_t()
+
+ if inst is None and attr:
+ raise orm_exc.ObjectDereferencedError(
+ "Can't emit change event for attribute '%s' - "
+ "parent object of type %s has been garbage "
+ "collected."
+ % (self.manager[attr.key], base.state_class_str(self))
+ )
+
+ def _commit(self, dict_: _InstanceDict, keys: Iterable[str]) -> None:
+ """Commit attributes.
+
+ This is used by a partial-attribute load operation to mark committed
+ those attributes which were refreshed from the database.
+
+ Attributes marked as "expired" can potentially remain "expired" after
+ this step if a value was not populated in state.dict.
+
+ """
+ for key in keys:
+ self.committed_state.pop(key, None)
+
+ self.expired = False
+
+ self.expired_attributes.difference_update(
+ set(keys).intersection(dict_)
+ )
+
+ # the per-keys commit removes object-level callables,
+ # while that of commit_all does not. it's not clear
+ # if this behavior has a clear rationale, however tests do
+ # ensure this is what it does.
+ if self.callables:
+ for key in (
+ set(self.callables).intersection(keys).intersection(dict_)
+ ):
+ del self.callables[key]
+
+ def _commit_all(
+ self, dict_: _InstanceDict, instance_dict: Optional[IdentityMap] = None
+ ) -> None:
+ """commit all attributes unconditionally.
+
+ This is used after a flush() or a full load/refresh
+ to remove all pending state from the instance.
+
+ - all attributes are marked as "committed"
+ - the "strong dirty reference" is removed
+ - the "modified" flag is set to False
+ - any "expired" markers for scalar attributes loaded are removed.
+ - lazy load callables for objects / collections *stay*
+
+ Attributes marked as "expired" can potentially remain
+ "expired" after this step if a value was not populated in state.dict.
+
+ """
+ self._commit_all_states([(self, dict_)], instance_dict)
+
+ @classmethod
+ def _commit_all_states(
+ self,
+ iter_: Iterable[Tuple[InstanceState[Any], _InstanceDict]],
+ instance_dict: Optional[IdentityMap] = None,
+ ) -> None:
+ """Mass / highly inlined version of commit_all()."""
+
+ for state, dict_ in iter_:
+ state_dict = state.__dict__
+
+ state.committed_state.clear()
+
+ if "_pending_mutations" in state_dict:
+ del state_dict["_pending_mutations"]
+
+ state.expired_attributes.difference_update(dict_)
+
+ if instance_dict and state.modified:
+ instance_dict._modified.discard(state)
+
+ state.modified = state.expired = False
+ state._strong_obj = None
+
+
+class AttributeState:
+ """Provide an inspection interface corresponding
+ to a particular attribute on a particular mapped object.
+
+ The :class:`.AttributeState` object is accessed
+ via the :attr:`.InstanceState.attrs` collection
+ of a particular :class:`.InstanceState`::
+
+ from sqlalchemy import inspect
+
+ insp = inspect(some_mapped_object)
+ attr_state = insp.attrs.some_attribute
+
+ """
+
+ __slots__ = ("state", "key")
+
+ state: InstanceState[Any]
+ key: str
+
+ def __init__(self, state: InstanceState[Any], key: str):
+ self.state = state
+ self.key = key
+
+ @property
+ def loaded_value(self) -> Any:
+ """The current value of this attribute as loaded from the database.
+
+ If the value has not been loaded, or is otherwise not present
+ in the object's dictionary, returns NO_VALUE.
+
+ """
+ return self.state.dict.get(self.key, NO_VALUE)
+
+ @property
+ def value(self) -> Any:
+ """Return the value of this attribute.
+
+ This operation is equivalent to accessing the object's
+ attribute directly or via ``getattr()``, and will fire
+ off any pending loader callables if needed.
+
+ """
+ return self.state.manager[self.key].__get__(
+ self.state.obj(), self.state.class_
+ )
+
+ @property
+ def history(self) -> History:
+ """Return the current **pre-flush** change history for
+ this attribute, via the :class:`.History` interface.
+
+ This method will **not** emit loader callables if the value of the
+ attribute is unloaded.
+
+ .. note::
+
+ The attribute history system tracks changes on a **per flush
+ basis**. Each time the :class:`.Session` is flushed, the history
+ of each attribute is reset to empty. The :class:`.Session` by
+ default autoflushes each time a :class:`_query.Query` is invoked.
+ For
+ options on how to control this, see :ref:`session_flushing`.
+
+
+ .. seealso::
+
+ :meth:`.AttributeState.load_history` - retrieve history
+ using loader callables if the value is not locally present.
+
+ :func:`.attributes.get_history` - underlying function
+
+ """
+ return self.state.get_history(self.key, PASSIVE_NO_INITIALIZE)
+
+ def load_history(self) -> History:
+ """Return the current **pre-flush** change history for
+ this attribute, via the :class:`.History` interface.
+
+ This method **will** emit loader callables if the value of the
+ attribute is unloaded.
+
+ .. note::
+
+ The attribute history system tracks changes on a **per flush
+ basis**. Each time the :class:`.Session` is flushed, the history
+ of each attribute is reset to empty. The :class:`.Session` by
+ default autoflushes each time a :class:`_query.Query` is invoked.
+ For
+ options on how to control this, see :ref:`session_flushing`.
+
+ .. seealso::
+
+ :attr:`.AttributeState.history`
+
+ :func:`.attributes.get_history` - underlying function
+
+ """
+ return self.state.get_history(self.key, PASSIVE_OFF ^ INIT_OK)
+
+
+class PendingCollection:
+ """A writable placeholder for an unloaded collection.
+
+ Stores items appended to and removed from a collection that has not yet
+ been loaded. When the collection is loaded, the changes stored in
+ PendingCollection are applied to it to produce the final result.
+
+ """
+
+ __slots__ = ("deleted_items", "added_items")
+
+ deleted_items: util.IdentitySet
+ added_items: util.OrderedIdentitySet
+
+ def __init__(self) -> None:
+ self.deleted_items = util.IdentitySet()
+ self.added_items = util.OrderedIdentitySet()
+
+ def merge_with_history(self, history: History) -> History:
+ return history._merge(self.added_items, self.deleted_items)
+
+ def append(self, value: Any) -> None:
+ if value in self.deleted_items:
+ self.deleted_items.remove(value)
+ else:
+ self.added_items.add(value)
+
+ def remove(self, value: Any) -> None:
+ if value in self.added_items:
+ self.added_items.remove(value)
+ else:
+ self.deleted_items.add(value)
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py
new file mode 100644
index 0000000..56963c6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py
@@ -0,0 +1,198 @@
+# orm/state_changes.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""State tracking utilities used by :class:`_orm.Session`.
+
+"""
+
+from __future__ import annotations
+
+import contextlib
+from enum import Enum
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Iterator
+from typing import NoReturn
+from typing import Optional
+from typing import Tuple
+from typing import TypeVar
+from typing import Union
+
+from .. import exc as sa_exc
+from .. import util
+from ..util.typing import Literal
+
+_F = TypeVar("_F", bound=Callable[..., Any])
+
+
+class _StateChangeState(Enum):
+ pass
+
+
+class _StateChangeStates(_StateChangeState):
+ ANY = 1
+ NO_CHANGE = 2
+ CHANGE_IN_PROGRESS = 3
+
+
+class _StateChange:
+ """Supplies state assertion decorators.
+
+ The current use case is for the :class:`_orm.SessionTransaction` class. The
+ :class:`_StateChange` class itself is agnostic of the
+ :class:`_orm.SessionTransaction` class so could in theory be generalized
+ for other systems as well.
+
+ """
+
+ _next_state: _StateChangeState = _StateChangeStates.ANY
+ _state: _StateChangeState = _StateChangeStates.NO_CHANGE
+ _current_fn: Optional[Callable[..., Any]] = None
+
+ def _raise_for_prerequisite_state(
+ self, operation_name: str, state: _StateChangeState
+ ) -> NoReturn:
+ raise sa_exc.IllegalStateChangeError(
+ f"Can't run operation '{operation_name}()' when Session "
+ f"is in state {state!r}",
+ code="isce",
+ )
+
+ @classmethod
+ def declare_states(
+ cls,
+ prerequisite_states: Union[
+ Literal[_StateChangeStates.ANY], Tuple[_StateChangeState, ...]
+ ],
+ moves_to: _StateChangeState,
+ ) -> Callable[[_F], _F]:
+ """Method decorator declaring valid states.
+
+ :param prerequisite_states: sequence of acceptable prerequisite
+ states. Can be the single constant _State.ANY to indicate no
+ prerequisite state
+
+ :param moves_to: the expected state at the end of the method, assuming
+ no exceptions raised. Can be the constant _State.NO_CHANGE to
+ indicate state should not change at the end of the method.
+
+ """
+ assert prerequisite_states, "no prequisite states sent"
+ has_prerequisite_states = (
+ prerequisite_states is not _StateChangeStates.ANY
+ )
+
+ prerequisite_state_collection = cast(
+ "Tuple[_StateChangeState, ...]", prerequisite_states
+ )
+ expect_state_change = moves_to is not _StateChangeStates.NO_CHANGE
+
+ @util.decorator
+ def _go(fn: _F, self: Any, *arg: Any, **kw: Any) -> Any:
+ current_state = self._state
+
+ if (
+ has_prerequisite_states
+ and current_state not in prerequisite_state_collection
+ ):
+ self._raise_for_prerequisite_state(fn.__name__, current_state)
+
+ next_state = self._next_state
+ existing_fn = self._current_fn
+ expect_state = moves_to if expect_state_change else current_state
+
+ if (
+ # destination states are restricted
+ next_state is not _StateChangeStates.ANY
+ # method seeks to change state
+ and expect_state_change
+ # destination state incorrect
+ and next_state is not expect_state
+ ):
+ if existing_fn and next_state in (
+ _StateChangeStates.NO_CHANGE,
+ _StateChangeStates.CHANGE_IN_PROGRESS,
+ ):
+ raise sa_exc.IllegalStateChangeError(
+ f"Method '{fn.__name__}()' can't be called here; "
+ f"method '{existing_fn.__name__}()' is already "
+ f"in progress and this would cause an unexpected "
+ f"state change to {moves_to!r}",
+ code="isce",
+ )
+ else:
+ raise sa_exc.IllegalStateChangeError(
+ f"Cant run operation '{fn.__name__}()' here; "
+ f"will move to state {moves_to!r} where we are "
+ f"expecting {next_state!r}",
+ code="isce",
+ )
+
+ self._current_fn = fn
+ self._next_state = _StateChangeStates.CHANGE_IN_PROGRESS
+ try:
+ ret_value = fn(self, *arg, **kw)
+ except:
+ raise
+ else:
+ if self._state is expect_state:
+ return ret_value
+
+ if self._state is current_state:
+ raise sa_exc.IllegalStateChangeError(
+ f"Method '{fn.__name__}()' failed to "
+ "change state "
+ f"to {moves_to!r} as expected",
+ code="isce",
+ )
+ elif existing_fn:
+ raise sa_exc.IllegalStateChangeError(
+ f"While method '{existing_fn.__name__}()' was "
+ "running, "
+ f"method '{fn.__name__}()' caused an "
+ "unexpected "
+ f"state change to {self._state!r}",
+ code="isce",
+ )
+ else:
+ raise sa_exc.IllegalStateChangeError(
+ f"Method '{fn.__name__}()' caused an unexpected "
+ f"state change to {self._state!r}",
+ code="isce",
+ )
+
+ finally:
+ self._next_state = next_state
+ self._current_fn = existing_fn
+
+ return _go
+
+ @contextlib.contextmanager
+ def _expect_state(self, expected: _StateChangeState) -> Iterator[Any]:
+ """called within a method that changes states.
+
+ method must also use the ``@declare_states()`` decorator.
+
+ """
+ assert self._next_state is _StateChangeStates.CHANGE_IN_PROGRESS, (
+ "Unexpected call to _expect_state outside of "
+ "state-changing method"
+ )
+
+ self._next_state = expected
+ try:
+ yield
+ except:
+ raise
+ else:
+ if self._state is not expected:
+ raise sa_exc.IllegalStateChangeError(
+ f"Unexpected state change to {self._state!r}", code="isce"
+ )
+ finally:
+ self._next_state = _StateChangeStates.CHANGE_IN_PROGRESS
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/strategies.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/strategies.py
new file mode 100644
index 0000000..20c3b9c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/strategies.py
@@ -0,0 +1,3344 @@
+# orm/strategies.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: ignore-errors
+
+
+"""sqlalchemy.orm.interfaces.LoaderStrategy
+ implementations, and related MapperOptions."""
+
+from __future__ import annotations
+
+import collections
+import itertools
+from typing import Any
+from typing import Dict
+from typing import Tuple
+from typing import TYPE_CHECKING
+
+from . import attributes
+from . import exc as orm_exc
+from . import interfaces
+from . import loading
+from . import path_registry
+from . import properties
+from . import query
+from . import relationships
+from . import unitofwork
+from . import util as orm_util
+from .base import _DEFER_FOR_STATE
+from .base import _RAISE_FOR_STATE
+from .base import _SET_DEFERRED_EXPIRED
+from .base import ATTR_WAS_SET
+from .base import LoaderCallableStatus
+from .base import PASSIVE_OFF
+from .base import PassiveFlag
+from .context import _column_descriptions
+from .context import ORMCompileState
+from .context import ORMSelectCompileState
+from .context import QueryContext
+from .interfaces import LoaderStrategy
+from .interfaces import StrategizedProperty
+from .session import _state_session
+from .state import InstanceState
+from .strategy_options import Load
+from .util import _none_set
+from .util import AliasedClass
+from .. import event
+from .. import exc as sa_exc
+from .. import inspect
+from .. import log
+from .. import sql
+from .. import util
+from ..sql import util as sql_util
+from ..sql import visitors
+from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
+from ..sql.selectable import Select
+
+if TYPE_CHECKING:
+ from .relationships import RelationshipProperty
+ from ..sql.elements import ColumnElement
+
+
+def _register_attribute(
+ prop,
+ mapper,
+ useobject,
+ compare_function=None,
+ typecallable=None,
+ callable_=None,
+ proxy_property=None,
+ active_history=False,
+ impl_class=None,
+ **kw,
+):
+ listen_hooks = []
+
+ uselist = useobject and prop.uselist
+
+ if useobject and prop.single_parent:
+ listen_hooks.append(single_parent_validator)
+
+ if prop.key in prop.parent.validators:
+ fn, opts = prop.parent.validators[prop.key]
+ listen_hooks.append(
+ lambda desc, prop: orm_util._validator_events(
+ desc, prop.key, fn, **opts
+ )
+ )
+
+ if useobject:
+ listen_hooks.append(unitofwork.track_cascade_events)
+
+ # need to assemble backref listeners
+ # after the singleparentvalidator, mapper validator
+ if useobject:
+ backref = prop.back_populates
+ if backref and prop._effective_sync_backref:
+ listen_hooks.append(
+ lambda desc, prop: attributes.backref_listeners(
+ desc, backref, uselist
+ )
+ )
+
+ # a single MapperProperty is shared down a class inheritance
+ # hierarchy, so we set up attribute instrumentation and backref event
+ # for each mapper down the hierarchy.
+
+ # typically, "mapper" is the same as prop.parent, due to the way
+ # the configure_mappers() process runs, however this is not strongly
+ # enforced, and in the case of a second configure_mappers() run the
+ # mapper here might not be prop.parent; also, a subclass mapper may
+ # be called here before a superclass mapper. That is, can't depend
+ # on mappers not already being set up so we have to check each one.
+
+ for m in mapper.self_and_descendants:
+ if prop is m._props.get(
+ prop.key
+ ) and not m.class_manager._attr_has_impl(prop.key):
+ desc = attributes.register_attribute_impl(
+ m.class_,
+ prop.key,
+ parent_token=prop,
+ uselist=uselist,
+ compare_function=compare_function,
+ useobject=useobject,
+ trackparent=useobject
+ and (
+ prop.single_parent
+ or prop.direction is interfaces.ONETOMANY
+ ),
+ typecallable=typecallable,
+ callable_=callable_,
+ active_history=active_history,
+ impl_class=impl_class,
+ send_modified_events=not useobject or not prop.viewonly,
+ doc=prop.doc,
+ **kw,
+ )
+
+ for hook in listen_hooks:
+ hook(desc, prop)
+
+
+@properties.ColumnProperty.strategy_for(instrument=False, deferred=False)
+class UninstrumentedColumnLoader(LoaderStrategy):
+ """Represent a non-instrumented MapperProperty.
+
+ The polymorphic_on argument of mapper() often results in this,
+ if the argument is against the with_polymorphic selectable.
+
+ """
+
+ __slots__ = ("columns",)
+
+ def __init__(self, parent, strategy_key):
+ super().__init__(parent, strategy_key)
+ self.columns = self.parent_property.columns
+
+ def setup_query(
+ self,
+ compile_state,
+ query_entity,
+ path,
+ loadopt,
+ adapter,
+ column_collection=None,
+ **kwargs,
+ ):
+ for c in self.columns:
+ if adapter:
+ c = adapter.columns[c]
+ compile_state._append_dedupe_col_collection(c, column_collection)
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ pass
+
+
+@log.class_logger
+@properties.ColumnProperty.strategy_for(instrument=True, deferred=False)
+class ColumnLoader(LoaderStrategy):
+ """Provide loading behavior for a :class:`.ColumnProperty`."""
+
+ __slots__ = "columns", "is_composite"
+
+ def __init__(self, parent, strategy_key):
+ super().__init__(parent, strategy_key)
+ self.columns = self.parent_property.columns
+ self.is_composite = hasattr(self.parent_property, "composite_class")
+
+ def setup_query(
+ self,
+ compile_state,
+ query_entity,
+ path,
+ loadopt,
+ adapter,
+ column_collection,
+ memoized_populators,
+ check_for_adapt=False,
+ **kwargs,
+ ):
+ for c in self.columns:
+ if adapter:
+ if check_for_adapt:
+ c = adapter.adapt_check_present(c)
+ if c is None:
+ return
+ else:
+ c = adapter.columns[c]
+
+ compile_state._append_dedupe_col_collection(c, column_collection)
+
+ fetch = self.columns[0]
+ if adapter:
+ fetch = adapter.columns[fetch]
+ if fetch is None:
+ # None happens here only for dml bulk_persistence cases
+ # when context.DMLReturningColFilter is used
+ return
+
+ memoized_populators[self.parent_property] = fetch
+
+ def init_class_attribute(self, mapper):
+ self.is_class_level = True
+ coltype = self.columns[0].type
+ # TODO: check all columns ? check for foreign key as well?
+ active_history = (
+ self.parent_property.active_history
+ or self.columns[0].primary_key
+ or (
+ mapper.version_id_col is not None
+ and mapper._columntoproperty.get(mapper.version_id_col, None)
+ is self.parent_property
+ )
+ )
+
+ _register_attribute(
+ self.parent_property,
+ mapper,
+ useobject=False,
+ compare_function=coltype.compare_values,
+ active_history=active_history,
+ )
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ # look through list of columns represented here
+ # to see which, if any, is present in the row.
+
+ for col in self.columns:
+ if adapter:
+ col = adapter.columns[col]
+ getter = result._getter(col, False)
+ if getter:
+ populators["quick"].append((self.key, getter))
+ break
+ else:
+ populators["expire"].append((self.key, True))
+
+
+@log.class_logger
+@properties.ColumnProperty.strategy_for(query_expression=True)
+class ExpressionColumnLoader(ColumnLoader):
+ def __init__(self, parent, strategy_key):
+ super().__init__(parent, strategy_key)
+
+ # compare to the "default" expression that is mapped in
+ # the column. If it's sql.null, we don't need to render
+ # unless an expr is passed in the options.
+ null = sql.null().label(None)
+ self._have_default_expression = any(
+ not c.compare(null) for c in self.parent_property.columns
+ )
+
+ def setup_query(
+ self,
+ compile_state,
+ query_entity,
+ path,
+ loadopt,
+ adapter,
+ column_collection,
+ memoized_populators,
+ **kwargs,
+ ):
+ columns = None
+ if loadopt and loadopt._extra_criteria:
+ columns = loadopt._extra_criteria
+
+ elif self._have_default_expression:
+ columns = self.parent_property.columns
+
+ if columns is None:
+ return
+
+ for c in columns:
+ if adapter:
+ c = adapter.columns[c]
+ compile_state._append_dedupe_col_collection(c, column_collection)
+
+ fetch = columns[0]
+ if adapter:
+ fetch = adapter.columns[fetch]
+ if fetch is None:
+ # None is not expected to be the result of any
+ # adapter implementation here, however there may be theoretical
+ # usages of returning() with context.DMLReturningColFilter
+ return
+
+ memoized_populators[self.parent_property] = fetch
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ # look through list of columns represented here
+ # to see which, if any, is present in the row.
+ if loadopt and loadopt._extra_criteria:
+ columns = loadopt._extra_criteria
+
+ for col in columns:
+ if adapter:
+ col = adapter.columns[col]
+ getter = result._getter(col, False)
+ if getter:
+ populators["quick"].append((self.key, getter))
+ break
+ else:
+ populators["expire"].append((self.key, True))
+
+ def init_class_attribute(self, mapper):
+ self.is_class_level = True
+
+ _register_attribute(
+ self.parent_property,
+ mapper,
+ useobject=False,
+ compare_function=self.columns[0].type.compare_values,
+ accepts_scalar_loader=False,
+ )
+
+
+@log.class_logger
+@properties.ColumnProperty.strategy_for(deferred=True, instrument=True)
+@properties.ColumnProperty.strategy_for(
+ deferred=True, instrument=True, raiseload=True
+)
+@properties.ColumnProperty.strategy_for(do_nothing=True)
+class DeferredColumnLoader(LoaderStrategy):
+ """Provide loading behavior for a deferred :class:`.ColumnProperty`."""
+
+ __slots__ = "columns", "group", "raiseload"
+
+ def __init__(self, parent, strategy_key):
+ super().__init__(parent, strategy_key)
+ if hasattr(self.parent_property, "composite_class"):
+ raise NotImplementedError(
+ "Deferred loading for composite types not implemented yet"
+ )
+ self.raiseload = self.strategy_opts.get("raiseload", False)
+ self.columns = self.parent_property.columns
+ self.group = self.parent_property.group
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ # for a DeferredColumnLoader, this method is only used during a
+ # "row processor only" query; see test_deferred.py ->
+ # tests with "rowproc_only" in their name. As of the 1.0 series,
+ # loading._instance_processor doesn't use a "row processing" function
+ # to populate columns, instead it uses data in the "populators"
+ # dictionary. Normally, the DeferredColumnLoader.setup_query()
+ # sets up that data in the "memoized_populators" dictionary
+ # and "create_row_processor()" here is never invoked.
+
+ if (
+ context.refresh_state
+ and context.query._compile_options._only_load_props
+ and self.key in context.query._compile_options._only_load_props
+ ):
+ self.parent_property._get_strategy(
+ (("deferred", False), ("instrument", True))
+ ).create_row_processor(
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ )
+
+ elif not self.is_class_level:
+ if self.raiseload:
+ set_deferred_for_local_state = (
+ self.parent_property._raise_column_loader
+ )
+ else:
+ set_deferred_for_local_state = (
+ self.parent_property._deferred_column_loader
+ )
+ populators["new"].append((self.key, set_deferred_for_local_state))
+ else:
+ populators["expire"].append((self.key, False))
+
+ def init_class_attribute(self, mapper):
+ self.is_class_level = True
+
+ _register_attribute(
+ self.parent_property,
+ mapper,
+ useobject=False,
+ compare_function=self.columns[0].type.compare_values,
+ callable_=self._load_for_state,
+ load_on_unexpire=False,
+ )
+
+ def setup_query(
+ self,
+ compile_state,
+ query_entity,
+ path,
+ loadopt,
+ adapter,
+ column_collection,
+ memoized_populators,
+ only_load_props=None,
+ **kw,
+ ):
+ if (
+ (
+ compile_state.compile_options._render_for_subquery
+ and self.parent_property._renders_in_subqueries
+ )
+ or (
+ loadopt
+ and set(self.columns).intersection(
+ self.parent._should_undefer_in_wildcard
+ )
+ )
+ or (
+ loadopt
+ and self.group
+ and loadopt.local_opts.get(
+ "undefer_group_%s" % self.group, False
+ )
+ )
+ or (only_load_props and self.key in only_load_props)
+ ):
+ self.parent_property._get_strategy(
+ (("deferred", False), ("instrument", True))
+ ).setup_query(
+ compile_state,
+ query_entity,
+ path,
+ loadopt,
+ adapter,
+ column_collection,
+ memoized_populators,
+ **kw,
+ )
+ elif self.is_class_level:
+ memoized_populators[self.parent_property] = _SET_DEFERRED_EXPIRED
+ elif not self.raiseload:
+ memoized_populators[self.parent_property] = _DEFER_FOR_STATE
+ else:
+ memoized_populators[self.parent_property] = _RAISE_FOR_STATE
+
+ def _load_for_state(self, state, passive):
+ if not state.key:
+ return LoaderCallableStatus.ATTR_EMPTY
+
+ if not passive & PassiveFlag.SQL_OK:
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
+
+ localparent = state.manager.mapper
+
+ if self.group:
+ toload = [
+ p.key
+ for p in localparent.iterate_properties
+ if isinstance(p, StrategizedProperty)
+ and isinstance(p.strategy, DeferredColumnLoader)
+ and p.group == self.group
+ ]
+ else:
+ toload = [self.key]
+
+ # narrow the keys down to just those which have no history
+ group = [k for k in toload if k in state.unmodified]
+
+ session = _state_session(state)
+ if session is None:
+ raise orm_exc.DetachedInstanceError(
+ "Parent instance %s is not bound to a Session; "
+ "deferred load operation of attribute '%s' cannot proceed"
+ % (orm_util.state_str(state), self.key)
+ )
+
+ if self.raiseload:
+ self._invoke_raise_load(state, passive, "raise")
+
+ loading.load_scalar_attributes(
+ state.mapper, state, set(group), PASSIVE_OFF
+ )
+
+ return LoaderCallableStatus.ATTR_WAS_SET
+
+ def _invoke_raise_load(self, state, passive, lazy):
+ raise sa_exc.InvalidRequestError(
+ "'%s' is not available due to raiseload=True" % (self,)
+ )
+
+
+class LoadDeferredColumns:
+ """serializable loader object used by DeferredColumnLoader"""
+
+ def __init__(self, key: str, raiseload: bool = False):
+ self.key = key
+ self.raiseload = raiseload
+
+ def __call__(self, state, passive=attributes.PASSIVE_OFF):
+ key = self.key
+
+ localparent = state.manager.mapper
+ prop = localparent._props[key]
+ if self.raiseload:
+ strategy_key = (
+ ("deferred", True),
+ ("instrument", True),
+ ("raiseload", True),
+ )
+ else:
+ strategy_key = (("deferred", True), ("instrument", True))
+ strategy = prop._get_strategy(strategy_key)
+ return strategy._load_for_state(state, passive)
+
+
+class AbstractRelationshipLoader(LoaderStrategy):
+ """LoaderStratgies which deal with related objects."""
+
+ __slots__ = "mapper", "target", "uselist", "entity"
+
+ def __init__(self, parent, strategy_key):
+ super().__init__(parent, strategy_key)
+ self.mapper = self.parent_property.mapper
+ self.entity = self.parent_property.entity
+ self.target = self.parent_property.target
+ self.uselist = self.parent_property.uselist
+
+ def _immediateload_create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ return self.parent_property._get_strategy(
+ (("lazy", "immediate"),)
+ ).create_row_processor(
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ )
+
+
+@log.class_logger
+@relationships.RelationshipProperty.strategy_for(do_nothing=True)
+class DoNothingLoader(LoaderStrategy):
+ """Relationship loader that makes no change to the object's state.
+
+ Compared to NoLoader, this loader does not initialize the
+ collection/attribute to empty/none; the usual default LazyLoader will
+ take effect.
+
+ """
+
+
+@log.class_logger
+@relationships.RelationshipProperty.strategy_for(lazy="noload")
+@relationships.RelationshipProperty.strategy_for(lazy=None)
+class NoLoader(AbstractRelationshipLoader):
+ """Provide loading behavior for a :class:`.Relationship`
+ with "lazy=None".
+
+ """
+
+ __slots__ = ()
+
+ def init_class_attribute(self, mapper):
+ self.is_class_level = True
+
+ _register_attribute(
+ self.parent_property,
+ mapper,
+ useobject=True,
+ typecallable=self.parent_property.collection_class,
+ )
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ def invoke_no_load(state, dict_, row):
+ if self.uselist:
+ attributes.init_state_collection(state, dict_, self.key)
+ else:
+ dict_[self.key] = None
+
+ populators["new"].append((self.key, invoke_no_load))
+
+
+@log.class_logger
+@relationships.RelationshipProperty.strategy_for(lazy=True)
+@relationships.RelationshipProperty.strategy_for(lazy="select")
+@relationships.RelationshipProperty.strategy_for(lazy="raise")
+@relationships.RelationshipProperty.strategy_for(lazy="raise_on_sql")
+@relationships.RelationshipProperty.strategy_for(lazy="baked_select")
+class LazyLoader(
+ AbstractRelationshipLoader, util.MemoizedSlots, log.Identified
+):
+ """Provide loading behavior for a :class:`.Relationship`
+ with "lazy=True", that is loads when first accessed.
+
+ """
+
+ __slots__ = (
+ "_lazywhere",
+ "_rev_lazywhere",
+ "_lazyload_reverse_option",
+ "_order_by",
+ "use_get",
+ "is_aliased_class",
+ "_bind_to_col",
+ "_equated_columns",
+ "_rev_bind_to_col",
+ "_rev_equated_columns",
+ "_simple_lazy_clause",
+ "_raise_always",
+ "_raise_on_sql",
+ )
+
+ _lazywhere: ColumnElement[bool]
+ _bind_to_col: Dict[str, ColumnElement[Any]]
+ _rev_lazywhere: ColumnElement[bool]
+ _rev_bind_to_col: Dict[str, ColumnElement[Any]]
+
+ parent_property: RelationshipProperty[Any]
+
+ def __init__(
+ self, parent: RelationshipProperty[Any], strategy_key: Tuple[Any, ...]
+ ):
+ super().__init__(parent, strategy_key)
+ self._raise_always = self.strategy_opts["lazy"] == "raise"
+ self._raise_on_sql = self.strategy_opts["lazy"] == "raise_on_sql"
+
+ self.is_aliased_class = inspect(self.entity).is_aliased_class
+
+ join_condition = self.parent_property._join_condition
+ (
+ self._lazywhere,
+ self._bind_to_col,
+ self._equated_columns,
+ ) = join_condition.create_lazy_clause()
+
+ (
+ self._rev_lazywhere,
+ self._rev_bind_to_col,
+ self._rev_equated_columns,
+ ) = join_condition.create_lazy_clause(reverse_direction=True)
+
+ if self.parent_property.order_by:
+ self._order_by = [
+ sql_util._deep_annotate(elem, {"_orm_adapt": True})
+ for elem in util.to_list(self.parent_property.order_by)
+ ]
+ else:
+ self._order_by = None
+
+ self.logger.info("%s lazy loading clause %s", self, self._lazywhere)
+
+ # determine if our "lazywhere" clause is the same as the mapper's
+ # get() clause. then we can just use mapper.get()
+ #
+ # TODO: the "not self.uselist" can be taken out entirely; a m2o
+ # load that populates for a list (very unusual, but is possible with
+ # the API) can still set for "None" and the attribute system will
+ # populate as an empty list.
+ self.use_get = (
+ not self.is_aliased_class
+ and not self.uselist
+ and self.entity._get_clause[0].compare(
+ self._lazywhere,
+ use_proxies=True,
+ compare_keys=False,
+ equivalents=self.mapper._equivalent_columns,
+ )
+ )
+
+ if self.use_get:
+ for col in list(self._equated_columns):
+ if col in self.mapper._equivalent_columns:
+ for c in self.mapper._equivalent_columns[col]:
+ self._equated_columns[c] = self._equated_columns[col]
+
+ self.logger.info(
+ "%s will use Session.get() to optimize instance loads", self
+ )
+
+ def init_class_attribute(self, mapper):
+ self.is_class_level = True
+
+ _legacy_inactive_history_style = (
+ self.parent_property._legacy_inactive_history_style
+ )
+
+ if self.parent_property.active_history:
+ active_history = True
+ _deferred_history = False
+
+ elif (
+ self.parent_property.direction is not interfaces.MANYTOONE
+ or not self.use_get
+ ):
+ if _legacy_inactive_history_style:
+ active_history = True
+ _deferred_history = False
+ else:
+ active_history = False
+ _deferred_history = True
+ else:
+ active_history = _deferred_history = False
+
+ _register_attribute(
+ self.parent_property,
+ mapper,
+ useobject=True,
+ callable_=self._load_for_state,
+ typecallable=self.parent_property.collection_class,
+ active_history=active_history,
+ _deferred_history=_deferred_history,
+ )
+
+ def _memoized_attr__simple_lazy_clause(self):
+ lazywhere = sql_util._deep_annotate(
+ self._lazywhere, {"_orm_adapt": True}
+ )
+
+ criterion, bind_to_col = (lazywhere, self._bind_to_col)
+
+ params = []
+
+ def visit_bindparam(bindparam):
+ bindparam.unique = False
+
+ visitors.traverse(criterion, {}, {"bindparam": visit_bindparam})
+
+ def visit_bindparam(bindparam):
+ if bindparam._identifying_key in bind_to_col:
+ params.append(
+ (
+ bindparam.key,
+ bind_to_col[bindparam._identifying_key],
+ None,
+ )
+ )
+ elif bindparam.callable is None:
+ params.append((bindparam.key, None, bindparam.value))
+
+ criterion = visitors.cloned_traverse(
+ criterion, {}, {"bindparam": visit_bindparam}
+ )
+
+ return criterion, params
+
+ def _generate_lazy_clause(self, state, passive):
+ criterion, param_keys = self._simple_lazy_clause
+
+ if state is None:
+ return sql_util.adapt_criterion_to_null(
+ criterion, [key for key, ident, value in param_keys]
+ )
+
+ mapper = self.parent_property.parent
+
+ o = state.obj() # strong ref
+ dict_ = attributes.instance_dict(o)
+
+ if passive & PassiveFlag.INIT_OK:
+ passive ^= PassiveFlag.INIT_OK
+
+ params = {}
+ for key, ident, value in param_keys:
+ if ident is not None:
+ if passive and passive & PassiveFlag.LOAD_AGAINST_COMMITTED:
+ value = mapper._get_committed_state_attr_by_column(
+ state, dict_, ident, passive
+ )
+ else:
+ value = mapper._get_state_attr_by_column(
+ state, dict_, ident, passive
+ )
+
+ params[key] = value
+
+ return criterion, params
+
+ def _invoke_raise_load(self, state, passive, lazy):
+ raise sa_exc.InvalidRequestError(
+ "'%s' is not available due to lazy='%s'" % (self, lazy)
+ )
+
+ def _load_for_state(
+ self,
+ state,
+ passive,
+ loadopt=None,
+ extra_criteria=(),
+ extra_options=(),
+ alternate_effective_path=None,
+ execution_options=util.EMPTY_DICT,
+ ):
+ if not state.key and (
+ (
+ not self.parent_property.load_on_pending
+ and not state._load_pending
+ )
+ or not state.session_id
+ ):
+ return LoaderCallableStatus.ATTR_EMPTY
+
+ pending = not state.key
+ primary_key_identity = None
+
+ use_get = self.use_get and (not loadopt or not loadopt._extra_criteria)
+
+ if (not passive & PassiveFlag.SQL_OK and not use_get) or (
+ not passive & attributes.NON_PERSISTENT_OK and pending
+ ):
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
+
+ if (
+ # we were given lazy="raise"
+ self._raise_always
+ # the no_raise history-related flag was not passed
+ and not passive & PassiveFlag.NO_RAISE
+ and (
+ # if we are use_get and related_object_ok is disabled,
+ # which means we are at most looking in the identity map
+ # for history purposes or otherwise returning
+ # PASSIVE_NO_RESULT, don't raise. This is also a
+ # history-related flag
+ not use_get
+ or passive & PassiveFlag.RELATED_OBJECT_OK
+ )
+ ):
+ self._invoke_raise_load(state, passive, "raise")
+
+ session = _state_session(state)
+ if not session:
+ if passive & PassiveFlag.NO_RAISE:
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
+
+ raise orm_exc.DetachedInstanceError(
+ "Parent instance %s is not bound to a Session; "
+ "lazy load operation of attribute '%s' cannot proceed"
+ % (orm_util.state_str(state), self.key)
+ )
+
+ # if we have a simple primary key load, check the
+ # identity map without generating a Query at all
+ if use_get:
+ primary_key_identity = self._get_ident_for_use_get(
+ session, state, passive
+ )
+ if LoaderCallableStatus.PASSIVE_NO_RESULT in primary_key_identity:
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
+ elif LoaderCallableStatus.NEVER_SET in primary_key_identity:
+ return LoaderCallableStatus.NEVER_SET
+
+ if _none_set.issuperset(primary_key_identity):
+ return None
+
+ if (
+ self.key in state.dict
+ and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD
+ ):
+ return LoaderCallableStatus.ATTR_WAS_SET
+
+ # look for this identity in the identity map. Delegate to the
+ # Query class in use, as it may have special rules for how it
+ # does this, including how it decides what the correct
+ # identity_token would be for this identity.
+
+ instance = session._identity_lookup(
+ self.entity,
+ primary_key_identity,
+ passive=passive,
+ lazy_loaded_from=state,
+ )
+
+ if instance is not None:
+ if instance is LoaderCallableStatus.PASSIVE_CLASS_MISMATCH:
+ return None
+ else:
+ return instance
+ elif (
+ not passive & PassiveFlag.SQL_OK
+ or not passive & PassiveFlag.RELATED_OBJECT_OK
+ ):
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
+
+ return self._emit_lazyload(
+ session,
+ state,
+ primary_key_identity,
+ passive,
+ loadopt,
+ extra_criteria,
+ extra_options,
+ alternate_effective_path,
+ execution_options,
+ )
+
+ def _get_ident_for_use_get(self, session, state, passive):
+ instance_mapper = state.manager.mapper
+
+ if passive & PassiveFlag.LOAD_AGAINST_COMMITTED:
+ get_attr = instance_mapper._get_committed_state_attr_by_column
+ else:
+ get_attr = instance_mapper._get_state_attr_by_column
+
+ dict_ = state.dict
+
+ return [
+ get_attr(state, dict_, self._equated_columns[pk], passive=passive)
+ for pk in self.mapper.primary_key
+ ]
+
+ @util.preload_module("sqlalchemy.orm.strategy_options")
+ def _emit_lazyload(
+ self,
+ session,
+ state,
+ primary_key_identity,
+ passive,
+ loadopt,
+ extra_criteria,
+ extra_options,
+ alternate_effective_path,
+ execution_options,
+ ):
+ strategy_options = util.preloaded.orm_strategy_options
+
+ clauseelement = self.entity.__clause_element__()
+ stmt = Select._create_raw_select(
+ _raw_columns=[clauseelement],
+ _propagate_attrs=clauseelement._propagate_attrs,
+ _label_style=LABEL_STYLE_TABLENAME_PLUS_COL,
+ _compile_options=ORMCompileState.default_compile_options,
+ )
+ load_options = QueryContext.default_load_options
+
+ load_options += {
+ "_invoke_all_eagers": False,
+ "_lazy_loaded_from": state,
+ }
+
+ if self.parent_property.secondary is not None:
+ stmt = stmt.select_from(
+ self.mapper, self.parent_property.secondary
+ )
+
+ pending = not state.key
+
+ # don't autoflush on pending
+ if pending or passive & attributes.NO_AUTOFLUSH:
+ stmt._execution_options = util.immutabledict({"autoflush": False})
+
+ use_get = self.use_get
+
+ if state.load_options or (loadopt and loadopt._extra_criteria):
+ if alternate_effective_path is None:
+ effective_path = state.load_path[self.parent_property]
+ else:
+ effective_path = alternate_effective_path[self.parent_property]
+
+ opts = state.load_options
+
+ if loadopt and loadopt._extra_criteria:
+ use_get = False
+ opts += (
+ orm_util.LoaderCriteriaOption(self.entity, extra_criteria),
+ )
+
+ stmt._with_options = opts
+ elif alternate_effective_path is None:
+ # this path is used if there are not already any options
+ # in the query, but an event may want to add them
+ effective_path = state.mapper._path_registry[self.parent_property]
+ else:
+ # added by immediateloader
+ effective_path = alternate_effective_path[self.parent_property]
+
+ if extra_options:
+ stmt._with_options += extra_options
+
+ stmt._compile_options += {"_current_path": effective_path}
+
+ if use_get:
+ if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE:
+ self._invoke_raise_load(state, passive, "raise_on_sql")
+
+ return loading.load_on_pk_identity(
+ session,
+ stmt,
+ primary_key_identity,
+ load_options=load_options,
+ execution_options=execution_options,
+ )
+
+ if self._order_by:
+ stmt._order_by_clauses = self._order_by
+
+ def _lazyload_reverse(compile_context):
+ for rev in self.parent_property._reverse_property:
+ # reverse props that are MANYTOONE are loading *this*
+ # object from get(), so don't need to eager out to those.
+ if (
+ rev.direction is interfaces.MANYTOONE
+ and rev._use_get
+ and not isinstance(rev.strategy, LazyLoader)
+ ):
+ strategy_options.Load._construct_for_existing_path(
+ compile_context.compile_options._current_path[
+ rev.parent
+ ]
+ ).lazyload(rev).process_compile_state(compile_context)
+
+ stmt._with_context_options += (
+ (_lazyload_reverse, self.parent_property),
+ )
+
+ lazy_clause, params = self._generate_lazy_clause(state, passive)
+
+ if execution_options:
+ execution_options = util.EMPTY_DICT.merge_with(
+ execution_options,
+ {
+ "_sa_orm_load_options": load_options,
+ },
+ )
+ else:
+ execution_options = {
+ "_sa_orm_load_options": load_options,
+ }
+
+ if (
+ self.key in state.dict
+ and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD
+ ):
+ return LoaderCallableStatus.ATTR_WAS_SET
+
+ if pending:
+ if util.has_intersection(orm_util._none_set, params.values()):
+ return None
+
+ elif util.has_intersection(orm_util._never_set, params.values()):
+ return None
+
+ if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE:
+ self._invoke_raise_load(state, passive, "raise_on_sql")
+
+ stmt._where_criteria = (lazy_clause,)
+
+ result = session.execute(
+ stmt, params, execution_options=execution_options
+ )
+
+ result = result.unique().scalars().all()
+
+ if self.uselist:
+ return result
+ else:
+ l = len(result)
+ if l:
+ if l > 1:
+ util.warn(
+ "Multiple rows returned with "
+ "uselist=False for lazily-loaded attribute '%s' "
+ % self.parent_property
+ )
+
+ return result[0]
+ else:
+ return None
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ key = self.key
+
+ if (
+ context.load_options._is_user_refresh
+ and context.query._compile_options._only_load_props
+ and self.key in context.query._compile_options._only_load_props
+ ):
+ return self._immediateload_create_row_processor(
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ )
+
+ if not self.is_class_level or (loadopt and loadopt._extra_criteria):
+ # we are not the primary manager for this attribute
+ # on this class - set up a
+ # per-instance lazyloader, which will override the
+ # class-level behavior.
+ # this currently only happens when using a
+ # "lazyload" option on a "no load"
+ # attribute - "eager" attributes always have a
+ # class-level lazyloader installed.
+ set_lazy_callable = (
+ InstanceState._instance_level_callable_processor
+ )(
+ mapper.class_manager,
+ LoadLazyAttribute(
+ key,
+ self,
+ loadopt,
+ (
+ loadopt._generate_extra_criteria(context)
+ if loadopt._extra_criteria
+ else None
+ ),
+ ),
+ key,
+ )
+
+ populators["new"].append((self.key, set_lazy_callable))
+ elif context.populate_existing or mapper.always_refresh:
+
+ def reset_for_lazy_callable(state, dict_, row):
+ # we are the primary manager for this attribute on
+ # this class - reset its
+ # per-instance attribute state, so that the class-level
+ # lazy loader is
+ # executed when next referenced on this instance.
+ # this is needed in
+ # populate_existing() types of scenarios to reset
+ # any existing state.
+ state._reset(dict_, key)
+
+ populators["new"].append((self.key, reset_for_lazy_callable))
+
+
+class LoadLazyAttribute:
+ """semi-serializable loader object used by LazyLoader
+
+ Historically, this object would be carried along with instances that
+ needed to run lazyloaders, so it had to be serializable to support
+ cached instances.
+
+ this is no longer a general requirement, and the case where this object
+ is used is exactly the case where we can't really serialize easily,
+ which is when extra criteria in the loader option is present.
+
+ We can't reliably serialize that as it refers to mapped entities and
+ AliasedClass objects that are local to the current process, which would
+ need to be matched up on deserialize e.g. the sqlalchemy.ext.serializer
+ approach.
+
+ """
+
+ def __init__(self, key, initiating_strategy, loadopt, extra_criteria):
+ self.key = key
+ self.strategy_key = initiating_strategy.strategy_key
+ self.loadopt = loadopt
+ self.extra_criteria = extra_criteria
+
+ def __getstate__(self):
+ if self.extra_criteria is not None:
+ util.warn(
+ "Can't reliably serialize a lazyload() option that "
+ "contains additional criteria; please use eager loading "
+ "for this case"
+ )
+ return {
+ "key": self.key,
+ "strategy_key": self.strategy_key,
+ "loadopt": self.loadopt,
+ "extra_criteria": (),
+ }
+
+ def __call__(self, state, passive=attributes.PASSIVE_OFF):
+ key = self.key
+ instance_mapper = state.manager.mapper
+ prop = instance_mapper._props[key]
+ strategy = prop._strategies[self.strategy_key]
+
+ return strategy._load_for_state(
+ state,
+ passive,
+ loadopt=self.loadopt,
+ extra_criteria=self.extra_criteria,
+ )
+
+
+class PostLoader(AbstractRelationshipLoader):
+ """A relationship loader that emits a second SELECT statement."""
+
+ __slots__ = ()
+
+ def _setup_for_recursion(self, context, path, loadopt, join_depth=None):
+ effective_path = (
+ context.compile_state.current_path or orm_util.PathRegistry.root
+ ) + path
+
+ top_level_context = context._get_top_level_context()
+ execution_options = util.immutabledict(
+ {"sa_top_level_orm_context": top_level_context}
+ )
+
+ if loadopt:
+ recursion_depth = loadopt.local_opts.get("recursion_depth", None)
+ unlimited_recursion = recursion_depth == -1
+ else:
+ recursion_depth = None
+ unlimited_recursion = False
+
+ if recursion_depth is not None:
+ if not self.parent_property._is_self_referential:
+ raise sa_exc.InvalidRequestError(
+ f"recursion_depth option on relationship "
+ f"{self.parent_property} not valid for "
+ "non-self-referential relationship"
+ )
+ recursion_depth = context.execution_options.get(
+ f"_recursion_depth_{id(self)}", recursion_depth
+ )
+
+ if not unlimited_recursion and recursion_depth < 0:
+ return (
+ effective_path,
+ False,
+ execution_options,
+ recursion_depth,
+ )
+
+ if not unlimited_recursion:
+ execution_options = execution_options.union(
+ {
+ f"_recursion_depth_{id(self)}": recursion_depth - 1,
+ }
+ )
+
+ if loading.PostLoad.path_exists(
+ context, effective_path, self.parent_property
+ ):
+ return effective_path, False, execution_options, recursion_depth
+
+ path_w_prop = path[self.parent_property]
+ effective_path_w_prop = effective_path[self.parent_property]
+
+ if not path_w_prop.contains(context.attributes, "loader"):
+ if join_depth:
+ if effective_path_w_prop.length / 2 > join_depth:
+ return (
+ effective_path,
+ False,
+ execution_options,
+ recursion_depth,
+ )
+ elif effective_path_w_prop.contains_mapper(self.mapper):
+ return (
+ effective_path,
+ False,
+ execution_options,
+ recursion_depth,
+ )
+
+ return effective_path, True, execution_options, recursion_depth
+
+
+@relationships.RelationshipProperty.strategy_for(lazy="immediate")
+class ImmediateLoader(PostLoader):
+ __slots__ = ("join_depth",)
+
+ def __init__(self, parent, strategy_key):
+ super().__init__(parent, strategy_key)
+ self.join_depth = self.parent_property.join_depth
+
+ def init_class_attribute(self, mapper):
+ self.parent_property._get_strategy(
+ (("lazy", "select"),)
+ ).init_class_attribute(mapper)
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ (
+ effective_path,
+ run_loader,
+ execution_options,
+ recursion_depth,
+ ) = self._setup_for_recursion(context, path, loadopt, self.join_depth)
+ if not run_loader:
+ # this will not emit SQL and will only emit for a many-to-one
+ # "use get" load. the "_RELATED" part means it may return
+ # instance even if its expired, since this is a mutually-recursive
+ # load operation.
+ flags = attributes.PASSIVE_NO_FETCH_RELATED | PassiveFlag.NO_RAISE
+ else:
+ flags = attributes.PASSIVE_OFF | PassiveFlag.NO_RAISE
+
+ loading.PostLoad.callable_for_path(
+ context,
+ effective_path,
+ self.parent,
+ self.parent_property,
+ self._load_for_path,
+ loadopt,
+ flags,
+ recursion_depth,
+ execution_options,
+ )
+
+ def _load_for_path(
+ self,
+ context,
+ path,
+ states,
+ load_only,
+ loadopt,
+ flags,
+ recursion_depth,
+ execution_options,
+ ):
+ if recursion_depth:
+ new_opt = Load(loadopt.path.entity)
+ new_opt.context = (
+ loadopt,
+ loadopt._recurse(),
+ )
+ alternate_effective_path = path._truncate_recursive()
+ extra_options = (new_opt,)
+ else:
+ new_opt = None
+ alternate_effective_path = path
+ extra_options = ()
+
+ key = self.key
+ lazyloader = self.parent_property._get_strategy((("lazy", "select"),))
+ for state, overwrite in states:
+ dict_ = state.dict
+
+ if overwrite or key not in dict_:
+ value = lazyloader._load_for_state(
+ state,
+ flags,
+ extra_options=extra_options,
+ alternate_effective_path=alternate_effective_path,
+ execution_options=execution_options,
+ )
+ if value not in (
+ ATTR_WAS_SET,
+ LoaderCallableStatus.PASSIVE_NO_RESULT,
+ ):
+ state.get_impl(key).set_committed_value(
+ state, dict_, value
+ )
+
+
+@log.class_logger
+@relationships.RelationshipProperty.strategy_for(lazy="subquery")
+class SubqueryLoader(PostLoader):
+ __slots__ = ("join_depth",)
+
+ def __init__(self, parent, strategy_key):
+ super().__init__(parent, strategy_key)
+ self.join_depth = self.parent_property.join_depth
+
+ def init_class_attribute(self, mapper):
+ self.parent_property._get_strategy(
+ (("lazy", "select"),)
+ ).init_class_attribute(mapper)
+
+ def _get_leftmost(
+ self,
+ orig_query_entity_index,
+ subq_path,
+ current_compile_state,
+ is_root,
+ ):
+ given_subq_path = subq_path
+ subq_path = subq_path.path
+ subq_mapper = orm_util._class_to_mapper(subq_path[0])
+
+ # determine attributes of the leftmost mapper
+ if (
+ self.parent.isa(subq_mapper)
+ and self.parent_property is subq_path[1]
+ ):
+ leftmost_mapper, leftmost_prop = self.parent, self.parent_property
+ else:
+ leftmost_mapper, leftmost_prop = subq_mapper, subq_path[1]
+
+ if is_root:
+ # the subq_path is also coming from cached state, so when we start
+ # building up this path, it has to also be converted to be in terms
+ # of the current state. this is for the specific case of the entity
+ # is an AliasedClass against a subquery that's not otherwise going
+ # to adapt
+ new_subq_path = current_compile_state._entities[
+ orig_query_entity_index
+ ].entity_zero._path_registry[leftmost_prop]
+ additional = len(subq_path) - len(new_subq_path)
+ if additional:
+ new_subq_path += path_registry.PathRegistry.coerce(
+ subq_path[-additional:]
+ )
+ else:
+ new_subq_path = given_subq_path
+
+ leftmost_cols = leftmost_prop.local_columns
+
+ leftmost_attr = [
+ getattr(
+ new_subq_path.path[0].entity,
+ leftmost_mapper._columntoproperty[c].key,
+ )
+ for c in leftmost_cols
+ ]
+
+ return leftmost_mapper, leftmost_attr, leftmost_prop, new_subq_path
+
+ def _generate_from_original_query(
+ self,
+ orig_compile_state,
+ orig_query,
+ leftmost_mapper,
+ leftmost_attr,
+ leftmost_relationship,
+ orig_entity,
+ ):
+ # reformat the original query
+ # to look only for significant columns
+ q = orig_query._clone().correlate(None)
+
+ # LEGACY: make a Query back from the select() !!
+ # This suits at least two legacy cases:
+ # 1. applications which expect before_compile() to be called
+ # below when we run .subquery() on this query (Keystone)
+ # 2. applications which are doing subqueryload with complex
+ # from_self() queries, as query.subquery() / .statement
+ # has to do the full compile context for multiply-nested
+ # from_self() (Neutron) - see test_subqload_from_self
+ # for demo.
+ q2 = query.Query.__new__(query.Query)
+ q2.__dict__.update(q.__dict__)
+ q = q2
+
+ # set the query's "FROM" list explicitly to what the
+ # FROM list would be in any case, as we will be limiting
+ # the columns in the SELECT list which may no longer include
+ # all entities mentioned in things like WHERE, JOIN, etc.
+ if not q._from_obj:
+ q._enable_assertions = False
+ q.select_from.non_generative(
+ q,
+ *{
+ ent["entity"]
+ for ent in _column_descriptions(
+ orig_query, compile_state=orig_compile_state
+ )
+ if ent["entity"] is not None
+ },
+ )
+
+ # select from the identity columns of the outer (specifically, these
+ # are the 'local_cols' of the property). This will remove other
+ # columns from the query that might suggest the right entity which is
+ # why we do set select_from above. The attributes we have are
+ # coerced and adapted using the original query's adapter, which is
+ # needed only for the case of adapting a subclass column to
+ # that of a polymorphic selectable, e.g. we have
+ # Engineer.primary_language and the entity is Person. All other
+ # adaptations, e.g. from_self, select_entity_from(), will occur
+ # within the new query when it compiles, as the compile_state we are
+ # using here is only a partial one. If the subqueryload is from a
+ # with_polymorphic() or other aliased() object, left_attr will already
+ # be the correct attributes so no adaptation is needed.
+ target_cols = orig_compile_state._adapt_col_list(
+ [
+ sql.coercions.expect(sql.roles.ColumnsClauseRole, o)
+ for o in leftmost_attr
+ ],
+ orig_compile_state._get_current_adapter(),
+ )
+ q._raw_columns = target_cols
+
+ distinct_target_key = leftmost_relationship.distinct_target_key
+
+ if distinct_target_key is True:
+ q._distinct = True
+ elif distinct_target_key is None:
+ # if target_cols refer to a non-primary key or only
+ # part of a composite primary key, set the q as distinct
+ for t in {c.table for c in target_cols}:
+ if not set(target_cols).issuperset(t.primary_key):
+ q._distinct = True
+ break
+
+ # don't need ORDER BY if no limit/offset
+ if not q._has_row_limiting_clause:
+ q._order_by_clauses = ()
+
+ if q._distinct is True and q._order_by_clauses:
+ # the logic to automatically add the order by columns to the query
+ # when distinct is True is deprecated in the query
+ to_add = sql_util.expand_column_list_from_order_by(
+ target_cols, q._order_by_clauses
+ )
+ if to_add:
+ q._set_entities(target_cols + to_add)
+
+ # the original query now becomes a subquery
+ # which we'll join onto.
+ # LEGACY: as "q" is a Query, the before_compile() event is invoked
+ # here.
+ embed_q = q.set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL).subquery()
+ left_alias = orm_util.AliasedClass(
+ leftmost_mapper, embed_q, use_mapper_path=True
+ )
+ return left_alias
+
+ def _prep_for_joins(self, left_alias, subq_path):
+ # figure out what's being joined. a.k.a. the fun part
+ to_join = []
+ pairs = list(subq_path.pairs())
+
+ for i, (mapper, prop) in enumerate(pairs):
+ if i > 0:
+ # look at the previous mapper in the chain -
+ # if it is as or more specific than this prop's
+ # mapper, use that instead.
+ # note we have an assumption here that
+ # the non-first element is always going to be a mapper,
+ # not an AliasedClass
+
+ prev_mapper = pairs[i - 1][1].mapper
+ to_append = prev_mapper if prev_mapper.isa(mapper) else mapper
+ else:
+ to_append = mapper
+
+ to_join.append((to_append, prop.key))
+
+ # determine the immediate parent class we are joining from,
+ # which needs to be aliased.
+
+ if len(to_join) < 2:
+ # in the case of a one level eager load, this is the
+ # leftmost "left_alias".
+ parent_alias = left_alias
+ else:
+ info = inspect(to_join[-1][0])
+ if info.is_aliased_class:
+ parent_alias = info.entity
+ else:
+ # alias a plain mapper as we may be
+ # joining multiple times
+ parent_alias = orm_util.AliasedClass(
+ info.entity, use_mapper_path=True
+ )
+
+ local_cols = self.parent_property.local_columns
+
+ local_attr = [
+ getattr(parent_alias, self.parent._columntoproperty[c].key)
+ for c in local_cols
+ ]
+ return to_join, local_attr, parent_alias
+
+ def _apply_joins(
+ self, q, to_join, left_alias, parent_alias, effective_entity
+ ):
+ ltj = len(to_join)
+ if ltj == 1:
+ to_join = [
+ getattr(left_alias, to_join[0][1]).of_type(effective_entity)
+ ]
+ elif ltj == 2:
+ to_join = [
+ getattr(left_alias, to_join[0][1]).of_type(parent_alias),
+ getattr(parent_alias, to_join[-1][1]).of_type(
+ effective_entity
+ ),
+ ]
+ elif ltj > 2:
+ middle = [
+ (
+ (
+ orm_util.AliasedClass(item[0])
+ if not inspect(item[0]).is_aliased_class
+ else item[0].entity
+ ),
+ item[1],
+ )
+ for item in to_join[1:-1]
+ ]
+ inner = []
+
+ while middle:
+ item = middle.pop(0)
+ attr = getattr(item[0], item[1])
+ if middle:
+ attr = attr.of_type(middle[0][0])
+ else:
+ attr = attr.of_type(parent_alias)
+
+ inner.append(attr)
+
+ to_join = (
+ [getattr(left_alias, to_join[0][1]).of_type(inner[0].parent)]
+ + inner
+ + [
+ getattr(parent_alias, to_join[-1][1]).of_type(
+ effective_entity
+ )
+ ]
+ )
+
+ for attr in to_join:
+ q = q.join(attr)
+
+ return q
+
+ def _setup_options(
+ self,
+ context,
+ q,
+ subq_path,
+ rewritten_path,
+ orig_query,
+ effective_entity,
+ loadopt,
+ ):
+ # note that because the subqueryload object
+ # does not re-use the cached query, instead always making
+ # use of the current invoked query, while we have two queries
+ # here (orig and context.query), they are both non-cached
+ # queries and we can transfer the options as is without
+ # adjusting for new criteria. Some work on #6881 / #6889
+ # brought this into question.
+ new_options = orig_query._with_options
+
+ if loadopt and loadopt._extra_criteria:
+ new_options += (
+ orm_util.LoaderCriteriaOption(
+ self.entity,
+ loadopt._generate_extra_criteria(context),
+ ),
+ )
+
+ # propagate loader options etc. to the new query.
+ # these will fire relative to subq_path.
+ q = q._with_current_path(rewritten_path)
+ q = q.options(*new_options)
+
+ return q
+
+ def _setup_outermost_orderby(self, q):
+ if self.parent_property.order_by:
+
+ def _setup_outermost_orderby(compile_context):
+ compile_context.eager_order_by += tuple(
+ util.to_list(self.parent_property.order_by)
+ )
+
+ q = q._add_context_option(
+ _setup_outermost_orderby, self.parent_property
+ )
+
+ return q
+
+ class _SubqCollections:
+ """Given a :class:`_query.Query` used to emit the "subquery load",
+ provide a load interface that executes the query at the
+ first moment a value is needed.
+
+ """
+
+ __slots__ = (
+ "session",
+ "execution_options",
+ "load_options",
+ "params",
+ "subq",
+ "_data",
+ )
+
+ def __init__(self, context, subq):
+ # avoid creating a cycle by storing context
+ # even though that's preferable
+ self.session = context.session
+ self.execution_options = context.execution_options
+ self.load_options = context.load_options
+ self.params = context.params or {}
+ self.subq = subq
+ self._data = None
+
+ def get(self, key, default):
+ if self._data is None:
+ self._load()
+ return self._data.get(key, default)
+
+ def _load(self):
+ self._data = collections.defaultdict(list)
+
+ q = self.subq
+ assert q.session is None
+
+ q = q.with_session(self.session)
+
+ if self.load_options._populate_existing:
+ q = q.populate_existing()
+ # to work with baked query, the parameters may have been
+ # updated since this query was created, so take these into account
+
+ rows = list(q.params(self.params))
+ for k, v in itertools.groupby(rows, lambda x: x[1:]):
+ self._data[k].extend(vv[0] for vv in v)
+
+ def loader(self, state, dict_, row):
+ if self._data is None:
+ self._load()
+
+ def _setup_query_from_rowproc(
+ self,
+ context,
+ query_entity,
+ path,
+ entity,
+ loadopt,
+ adapter,
+ ):
+ compile_state = context.compile_state
+ if (
+ not compile_state.compile_options._enable_eagerloads
+ or compile_state.compile_options._for_refresh_state
+ ):
+ return
+
+ orig_query_entity_index = compile_state._entities.index(query_entity)
+ context.loaders_require_buffering = True
+
+ path = path[self.parent_property]
+
+ # build up a path indicating the path from the leftmost
+ # entity to the thing we're subquery loading.
+ with_poly_entity = path.get(
+ compile_state.attributes, "path_with_polymorphic", None
+ )
+ if with_poly_entity is not None:
+ effective_entity = with_poly_entity
+ else:
+ effective_entity = self.entity
+
+ subq_path, rewritten_path = context.query._execution_options.get(
+ ("subquery_paths", None),
+ (orm_util.PathRegistry.root, orm_util.PathRegistry.root),
+ )
+ is_root = subq_path is orm_util.PathRegistry.root
+ subq_path = subq_path + path
+ rewritten_path = rewritten_path + path
+
+ # use the current query being invoked, not the compile state
+ # one. this is so that we get the current parameters. however,
+ # it means we can't use the existing compile state, we have to make
+ # a new one. other approaches include possibly using the
+ # compiled query but swapping the params, seems only marginally
+ # less time spent but more complicated
+ orig_query = context.query._execution_options.get(
+ ("orig_query", SubqueryLoader), context.query
+ )
+
+ # make a new compile_state for the query that's probably cached, but
+ # we're sort of undoing a bit of that caching :(
+ compile_state_cls = ORMCompileState._get_plugin_class_for_plugin(
+ orig_query, "orm"
+ )
+
+ if orig_query._is_lambda_element:
+ if context.load_options._lazy_loaded_from is None:
+ util.warn(
+ 'subqueryloader for "%s" must invoke lambda callable '
+ "at %r in "
+ "order to produce a new query, decreasing the efficiency "
+ "of caching for this statement. Consider using "
+ "selectinload() for more effective full-lambda caching"
+ % (self, orig_query)
+ )
+ orig_query = orig_query._resolved
+
+ # this is the more "quick" version, however it's not clear how
+ # much of this we need. in particular I can't get a test to
+ # fail if the "set_base_alias" is missing and not sure why that is.
+ orig_compile_state = compile_state_cls._create_entities_collection(
+ orig_query, legacy=False
+ )
+
+ (
+ leftmost_mapper,
+ leftmost_attr,
+ leftmost_relationship,
+ rewritten_path,
+ ) = self._get_leftmost(
+ orig_query_entity_index,
+ rewritten_path,
+ orig_compile_state,
+ is_root,
+ )
+
+ # generate a new Query from the original, then
+ # produce a subquery from it.
+ left_alias = self._generate_from_original_query(
+ orig_compile_state,
+ orig_query,
+ leftmost_mapper,
+ leftmost_attr,
+ leftmost_relationship,
+ entity,
+ )
+
+ # generate another Query that will join the
+ # left alias to the target relationships.
+ # basically doing a longhand
+ # "from_self()". (from_self() itself not quite industrial
+ # strength enough for all contingencies...but very close)
+
+ q = query.Query(effective_entity)
+
+ q._execution_options = context.query._execution_options.merge_with(
+ context.execution_options,
+ {
+ ("orig_query", SubqueryLoader): orig_query,
+ ("subquery_paths", None): (subq_path, rewritten_path),
+ },
+ )
+
+ q = q._set_enable_single_crit(False)
+ to_join, local_attr, parent_alias = self._prep_for_joins(
+ left_alias, subq_path
+ )
+
+ q = q.add_columns(*local_attr)
+ q = self._apply_joins(
+ q, to_join, left_alias, parent_alias, effective_entity
+ )
+
+ q = self._setup_options(
+ context,
+ q,
+ subq_path,
+ rewritten_path,
+ orig_query,
+ effective_entity,
+ loadopt,
+ )
+ q = self._setup_outermost_orderby(q)
+
+ return q
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ if context.refresh_state:
+ return self._immediateload_create_row_processor(
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ )
+
+ _, run_loader, _, _ = self._setup_for_recursion(
+ context, path, loadopt, self.join_depth
+ )
+ if not run_loader:
+ return
+
+ if not isinstance(context.compile_state, ORMSelectCompileState):
+ # issue 7505 - subqueryload() in 1.3 and previous would silently
+ # degrade for from_statement() without warning. this behavior
+ # is restored here
+ return
+
+ if not self.parent.class_manager[self.key].impl.supports_population:
+ raise sa_exc.InvalidRequestError(
+ "'%s' does not support object "
+ "population - eager loading cannot be applied." % self
+ )
+
+ # a little dance here as the "path" is still something that only
+ # semi-tracks the exact series of things we are loading, still not
+ # telling us about with_polymorphic() and stuff like that when it's at
+ # the root.. the initial MapperEntity is more accurate for this case.
+ if len(path) == 1:
+ if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
+ return
+ elif not orm_util._entity_isa(path[-1], self.parent):
+ return
+
+ subq = self._setup_query_from_rowproc(
+ context,
+ query_entity,
+ path,
+ path[-1],
+ loadopt,
+ adapter,
+ )
+
+ if subq is None:
+ return
+
+ assert subq.session is None
+
+ path = path[self.parent_property]
+
+ local_cols = self.parent_property.local_columns
+
+ # cache the loaded collections in the context
+ # so that inheriting mappers don't re-load when they
+ # call upon create_row_processor again
+ collections = path.get(context.attributes, "collections")
+ if collections is None:
+ collections = self._SubqCollections(context, subq)
+ path.set(context.attributes, "collections", collections)
+
+ if adapter:
+ local_cols = [adapter.columns[c] for c in local_cols]
+
+ if self.uselist:
+ self._create_collection_loader(
+ context, result, collections, local_cols, populators
+ )
+ else:
+ self._create_scalar_loader(
+ context, result, collections, local_cols, populators
+ )
+
+ def _create_collection_loader(
+ self, context, result, collections, local_cols, populators
+ ):
+ tuple_getter = result._tuple_getter(local_cols)
+
+ def load_collection_from_subq(state, dict_, row):
+ collection = collections.get(tuple_getter(row), ())
+ state.get_impl(self.key).set_committed_value(
+ state, dict_, collection
+ )
+
+ def load_collection_from_subq_existing_row(state, dict_, row):
+ if self.key not in dict_:
+ load_collection_from_subq(state, dict_, row)
+
+ populators["new"].append((self.key, load_collection_from_subq))
+ populators["existing"].append(
+ (self.key, load_collection_from_subq_existing_row)
+ )
+
+ if context.invoke_all_eagers:
+ populators["eager"].append((self.key, collections.loader))
+
+ def _create_scalar_loader(
+ self, context, result, collections, local_cols, populators
+ ):
+ tuple_getter = result._tuple_getter(local_cols)
+
+ def load_scalar_from_subq(state, dict_, row):
+ collection = collections.get(tuple_getter(row), (None,))
+ if len(collection) > 1:
+ util.warn(
+ "Multiple rows returned with "
+ "uselist=False for eagerly-loaded attribute '%s' " % self
+ )
+
+ scalar = collection[0]
+ state.get_impl(self.key).set_committed_value(state, dict_, scalar)
+
+ def load_scalar_from_subq_existing_row(state, dict_, row):
+ if self.key not in dict_:
+ load_scalar_from_subq(state, dict_, row)
+
+ populators["new"].append((self.key, load_scalar_from_subq))
+ populators["existing"].append(
+ (self.key, load_scalar_from_subq_existing_row)
+ )
+ if context.invoke_all_eagers:
+ populators["eager"].append((self.key, collections.loader))
+
+
+@log.class_logger
+@relationships.RelationshipProperty.strategy_for(lazy="joined")
+@relationships.RelationshipProperty.strategy_for(lazy=False)
+class JoinedLoader(AbstractRelationshipLoader):
+ """Provide loading behavior for a :class:`.Relationship`
+ using joined eager loading.
+
+ """
+
+ __slots__ = "join_depth"
+
+ def __init__(self, parent, strategy_key):
+ super().__init__(parent, strategy_key)
+ self.join_depth = self.parent_property.join_depth
+
+ def init_class_attribute(self, mapper):
+ self.parent_property._get_strategy(
+ (("lazy", "select"),)
+ ).init_class_attribute(mapper)
+
+ def setup_query(
+ self,
+ compile_state,
+ query_entity,
+ path,
+ loadopt,
+ adapter,
+ column_collection=None,
+ parentmapper=None,
+ chained_from_outerjoin=False,
+ **kwargs,
+ ):
+ """Add a left outer join to the statement that's being constructed."""
+
+ if not compile_state.compile_options._enable_eagerloads:
+ return
+ elif self.uselist:
+ compile_state.multi_row_eager_loaders = True
+
+ path = path[self.parent_property]
+
+ with_polymorphic = None
+
+ user_defined_adapter = (
+ self._init_user_defined_eager_proc(
+ loadopt, compile_state, compile_state.attributes
+ )
+ if loadopt
+ else False
+ )
+
+ if user_defined_adapter is not False:
+ # setup an adapter but dont create any JOIN, assume it's already
+ # in the query
+ (
+ clauses,
+ adapter,
+ add_to_collection,
+ ) = self._setup_query_on_user_defined_adapter(
+ compile_state,
+ query_entity,
+ path,
+ adapter,
+ user_defined_adapter,
+ )
+
+ # don't do "wrap" for multi-row, we want to wrap
+ # limited/distinct SELECT,
+ # because we want to put the JOIN on the outside.
+
+ else:
+ # if not via query option, check for
+ # a cycle
+ if not path.contains(compile_state.attributes, "loader"):
+ if self.join_depth:
+ if path.length / 2 > self.join_depth:
+ return
+ elif path.contains_mapper(self.mapper):
+ return
+
+ # add the JOIN and create an adapter
+ (
+ clauses,
+ adapter,
+ add_to_collection,
+ chained_from_outerjoin,
+ ) = self._generate_row_adapter(
+ compile_state,
+ query_entity,
+ path,
+ loadopt,
+ adapter,
+ column_collection,
+ parentmapper,
+ chained_from_outerjoin,
+ )
+
+ # for multi-row, we want to wrap limited/distinct SELECT,
+ # because we want to put the JOIN on the outside.
+ compile_state.eager_adding_joins = True
+
+ with_poly_entity = path.get(
+ compile_state.attributes, "path_with_polymorphic", None
+ )
+ if with_poly_entity is not None:
+ with_polymorphic = inspect(
+ with_poly_entity
+ ).with_polymorphic_mappers
+ else:
+ with_polymorphic = None
+
+ path = path[self.entity]
+
+ loading._setup_entity_query(
+ compile_state,
+ self.mapper,
+ query_entity,
+ path,
+ clauses,
+ add_to_collection,
+ with_polymorphic=with_polymorphic,
+ parentmapper=self.mapper,
+ chained_from_outerjoin=chained_from_outerjoin,
+ )
+
+ has_nones = util.NONE_SET.intersection(compile_state.secondary_columns)
+
+ if has_nones:
+ if with_poly_entity is not None:
+ raise sa_exc.InvalidRequestError(
+ "Detected unaliased columns when generating joined "
+ "load. Make sure to use aliased=True or flat=True "
+ "when using joined loading with with_polymorphic()."
+ )
+ else:
+ compile_state.secondary_columns = [
+ c for c in compile_state.secondary_columns if c is not None
+ ]
+
+ def _init_user_defined_eager_proc(
+ self, loadopt, compile_state, target_attributes
+ ):
+ # check if the opt applies at all
+ if "eager_from_alias" not in loadopt.local_opts:
+ # nope
+ return False
+
+ path = loadopt.path.parent
+
+ # the option applies. check if the "user_defined_eager_row_processor"
+ # has been built up.
+ adapter = path.get(
+ compile_state.attributes, "user_defined_eager_row_processor", False
+ )
+ if adapter is not False:
+ # just return it
+ return adapter
+
+ # otherwise figure it out.
+ alias = loadopt.local_opts["eager_from_alias"]
+ root_mapper, prop = path[-2:]
+
+ if alias is not None:
+ if isinstance(alias, str):
+ alias = prop.target.alias(alias)
+ adapter = orm_util.ORMAdapter(
+ orm_util._TraceAdaptRole.JOINEDLOAD_USER_DEFINED_ALIAS,
+ prop.mapper,
+ selectable=alias,
+ equivalents=prop.mapper._equivalent_columns,
+ limit_on_entity=False,
+ )
+ else:
+ if path.contains(
+ compile_state.attributes, "path_with_polymorphic"
+ ):
+ with_poly_entity = path.get(
+ compile_state.attributes, "path_with_polymorphic"
+ )
+ adapter = orm_util.ORMAdapter(
+ orm_util._TraceAdaptRole.JOINEDLOAD_PATH_WITH_POLYMORPHIC,
+ with_poly_entity,
+ equivalents=prop.mapper._equivalent_columns,
+ )
+ else:
+ adapter = compile_state._polymorphic_adapters.get(
+ prop.mapper, None
+ )
+ path.set(
+ target_attributes,
+ "user_defined_eager_row_processor",
+ adapter,
+ )
+
+ return adapter
+
+ def _setup_query_on_user_defined_adapter(
+ self, context, entity, path, adapter, user_defined_adapter
+ ):
+ # apply some more wrapping to the "user defined adapter"
+ # if we are setting up the query for SQL render.
+ adapter = entity._get_entity_clauses(context)
+
+ if adapter and user_defined_adapter:
+ user_defined_adapter = user_defined_adapter.wrap(adapter)
+ path.set(
+ context.attributes,
+ "user_defined_eager_row_processor",
+ user_defined_adapter,
+ )
+ elif adapter:
+ user_defined_adapter = adapter
+ path.set(
+ context.attributes,
+ "user_defined_eager_row_processor",
+ user_defined_adapter,
+ )
+
+ add_to_collection = context.primary_columns
+ return user_defined_adapter, adapter, add_to_collection
+
+ def _generate_row_adapter(
+ self,
+ compile_state,
+ entity,
+ path,
+ loadopt,
+ adapter,
+ column_collection,
+ parentmapper,
+ chained_from_outerjoin,
+ ):
+ with_poly_entity = path.get(
+ compile_state.attributes, "path_with_polymorphic", None
+ )
+ if with_poly_entity:
+ to_adapt = with_poly_entity
+ else:
+ insp = inspect(self.entity)
+ if insp.is_aliased_class:
+ alt_selectable = insp.selectable
+ else:
+ alt_selectable = None
+
+ to_adapt = orm_util.AliasedClass(
+ self.mapper,
+ alias=(
+ alt_selectable._anonymous_fromclause(flat=True)
+ if alt_selectable is not None
+ else None
+ ),
+ flat=True,
+ use_mapper_path=True,
+ )
+
+ to_adapt_insp = inspect(to_adapt)
+
+ clauses = to_adapt_insp._memo(
+ ("joinedloader_ormadapter", self),
+ orm_util.ORMAdapter,
+ orm_util._TraceAdaptRole.JOINEDLOAD_MEMOIZED_ADAPTER,
+ to_adapt_insp,
+ equivalents=self.mapper._equivalent_columns,
+ adapt_required=True,
+ allow_label_resolve=False,
+ anonymize_labels=True,
+ )
+
+ assert clauses.is_aliased_class
+
+ innerjoin = (
+ loadopt.local_opts.get("innerjoin", self.parent_property.innerjoin)
+ if loadopt is not None
+ else self.parent_property.innerjoin
+ )
+
+ if not innerjoin:
+ # if this is an outer join, all non-nested eager joins from
+ # this path must also be outer joins
+ chained_from_outerjoin = True
+
+ compile_state.create_eager_joins.append(
+ (
+ self._create_eager_join,
+ entity,
+ path,
+ adapter,
+ parentmapper,
+ clauses,
+ innerjoin,
+ chained_from_outerjoin,
+ loadopt._extra_criteria if loadopt else (),
+ )
+ )
+
+ add_to_collection = compile_state.secondary_columns
+ path.set(compile_state.attributes, "eager_row_processor", clauses)
+
+ return clauses, adapter, add_to_collection, chained_from_outerjoin
+
+ def _create_eager_join(
+ self,
+ compile_state,
+ query_entity,
+ path,
+ adapter,
+ parentmapper,
+ clauses,
+ innerjoin,
+ chained_from_outerjoin,
+ extra_criteria,
+ ):
+ if parentmapper is None:
+ localparent = query_entity.mapper
+ else:
+ localparent = parentmapper
+
+ # whether or not the Query will wrap the selectable in a subquery,
+ # and then attach eager load joins to that (i.e., in the case of
+ # LIMIT/OFFSET etc.)
+ should_nest_selectable = (
+ compile_state.multi_row_eager_loaders
+ and compile_state._should_nest_selectable
+ )
+
+ query_entity_key = None
+
+ if (
+ query_entity not in compile_state.eager_joins
+ and not should_nest_selectable
+ and compile_state.from_clauses
+ ):
+ indexes = sql_util.find_left_clause_that_matches_given(
+ compile_state.from_clauses, query_entity.selectable
+ )
+
+ if len(indexes) > 1:
+ # for the eager load case, I can't reproduce this right
+ # now. For query.join() I can.
+ raise sa_exc.InvalidRequestError(
+ "Can't identify which query entity in which to joined "
+ "eager load from. Please use an exact match when "
+ "specifying the join path."
+ )
+
+ if indexes:
+ clause = compile_state.from_clauses[indexes[0]]
+ # join to an existing FROM clause on the query.
+ # key it to its list index in the eager_joins dict.
+ # Query._compile_context will adapt as needed and
+ # append to the FROM clause of the select().
+ query_entity_key, default_towrap = indexes[0], clause
+
+ if query_entity_key is None:
+ query_entity_key, default_towrap = (
+ query_entity,
+ query_entity.selectable,
+ )
+
+ towrap = compile_state.eager_joins.setdefault(
+ query_entity_key, default_towrap
+ )
+
+ if adapter:
+ if getattr(adapter, "is_aliased_class", False):
+ # joining from an adapted entity. The adapted entity
+ # might be a "with_polymorphic", so resolve that to our
+ # specific mapper's entity before looking for our attribute
+ # name on it.
+ efm = adapter.aliased_insp._entity_for_mapper(
+ localparent
+ if localparent.isa(self.parent)
+ else self.parent
+ )
+
+ # look for our attribute on the adapted entity, else fall back
+ # to our straight property
+ onclause = getattr(efm.entity, self.key, self.parent_property)
+ else:
+ onclause = getattr(
+ orm_util.AliasedClass(
+ self.parent, adapter.selectable, use_mapper_path=True
+ ),
+ self.key,
+ self.parent_property,
+ )
+
+ else:
+ onclause = self.parent_property
+
+ assert clauses.is_aliased_class
+
+ attach_on_outside = (
+ not chained_from_outerjoin
+ or not innerjoin
+ or innerjoin == "unnested"
+ or query_entity.entity_zero.represents_outer_join
+ )
+
+ extra_join_criteria = extra_criteria
+ additional_entity_criteria = compile_state.global_attributes.get(
+ ("additional_entity_criteria", self.mapper), ()
+ )
+ if additional_entity_criteria:
+ extra_join_criteria += tuple(
+ ae._resolve_where_criteria(self.mapper)
+ for ae in additional_entity_criteria
+ if ae.propagate_to_loaders
+ )
+
+ if attach_on_outside:
+ # this is the "classic" eager join case.
+ eagerjoin = orm_util._ORMJoin(
+ towrap,
+ clauses.aliased_insp,
+ onclause,
+ isouter=not innerjoin
+ or query_entity.entity_zero.represents_outer_join
+ or (chained_from_outerjoin and isinstance(towrap, sql.Join)),
+ _left_memo=self.parent,
+ _right_memo=self.mapper,
+ _extra_criteria=extra_join_criteria,
+ )
+ else:
+ # all other cases are innerjoin=='nested' approach
+ eagerjoin = self._splice_nested_inner_join(
+ path, towrap, clauses, onclause, extra_join_criteria
+ )
+
+ compile_state.eager_joins[query_entity_key] = eagerjoin
+
+ # send a hint to the Query as to where it may "splice" this join
+ eagerjoin.stop_on = query_entity.selectable
+
+ if not parentmapper:
+ # for parentclause that is the non-eager end of the join,
+ # ensure all the parent cols in the primaryjoin are actually
+ # in the
+ # columns clause (i.e. are not deferred), so that aliasing applied
+ # by the Query propagates those columns outward.
+ # This has the effect
+ # of "undefering" those columns.
+ for col in sql_util._find_columns(
+ self.parent_property.primaryjoin
+ ):
+ if localparent.persist_selectable.c.contains_column(col):
+ if adapter:
+ col = adapter.columns[col]
+ compile_state._append_dedupe_col_collection(
+ col, compile_state.primary_columns
+ )
+
+ if self.parent_property.order_by:
+ compile_state.eager_order_by += tuple(
+ (eagerjoin._target_adapter.copy_and_process)(
+ util.to_list(self.parent_property.order_by)
+ )
+ )
+
+ def _splice_nested_inner_join(
+ self, path, join_obj, clauses, onclause, extra_criteria, splicing=False
+ ):
+ # recursive fn to splice a nested join into an existing one.
+ # splicing=False means this is the outermost call, and it
+ # should return a value. splicing=<from object> is the recursive
+ # form, where it can return None to indicate the end of the recursion
+
+ if splicing is False:
+ # first call is always handed a join object
+ # from the outside
+ assert isinstance(join_obj, orm_util._ORMJoin)
+ elif isinstance(join_obj, sql.selectable.FromGrouping):
+ return self._splice_nested_inner_join(
+ path,
+ join_obj.element,
+ clauses,
+ onclause,
+ extra_criteria,
+ splicing,
+ )
+ elif not isinstance(join_obj, orm_util._ORMJoin):
+ if path[-2].isa(splicing):
+ return orm_util._ORMJoin(
+ join_obj,
+ clauses.aliased_insp,
+ onclause,
+ isouter=False,
+ _left_memo=splicing,
+ _right_memo=path[-1].mapper,
+ _extra_criteria=extra_criteria,
+ )
+ else:
+ return None
+
+ target_join = self._splice_nested_inner_join(
+ path,
+ join_obj.right,
+ clauses,
+ onclause,
+ extra_criteria,
+ join_obj._right_memo,
+ )
+ if target_join is None:
+ right_splice = False
+ target_join = self._splice_nested_inner_join(
+ path,
+ join_obj.left,
+ clauses,
+ onclause,
+ extra_criteria,
+ join_obj._left_memo,
+ )
+ if target_join is None:
+ # should only return None when recursively called,
+ # e.g. splicing refers to a from obj
+ assert (
+ splicing is not False
+ ), "assertion failed attempting to produce joined eager loads"
+ return None
+ else:
+ right_splice = True
+
+ if right_splice:
+ # for a right splice, attempt to flatten out
+ # a JOIN b JOIN c JOIN .. to avoid needless
+ # parenthesis nesting
+ if not join_obj.isouter and not target_join.isouter:
+ eagerjoin = join_obj._splice_into_center(target_join)
+ else:
+ eagerjoin = orm_util._ORMJoin(
+ join_obj.left,
+ target_join,
+ join_obj.onclause,
+ isouter=join_obj.isouter,
+ _left_memo=join_obj._left_memo,
+ )
+ else:
+ eagerjoin = orm_util._ORMJoin(
+ target_join,
+ join_obj.right,
+ join_obj.onclause,
+ isouter=join_obj.isouter,
+ _right_memo=join_obj._right_memo,
+ )
+
+ eagerjoin._target_adapter = target_join._target_adapter
+ return eagerjoin
+
+ def _create_eager_adapter(self, context, result, adapter, path, loadopt):
+ compile_state = context.compile_state
+
+ user_defined_adapter = (
+ self._init_user_defined_eager_proc(
+ loadopt, compile_state, context.attributes
+ )
+ if loadopt
+ else False
+ )
+
+ if user_defined_adapter is not False:
+ decorator = user_defined_adapter
+ # user defined eagerloads are part of the "primary"
+ # portion of the load.
+ # the adapters applied to the Query should be honored.
+ if compile_state.compound_eager_adapter and decorator:
+ decorator = decorator.wrap(
+ compile_state.compound_eager_adapter
+ )
+ elif compile_state.compound_eager_adapter:
+ decorator = compile_state.compound_eager_adapter
+ else:
+ decorator = path.get(
+ compile_state.attributes, "eager_row_processor"
+ )
+ if decorator is None:
+ return False
+
+ if self.mapper._result_has_identity_key(result, decorator):
+ return decorator
+ else:
+ # no identity key - don't return a row
+ # processor, will cause a degrade to lazy
+ return False
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ if not self.parent.class_manager[self.key].impl.supports_population:
+ raise sa_exc.InvalidRequestError(
+ "'%s' does not support object "
+ "population - eager loading cannot be applied." % self
+ )
+
+ if self.uselist:
+ context.loaders_require_uniquing = True
+
+ our_path = path[self.parent_property]
+
+ eager_adapter = self._create_eager_adapter(
+ context, result, adapter, our_path, loadopt
+ )
+
+ if eager_adapter is not False:
+ key = self.key
+
+ _instance = loading._instance_processor(
+ query_entity,
+ self.mapper,
+ context,
+ result,
+ our_path[self.entity],
+ eager_adapter,
+ )
+
+ if not self.uselist:
+ self._create_scalar_loader(context, key, _instance, populators)
+ else:
+ self._create_collection_loader(
+ context, key, _instance, populators
+ )
+ else:
+ self.parent_property._get_strategy(
+ (("lazy", "select"),)
+ ).create_row_processor(
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ )
+
+ def _create_collection_loader(self, context, key, _instance, populators):
+ def load_collection_from_joined_new_row(state, dict_, row):
+ # note this must unconditionally clear out any existing collection.
+ # an existing collection would be present only in the case of
+ # populate_existing().
+ collection = attributes.init_state_collection(state, dict_, key)
+ result_list = util.UniqueAppender(
+ collection, "append_without_event"
+ )
+ context.attributes[(state, key)] = result_list
+ inst = _instance(row)
+ if inst is not None:
+ result_list.append(inst)
+
+ def load_collection_from_joined_existing_row(state, dict_, row):
+ if (state, key) in context.attributes:
+ result_list = context.attributes[(state, key)]
+ else:
+ # appender_key can be absent from context.attributes
+ # with isnew=False when self-referential eager loading
+ # is used; the same instance may be present in two
+ # distinct sets of result columns
+ collection = attributes.init_state_collection(
+ state, dict_, key
+ )
+ result_list = util.UniqueAppender(
+ collection, "append_without_event"
+ )
+ context.attributes[(state, key)] = result_list
+ inst = _instance(row)
+ if inst is not None:
+ result_list.append(inst)
+
+ def load_collection_from_joined_exec(state, dict_, row):
+ _instance(row)
+
+ populators["new"].append(
+ (self.key, load_collection_from_joined_new_row)
+ )
+ populators["existing"].append(
+ (self.key, load_collection_from_joined_existing_row)
+ )
+ if context.invoke_all_eagers:
+ populators["eager"].append(
+ (self.key, load_collection_from_joined_exec)
+ )
+
+ def _create_scalar_loader(self, context, key, _instance, populators):
+ def load_scalar_from_joined_new_row(state, dict_, row):
+ # set a scalar object instance directly on the parent
+ # object, bypassing InstrumentedAttribute event handlers.
+ dict_[key] = _instance(row)
+
+ def load_scalar_from_joined_existing_row(state, dict_, row):
+ # call _instance on the row, even though the object has
+ # been created, so that we further descend into properties
+ existing = _instance(row)
+
+ # conflicting value already loaded, this shouldn't happen
+ if key in dict_:
+ if existing is not dict_[key]:
+ util.warn(
+ "Multiple rows returned with "
+ "uselist=False for eagerly-loaded attribute '%s' "
+ % self
+ )
+ else:
+ # this case is when one row has multiple loads of the
+ # same entity (e.g. via aliasing), one has an attribute
+ # that the other doesn't.
+ dict_[key] = existing
+
+ def load_scalar_from_joined_exec(state, dict_, row):
+ _instance(row)
+
+ populators["new"].append((self.key, load_scalar_from_joined_new_row))
+ populators["existing"].append(
+ (self.key, load_scalar_from_joined_existing_row)
+ )
+ if context.invoke_all_eagers:
+ populators["eager"].append(
+ (self.key, load_scalar_from_joined_exec)
+ )
+
+
+@log.class_logger
+@relationships.RelationshipProperty.strategy_for(lazy="selectin")
+class SelectInLoader(PostLoader, util.MemoizedSlots):
+ __slots__ = (
+ "join_depth",
+ "omit_join",
+ "_parent_alias",
+ "_query_info",
+ "_fallback_query_info",
+ )
+
+ query_info = collections.namedtuple(
+ "queryinfo",
+ [
+ "load_only_child",
+ "load_with_join",
+ "in_expr",
+ "pk_cols",
+ "zero_idx",
+ "child_lookup_cols",
+ ],
+ )
+
+ _chunksize = 500
+
+ def __init__(self, parent, strategy_key):
+ super().__init__(parent, strategy_key)
+ self.join_depth = self.parent_property.join_depth
+ is_m2o = self.parent_property.direction is interfaces.MANYTOONE
+
+ if self.parent_property.omit_join is not None:
+ self.omit_join = self.parent_property.omit_join
+ else:
+ lazyloader = self.parent_property._get_strategy(
+ (("lazy", "select"),)
+ )
+ if is_m2o:
+ self.omit_join = lazyloader.use_get
+ else:
+ self.omit_join = self.parent._get_clause[0].compare(
+ lazyloader._rev_lazywhere,
+ use_proxies=True,
+ compare_keys=False,
+ equivalents=self.parent._equivalent_columns,
+ )
+
+ if self.omit_join:
+ if is_m2o:
+ self._query_info = self._init_for_omit_join_m2o()
+ self._fallback_query_info = self._init_for_join()
+ else:
+ self._query_info = self._init_for_omit_join()
+ else:
+ self._query_info = self._init_for_join()
+
+ def _init_for_omit_join(self):
+ pk_to_fk = dict(
+ self.parent_property._join_condition.local_remote_pairs
+ )
+ pk_to_fk.update(
+ (equiv, pk_to_fk[k])
+ for k in list(pk_to_fk)
+ for equiv in self.parent._equivalent_columns.get(k, ())
+ )
+
+ pk_cols = fk_cols = [
+ pk_to_fk[col] for col in self.parent.primary_key if col in pk_to_fk
+ ]
+ if len(fk_cols) > 1:
+ in_expr = sql.tuple_(*fk_cols)
+ zero_idx = False
+ else:
+ in_expr = fk_cols[0]
+ zero_idx = True
+
+ return self.query_info(False, False, in_expr, pk_cols, zero_idx, None)
+
+ def _init_for_omit_join_m2o(self):
+ pk_cols = self.mapper.primary_key
+ if len(pk_cols) > 1:
+ in_expr = sql.tuple_(*pk_cols)
+ zero_idx = False
+ else:
+ in_expr = pk_cols[0]
+ zero_idx = True
+
+ lazyloader = self.parent_property._get_strategy((("lazy", "select"),))
+ lookup_cols = [lazyloader._equated_columns[pk] for pk in pk_cols]
+
+ return self.query_info(
+ True, False, in_expr, pk_cols, zero_idx, lookup_cols
+ )
+
+ def _init_for_join(self):
+ self._parent_alias = AliasedClass(self.parent.class_)
+ pa_insp = inspect(self._parent_alias)
+ pk_cols = [
+ pa_insp._adapt_element(col) for col in self.parent.primary_key
+ ]
+ if len(pk_cols) > 1:
+ in_expr = sql.tuple_(*pk_cols)
+ zero_idx = False
+ else:
+ in_expr = pk_cols[0]
+ zero_idx = True
+ return self.query_info(False, True, in_expr, pk_cols, zero_idx, None)
+
+ def init_class_attribute(self, mapper):
+ self.parent_property._get_strategy(
+ (("lazy", "select"),)
+ ).init_class_attribute(mapper)
+
+ def create_row_processor(
+ self,
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ ):
+ if context.refresh_state:
+ return self._immediateload_create_row_processor(
+ context,
+ query_entity,
+ path,
+ loadopt,
+ mapper,
+ result,
+ adapter,
+ populators,
+ )
+
+ (
+ effective_path,
+ run_loader,
+ execution_options,
+ recursion_depth,
+ ) = self._setup_for_recursion(
+ context, path, loadopt, join_depth=self.join_depth
+ )
+
+ if not run_loader:
+ return
+
+ if not self.parent.class_manager[self.key].impl.supports_population:
+ raise sa_exc.InvalidRequestError(
+ "'%s' does not support object "
+ "population - eager loading cannot be applied." % self
+ )
+
+ # a little dance here as the "path" is still something that only
+ # semi-tracks the exact series of things we are loading, still not
+ # telling us about with_polymorphic() and stuff like that when it's at
+ # the root.. the initial MapperEntity is more accurate for this case.
+ if len(path) == 1:
+ if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
+ return
+ elif not orm_util._entity_isa(path[-1], self.parent):
+ return
+
+ selectin_path = effective_path
+
+ path_w_prop = path[self.parent_property]
+
+ # build up a path indicating the path from the leftmost
+ # entity to the thing we're subquery loading.
+ with_poly_entity = path_w_prop.get(
+ context.attributes, "path_with_polymorphic", None
+ )
+ if with_poly_entity is not None:
+ effective_entity = inspect(with_poly_entity)
+ else:
+ effective_entity = self.entity
+
+ loading.PostLoad.callable_for_path(
+ context,
+ selectin_path,
+ self.parent,
+ self.parent_property,
+ self._load_for_path,
+ effective_entity,
+ loadopt,
+ recursion_depth,
+ execution_options,
+ )
+
+ def _load_for_path(
+ self,
+ context,
+ path,
+ states,
+ load_only,
+ effective_entity,
+ loadopt,
+ recursion_depth,
+ execution_options,
+ ):
+ if load_only and self.key not in load_only:
+ return
+
+ query_info = self._query_info
+
+ if query_info.load_only_child:
+ our_states = collections.defaultdict(list)
+ none_states = []
+
+ mapper = self.parent
+
+ for state, overwrite in states:
+ state_dict = state.dict
+ related_ident = tuple(
+ mapper._get_state_attr_by_column(
+ state,
+ state_dict,
+ lk,
+ passive=attributes.PASSIVE_NO_FETCH,
+ )
+ for lk in query_info.child_lookup_cols
+ )
+ # if the loaded parent objects do not have the foreign key
+ # to the related item loaded, then degrade into the joined
+ # version of selectinload
+ if LoaderCallableStatus.PASSIVE_NO_RESULT in related_ident:
+ query_info = self._fallback_query_info
+ break
+
+ # organize states into lists keyed to particular foreign
+ # key values.
+ if None not in related_ident:
+ our_states[related_ident].append(
+ (state, state_dict, overwrite)
+ )
+ else:
+ # For FK values that have None, add them to a
+ # separate collection that will be populated separately
+ none_states.append((state, state_dict, overwrite))
+
+ # note the above conditional may have changed query_info
+ if not query_info.load_only_child:
+ our_states = [
+ (state.key[1], state, state.dict, overwrite)
+ for state, overwrite in states
+ ]
+
+ pk_cols = query_info.pk_cols
+ in_expr = query_info.in_expr
+
+ if not query_info.load_with_join:
+ # in "omit join" mode, the primary key column and the
+ # "in" expression are in terms of the related entity. So
+ # if the related entity is polymorphic or otherwise aliased,
+ # we need to adapt our "pk_cols" and "in_expr" to that
+ # entity. in non-"omit join" mode, these are against the
+ # parent entity and do not need adaption.
+ if effective_entity.is_aliased_class:
+ pk_cols = [
+ effective_entity._adapt_element(col) for col in pk_cols
+ ]
+ in_expr = effective_entity._adapt_element(in_expr)
+
+ bundle_ent = orm_util.Bundle("pk", *pk_cols)
+ bundle_sql = bundle_ent.__clause_element__()
+
+ entity_sql = effective_entity.__clause_element__()
+ q = Select._create_raw_select(
+ _raw_columns=[bundle_sql, entity_sql],
+ _label_style=LABEL_STYLE_TABLENAME_PLUS_COL,
+ _compile_options=ORMCompileState.default_compile_options,
+ _propagate_attrs={
+ "compile_state_plugin": "orm",
+ "plugin_subject": effective_entity,
+ },
+ )
+
+ if not query_info.load_with_join:
+ # the Bundle we have in the "omit_join" case is against raw, non
+ # annotated columns, so to ensure the Query knows its primary
+ # entity, we add it explicitly. If we made the Bundle against
+ # annotated columns, we hit a performance issue in this specific
+ # case, which is detailed in issue #4347.
+ q = q.select_from(effective_entity)
+ else:
+ # in the non-omit_join case, the Bundle is against the annotated/
+ # mapped column of the parent entity, but the #4347 issue does not
+ # occur in this case.
+ q = q.select_from(self._parent_alias).join(
+ getattr(self._parent_alias, self.parent_property.key).of_type(
+ effective_entity
+ )
+ )
+
+ q = q.filter(in_expr.in_(sql.bindparam("primary_keys")))
+
+ # a test which exercises what these comments talk about is
+ # test_selectin_relations.py -> test_twolevel_selectin_w_polymorphic
+ #
+ # effective_entity above is given to us in terms of the cached
+ # statement, namely this one:
+ orig_query = context.compile_state.select_statement
+
+ # the actual statement that was requested is this one:
+ # context_query = context.query
+ #
+ # that's not the cached one, however. So while it is of the identical
+ # structure, if it has entities like AliasedInsp, which we get from
+ # aliased() or with_polymorphic(), the AliasedInsp will likely be a
+ # different object identity each time, and will not match up
+ # hashing-wise to the corresponding AliasedInsp that's in the
+ # cached query, meaning it won't match on paths and loader lookups
+ # and loaders like this one will be skipped if it is used in options.
+ #
+ # as it turns out, standard loader options like selectinload(),
+ # lazyload() that have a path need
+ # to come from the cached query so that the AliasedInsp etc. objects
+ # that are in the query line up with the object that's in the path
+ # of the strategy object. however other options like
+ # with_loader_criteria() that doesn't have a path (has a fixed entity)
+ # and needs to have access to the latest closure state in order to
+ # be correct, we need to use the uncached one.
+ #
+ # as of #8399 we let the loader option itself figure out what it
+ # wants to do given cached and uncached version of itself.
+
+ effective_path = path[self.parent_property]
+
+ if orig_query is context.query:
+ new_options = orig_query._with_options
+ else:
+ cached_options = orig_query._with_options
+ uncached_options = context.query._with_options
+
+ # propagate compile state options from the original query,
+ # updating their "extra_criteria" as necessary.
+ # note this will create a different cache key than
+ # "orig" options if extra_criteria is present, because the copy
+ # of extra_criteria will have different boundparam than that of
+ # the QueryableAttribute in the path
+ new_options = [
+ orig_opt._adapt_cached_option_to_uncached_option(
+ context, uncached_opt
+ )
+ for orig_opt, uncached_opt in zip(
+ cached_options, uncached_options
+ )
+ ]
+
+ if loadopt and loadopt._extra_criteria:
+ new_options += (
+ orm_util.LoaderCriteriaOption(
+ effective_entity,
+ loadopt._generate_extra_criteria(context),
+ ),
+ )
+
+ if recursion_depth is not None:
+ effective_path = effective_path._truncate_recursive()
+
+ q = q.options(*new_options)
+
+ q = q._update_compile_options({"_current_path": effective_path})
+ if context.populate_existing:
+ q = q.execution_options(populate_existing=True)
+
+ if self.parent_property.order_by:
+ if not query_info.load_with_join:
+ eager_order_by = self.parent_property.order_by
+ if effective_entity.is_aliased_class:
+ eager_order_by = [
+ effective_entity._adapt_element(elem)
+ for elem in eager_order_by
+ ]
+ q = q.order_by(*eager_order_by)
+ else:
+
+ def _setup_outermost_orderby(compile_context):
+ compile_context.eager_order_by += tuple(
+ util.to_list(self.parent_property.order_by)
+ )
+
+ q = q._add_context_option(
+ _setup_outermost_orderby, self.parent_property
+ )
+
+ if query_info.load_only_child:
+ self._load_via_child(
+ our_states,
+ none_states,
+ query_info,
+ q,
+ context,
+ execution_options,
+ )
+ else:
+ self._load_via_parent(
+ our_states, query_info, q, context, execution_options
+ )
+
+ def _load_via_child(
+ self,
+ our_states,
+ none_states,
+ query_info,
+ q,
+ context,
+ execution_options,
+ ):
+ uselist = self.uselist
+
+ # this sort is really for the benefit of the unit tests
+ our_keys = sorted(our_states)
+ while our_keys:
+ chunk = our_keys[0 : self._chunksize]
+ our_keys = our_keys[self._chunksize :]
+ data = {
+ k: v
+ for k, v in context.session.execute(
+ q,
+ params={
+ "primary_keys": [
+ key[0] if query_info.zero_idx else key
+ for key in chunk
+ ]
+ },
+ execution_options=execution_options,
+ ).unique()
+ }
+
+ for key in chunk:
+ # for a real foreign key and no concurrent changes to the
+ # DB while running this method, "key" is always present in
+ # data. However, for primaryjoins without real foreign keys
+ # a non-None primaryjoin condition may still refer to no
+ # related object.
+ related_obj = data.get(key, None)
+ for state, dict_, overwrite in our_states[key]:
+ if not overwrite and self.key in dict_:
+ continue
+
+ state.get_impl(self.key).set_committed_value(
+ state,
+ dict_,
+ related_obj if not uselist else [related_obj],
+ )
+ # populate none states with empty value / collection
+ for state, dict_, overwrite in none_states:
+ if not overwrite and self.key in dict_:
+ continue
+
+ # note it's OK if this is a uselist=True attribute, the empty
+ # collection will be populated
+ state.get_impl(self.key).set_committed_value(state, dict_, None)
+
+ def _load_via_parent(
+ self, our_states, query_info, q, context, execution_options
+ ):
+ uselist = self.uselist
+ _empty_result = () if uselist else None
+
+ while our_states:
+ chunk = our_states[0 : self._chunksize]
+ our_states = our_states[self._chunksize :]
+
+ primary_keys = [
+ key[0] if query_info.zero_idx else key
+ for key, state, state_dict, overwrite in chunk
+ ]
+
+ data = collections.defaultdict(list)
+ for k, v in itertools.groupby(
+ context.session.execute(
+ q,
+ params={"primary_keys": primary_keys},
+ execution_options=execution_options,
+ ).unique(),
+ lambda x: x[0],
+ ):
+ data[k].extend(vv[1] for vv in v)
+
+ for key, state, state_dict, overwrite in chunk:
+ if not overwrite and self.key in state_dict:
+ continue
+
+ collection = data.get(key, _empty_result)
+
+ if not uselist and collection:
+ if len(collection) > 1:
+ util.warn(
+ "Multiple rows returned with "
+ "uselist=False for eagerly-loaded "
+ "attribute '%s' " % self
+ )
+ state.get_impl(self.key).set_committed_value(
+ state, state_dict, collection[0]
+ )
+ else:
+ # note that empty tuple set on uselist=False sets the
+ # value to None
+ state.get_impl(self.key).set_committed_value(
+ state, state_dict, collection
+ )
+
+
+def single_parent_validator(desc, prop):
+ def _do_check(state, value, oldvalue, initiator):
+ if value is not None and initiator.key == prop.key:
+ hasparent = initiator.hasparent(attributes.instance_state(value))
+ if hasparent and oldvalue is not value:
+ raise sa_exc.InvalidRequestError(
+ "Instance %s is already associated with an instance "
+ "of %s via its %s attribute, and is only allowed a "
+ "single parent."
+ % (orm_util.instance_str(value), state.class_, prop),
+ code="bbf1",
+ )
+ return value
+
+ def append(state, value, initiator):
+ return _do_check(state, value, None, initiator)
+
+ def set_(state, value, oldvalue, initiator):
+ return _do_check(state, value, oldvalue, initiator)
+
+ event.listen(
+ desc, "append", append, raw=True, retval=True, active_history=True
+ )
+ event.listen(desc, "set", set_, raw=True, retval=True, active_history=True)
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/strategy_options.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/strategy_options.py
new file mode 100644
index 0000000..25c6332
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/strategy_options.py
@@ -0,0 +1,2555 @@
+# orm/strategy_options.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: allow-untyped-defs, allow-untyped-calls
+
+"""
+
+"""
+
+from __future__ import annotations
+
+import typing
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Iterable
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TypeVar
+from typing import Union
+
+from . import util as orm_util
+from ._typing import insp_is_aliased_class
+from ._typing import insp_is_attribute
+from ._typing import insp_is_mapper
+from ._typing import insp_is_mapper_property
+from .attributes import QueryableAttribute
+from .base import InspectionAttr
+from .interfaces import LoaderOption
+from .path_registry import _DEFAULT_TOKEN
+from .path_registry import _StrPathToken
+from .path_registry import _WILDCARD_TOKEN
+from .path_registry import AbstractEntityRegistry
+from .path_registry import path_is_property
+from .path_registry import PathRegistry
+from .path_registry import TokenRegistry
+from .util import _orm_full_deannotate
+from .util import AliasedInsp
+from .. import exc as sa_exc
+from .. import inspect
+from .. import util
+from ..sql import and_
+from ..sql import cache_key
+from ..sql import coercions
+from ..sql import roles
+from ..sql import traversals
+from ..sql import visitors
+from ..sql.base import _generative
+from ..util.typing import Final
+from ..util.typing import Literal
+from ..util.typing import Self
+
+_RELATIONSHIP_TOKEN: Final[Literal["relationship"]] = "relationship"
+_COLUMN_TOKEN: Final[Literal["column"]] = "column"
+
+_FN = TypeVar("_FN", bound="Callable[..., Any]")
+
+if typing.TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _InternalEntityType
+ from .context import _MapperEntity
+ from .context import ORMCompileState
+ from .context import QueryContext
+ from .interfaces import _StrategyKey
+ from .interfaces import MapperProperty
+ from .interfaces import ORMOption
+ from .mapper import Mapper
+ from .path_registry import _PathRepresentation
+ from ..sql._typing import _ColumnExpressionArgument
+ from ..sql._typing import _FromClauseArgument
+ from ..sql.cache_key import _CacheKeyTraversalType
+ from ..sql.cache_key import CacheKey
+
+
+_AttrType = Union[Literal["*"], "QueryableAttribute[Any]"]
+
+_WildcardKeyType = Literal["relationship", "column"]
+_StrategySpec = Dict[str, Any]
+_OptsType = Dict[str, Any]
+_AttrGroupType = Tuple[_AttrType, ...]
+
+
+class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption):
+ __slots__ = ("propagate_to_loaders",)
+
+ _is_strategy_option = True
+ propagate_to_loaders: bool
+
+ def contains_eager(
+ self,
+ attr: _AttrType,
+ alias: Optional[_FromClauseArgument] = None,
+ _is_chain: bool = False,
+ ) -> Self:
+ r"""Indicate that the given attribute should be eagerly loaded from
+ columns stated manually in the query.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ The option is used in conjunction with an explicit join that loads
+ the desired rows, i.e.::
+
+ sess.query(Order).join(Order.user).options(
+ contains_eager(Order.user)
+ )
+
+ The above query would join from the ``Order`` entity to its related
+ ``User`` entity, and the returned ``Order`` objects would have the
+ ``Order.user`` attribute pre-populated.
+
+ It may also be used for customizing the entries in an eagerly loaded
+ collection; queries will normally want to use the
+ :ref:`orm_queryguide_populate_existing` execution option assuming the
+ primary collection of parent objects may already have been loaded::
+
+ sess.query(User).join(User.addresses).filter(
+ Address.email_address.like("%@aol.com")
+ ).options(contains_eager(User.addresses)).populate_existing()
+
+ See the section :ref:`contains_eager` for complete usage details.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`contains_eager`
+
+ """
+ if alias is not None:
+ if not isinstance(alias, str):
+ coerced_alias = coercions.expect(roles.FromClauseRole, alias)
+ else:
+ util.warn_deprecated(
+ "Passing a string name for the 'alias' argument to "
+ "'contains_eager()` is deprecated, and will not work in a "
+ "future release. Please use a sqlalchemy.alias() or "
+ "sqlalchemy.orm.aliased() construct.",
+ version="1.4",
+ )
+ coerced_alias = alias
+
+ elif getattr(attr, "_of_type", None):
+ assert isinstance(attr, QueryableAttribute)
+ ot: Optional[_InternalEntityType[Any]] = inspect(attr._of_type)
+ assert ot is not None
+ coerced_alias = ot.selectable
+ else:
+ coerced_alias = None
+
+ cloned = self._set_relationship_strategy(
+ attr,
+ {"lazy": "joined"},
+ propagate_to_loaders=False,
+ opts={"eager_from_alias": coerced_alias},
+ _reconcile_to_other=True if _is_chain else None,
+ )
+ return cloned
+
+ def load_only(self, *attrs: _AttrType, raiseload: bool = False) -> Self:
+ r"""Indicate that for a particular entity, only the given list
+ of column-based attribute names should be loaded; all others will be
+ deferred.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ Example - given a class ``User``, load only the ``name`` and
+ ``fullname`` attributes::
+
+ session.query(User).options(load_only(User.name, User.fullname))
+
+ Example - given a relationship ``User.addresses -> Address``, specify
+ subquery loading for the ``User.addresses`` collection, but on each
+ ``Address`` object load only the ``email_address`` attribute::
+
+ session.query(User).options(
+ subqueryload(User.addresses).load_only(Address.email_address)
+ )
+
+ For a statement that has multiple entities,
+ the lead entity can be
+ specifically referred to using the :class:`_orm.Load` constructor::
+
+ stmt = (
+ select(User, Address)
+ .join(User.addresses)
+ .options(
+ Load(User).load_only(User.name, User.fullname),
+ Load(Address).load_only(Address.email_address),
+ )
+ )
+
+ When used together with the
+ :ref:`populate_existing <orm_queryguide_populate_existing>`
+ execution option only the attributes listed will be refreshed.
+
+ :param \*attrs: Attributes to be loaded, all others will be deferred.
+
+ :param raiseload: raise :class:`.InvalidRequestError` rather than
+ lazy loading a value when a deferred attribute is accessed. Used
+ to prevent unwanted SQL from being emitted.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`orm_queryguide_column_deferral` - in the
+ :ref:`queryguide_toplevel`
+
+ :param \*attrs: Attributes to be loaded, all others will be deferred.
+
+ :param raiseload: raise :class:`.InvalidRequestError` rather than
+ lazy loading a value when a deferred attribute is accessed. Used
+ to prevent unwanted SQL from being emitted.
+
+ .. versionadded:: 2.0
+
+ """
+ cloned = self._set_column_strategy(
+ attrs,
+ {"deferred": False, "instrument": True},
+ )
+
+ wildcard_strategy = {"deferred": True, "instrument": True}
+ if raiseload:
+ wildcard_strategy["raiseload"] = True
+
+ cloned = cloned._set_column_strategy(
+ ("*",),
+ wildcard_strategy,
+ )
+ return cloned
+
+ def joinedload(
+ self,
+ attr: _AttrType,
+ innerjoin: Optional[bool] = None,
+ ) -> Self:
+ """Indicate that the given attribute should be loaded using joined
+ eager loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ examples::
+
+ # joined-load the "orders" collection on "User"
+ select(User).options(joinedload(User.orders))
+
+ # joined-load Order.items and then Item.keywords
+ select(Order).options(
+ joinedload(Order.items).joinedload(Item.keywords)
+ )
+
+ # lazily load Order.items, but when Items are loaded,
+ # joined-load the keywords collection
+ select(Order).options(
+ lazyload(Order.items).joinedload(Item.keywords)
+ )
+
+ :param innerjoin: if ``True``, indicates that the joined eager load
+ should use an inner join instead of the default of left outer join::
+
+ select(Order).options(joinedload(Order.user, innerjoin=True))
+
+ In order to chain multiple eager joins together where some may be
+ OUTER and others INNER, right-nested joins are used to link them::
+
+ select(A).options(
+ joinedload(A.bs, innerjoin=False).joinedload(
+ B.cs, innerjoin=True
+ )
+ )
+
+ The above query, linking A.bs via "outer" join and B.cs via "inner"
+ join would render the joins as "a LEFT OUTER JOIN (b JOIN c)". When
+ using older versions of SQLite (< 3.7.16), this form of JOIN is
+ translated to use full subqueries as this syntax is otherwise not
+ directly supported.
+
+ The ``innerjoin`` flag can also be stated with the term ``"unnested"``.
+ This indicates that an INNER JOIN should be used, *unless* the join
+ is linked to a LEFT OUTER JOIN to the left, in which case it
+ will render as LEFT OUTER JOIN. For example, supposing ``A.bs``
+ is an outerjoin::
+
+ select(A).options(
+ joinedload(A.bs).joinedload(B.cs, innerjoin="unnested")
+ )
+
+
+ The above join will render as "a LEFT OUTER JOIN b LEFT OUTER JOIN c",
+ rather than as "a LEFT OUTER JOIN (b JOIN c)".
+
+ .. note:: The "unnested" flag does **not** affect the JOIN rendered
+ from a many-to-many association table, e.g. a table configured as
+ :paramref:`_orm.relationship.secondary`, to the target table; for
+ correctness of results, these joins are always INNER and are
+ therefore right-nested if linked to an OUTER join.
+
+ .. note::
+
+ The joins produced by :func:`_orm.joinedload` are **anonymously
+ aliased**. The criteria by which the join proceeds cannot be
+ modified, nor can the ORM-enabled :class:`_sql.Select` or legacy
+ :class:`_query.Query` refer to these joins in any way, including
+ ordering. See :ref:`zen_of_eager_loading` for further detail.
+
+ To produce a specific SQL JOIN which is explicitly available, use
+ :meth:`_sql.Select.join` and :meth:`_query.Query.join`. To combine
+ explicit JOINs with eager loading of collections, use
+ :func:`_orm.contains_eager`; see :ref:`contains_eager`.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`joined_eager_loading`
+
+ """
+ loader = self._set_relationship_strategy(
+ attr,
+ {"lazy": "joined"},
+ opts=(
+ {"innerjoin": innerjoin}
+ if innerjoin is not None
+ else util.EMPTY_DICT
+ ),
+ )
+ return loader
+
+ def subqueryload(self, attr: _AttrType) -> Self:
+ """Indicate that the given attribute should be loaded using
+ subquery eager loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ examples::
+
+ # subquery-load the "orders" collection on "User"
+ select(User).options(subqueryload(User.orders))
+
+ # subquery-load Order.items and then Item.keywords
+ select(Order).options(
+ subqueryload(Order.items).subqueryload(Item.keywords)
+ )
+
+ # lazily load Order.items, but when Items are loaded,
+ # subquery-load the keywords collection
+ select(Order).options(
+ lazyload(Order.items).subqueryload(Item.keywords)
+ )
+
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`subquery_eager_loading`
+
+ """
+ return self._set_relationship_strategy(attr, {"lazy": "subquery"})
+
+ def selectinload(
+ self,
+ attr: _AttrType,
+ recursion_depth: Optional[int] = None,
+ ) -> Self:
+ """Indicate that the given attribute should be loaded using
+ SELECT IN eager loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ examples::
+
+ # selectin-load the "orders" collection on "User"
+ select(User).options(selectinload(User.orders))
+
+ # selectin-load Order.items and then Item.keywords
+ select(Order).options(
+ selectinload(Order.items).selectinload(Item.keywords)
+ )
+
+ # lazily load Order.items, but when Items are loaded,
+ # selectin-load the keywords collection
+ select(Order).options(
+ lazyload(Order.items).selectinload(Item.keywords)
+ )
+
+ :param recursion_depth: optional int; when set to a positive integer
+ in conjunction with a self-referential relationship,
+ indicates "selectin" loading will continue that many levels deep
+ automatically until no items are found.
+
+ .. note:: The :paramref:`_orm.selectinload.recursion_depth` option
+ currently supports only self-referential relationships. There
+ is not yet an option to automatically traverse recursive structures
+ with more than one relationship involved.
+
+ Additionally, the :paramref:`_orm.selectinload.recursion_depth`
+ parameter is new and experimental and should be treated as "alpha"
+ status for the 2.0 series.
+
+ .. versionadded:: 2.0 added
+ :paramref:`_orm.selectinload.recursion_depth`
+
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`selectin_eager_loading`
+
+ """
+ return self._set_relationship_strategy(
+ attr,
+ {"lazy": "selectin"},
+ opts={"recursion_depth": recursion_depth},
+ )
+
+ def lazyload(self, attr: _AttrType) -> Self:
+ """Indicate that the given attribute should be loaded using "lazy"
+ loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`lazy_loading`
+
+ """
+ return self._set_relationship_strategy(attr, {"lazy": "select"})
+
+ def immediateload(
+ self,
+ attr: _AttrType,
+ recursion_depth: Optional[int] = None,
+ ) -> Self:
+ """Indicate that the given attribute should be loaded using
+ an immediate load with a per-attribute SELECT statement.
+
+ The load is achieved using the "lazyloader" strategy and does not
+ fire off any additional eager loaders.
+
+ The :func:`.immediateload` option is superseded in general
+ by the :func:`.selectinload` option, which performs the same task
+ more efficiently by emitting a SELECT for all loaded objects.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ :param recursion_depth: optional int; when set to a positive integer
+ in conjunction with a self-referential relationship,
+ indicates "selectin" loading will continue that many levels deep
+ automatically until no items are found.
+
+ .. note:: The :paramref:`_orm.immediateload.recursion_depth` option
+ currently supports only self-referential relationships. There
+ is not yet an option to automatically traverse recursive structures
+ with more than one relationship involved.
+
+ .. warning:: This parameter is new and experimental and should be
+ treated as "alpha" status
+
+ .. versionadded:: 2.0 added
+ :paramref:`_orm.immediateload.recursion_depth`
+
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`selectin_eager_loading`
+
+ """
+ loader = self._set_relationship_strategy(
+ attr,
+ {"lazy": "immediate"},
+ opts={"recursion_depth": recursion_depth},
+ )
+ return loader
+
+ def noload(self, attr: _AttrType) -> Self:
+ """Indicate that the given relationship attribute should remain
+ unloaded.
+
+ The relationship attribute will return ``None`` when accessed without
+ producing any loading effect.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ :func:`_orm.noload` applies to :func:`_orm.relationship` attributes
+ only.
+
+ .. note:: Setting this loading strategy as the default strategy
+ for a relationship using the :paramref:`.orm.relationship.lazy`
+ parameter may cause issues with flushes, such if a delete operation
+ needs to load related objects and instead ``None`` was returned.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ """
+
+ return self._set_relationship_strategy(attr, {"lazy": "noload"})
+
+ def raiseload(self, attr: _AttrType, sql_only: bool = False) -> Self:
+ """Indicate that the given attribute should raise an error if accessed.
+
+ A relationship attribute configured with :func:`_orm.raiseload` will
+ raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The
+ typical way this is useful is when an application is attempting to
+ ensure that all relationship attributes that are accessed in a
+ particular context would have been already loaded via eager loading.
+ Instead of having to read through SQL logs to ensure lazy loads aren't
+ occurring, this strategy will cause them to raise immediately.
+
+ :func:`_orm.raiseload` applies to :func:`_orm.relationship` attributes
+ only. In order to apply raise-on-SQL behavior to a column-based
+ attribute, use the :paramref:`.orm.defer.raiseload` parameter on the
+ :func:`.defer` loader option.
+
+ :param sql_only: if True, raise only if the lazy load would emit SQL,
+ but not if it is only checking the identity map, or determining that
+ the related value should just be None due to missing keys. When False,
+ the strategy will raise for all varieties of relationship loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`prevent_lazy_with_raiseload`
+
+ :ref:`orm_queryguide_deferred_raiseload`
+
+ """
+
+ return self._set_relationship_strategy(
+ attr, {"lazy": "raise_on_sql" if sql_only else "raise"}
+ )
+
+ def defaultload(self, attr: _AttrType) -> Self:
+ """Indicate an attribute should load using its predefined loader style.
+
+ The behavior of this loading option is to not change the current
+ loading style of the attribute, meaning that the previously configured
+ one is used or, if no previous style was selected, the default
+ loading will be used.
+
+ This method is used to link to other loader options further into
+ a chain of attributes without altering the loader style of the links
+ along the chain. For example, to set joined eager loading for an
+ element of an element::
+
+ session.query(MyClass).options(
+ defaultload(MyClass.someattribute).joinedload(
+ MyOtherClass.someotherattribute
+ )
+ )
+
+ :func:`.defaultload` is also useful for setting column-level options on
+ a related class, namely that of :func:`.defer` and :func:`.undefer`::
+
+ session.scalars(
+ select(MyClass).options(
+ defaultload(MyClass.someattribute)
+ .defer("some_column")
+ .undefer("some_other_column")
+ )
+ )
+
+ .. seealso::
+
+ :ref:`orm_queryguide_relationship_sub_options`
+
+ :meth:`_orm.Load.options`
+
+ """
+ return self._set_relationship_strategy(attr, None)
+
+ def defer(self, key: _AttrType, raiseload: bool = False) -> Self:
+ r"""Indicate that the given column-oriented attribute should be
+ deferred, e.g. not loaded until accessed.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ e.g.::
+
+ from sqlalchemy.orm import defer
+
+ session.query(MyClass).options(
+ defer(MyClass.attribute_one),
+ defer(MyClass.attribute_two)
+ )
+
+ To specify a deferred load of an attribute on a related class,
+ the path can be specified one token at a time, specifying the loading
+ style for each link along the chain. To leave the loading style
+ for a link unchanged, use :func:`_orm.defaultload`::
+
+ session.query(MyClass).options(
+ defaultload(MyClass.someattr).defer(RelatedClass.some_column)
+ )
+
+ Multiple deferral options related to a relationship can be bundled
+ at once using :meth:`_orm.Load.options`::
+
+
+ select(MyClass).options(
+ defaultload(MyClass.someattr).options(
+ defer(RelatedClass.some_column),
+ defer(RelatedClass.some_other_column),
+ defer(RelatedClass.another_column)
+ )
+ )
+
+ :param key: Attribute to be deferred.
+
+ :param raiseload: raise :class:`.InvalidRequestError` rather than
+ lazy loading a value when the deferred attribute is accessed. Used
+ to prevent unwanted SQL from being emitted.
+
+ .. versionadded:: 1.4
+
+ .. seealso::
+
+ :ref:`orm_queryguide_column_deferral` - in the
+ :ref:`queryguide_toplevel`
+
+ :func:`_orm.load_only`
+
+ :func:`_orm.undefer`
+
+ """
+ strategy = {"deferred": True, "instrument": True}
+ if raiseload:
+ strategy["raiseload"] = True
+ return self._set_column_strategy((key,), strategy)
+
+ def undefer(self, key: _AttrType) -> Self:
+ r"""Indicate that the given column-oriented attribute should be
+ undeferred, e.g. specified within the SELECT statement of the entity
+ as a whole.
+
+ The column being undeferred is typically set up on the mapping as a
+ :func:`.deferred` attribute.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ Examples::
+
+ # undefer two columns
+ session.query(MyClass).options(
+ undefer(MyClass.col1), undefer(MyClass.col2)
+ )
+
+ # undefer all columns specific to a single class using Load + *
+ session.query(MyClass, MyOtherClass).options(
+ Load(MyClass).undefer("*")
+ )
+
+ # undefer a column on a related object
+ select(MyClass).options(
+ defaultload(MyClass.items).undefer(MyClass.text)
+ )
+
+ :param key: Attribute to be undeferred.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_column_deferral` - in the
+ :ref:`queryguide_toplevel`
+
+ :func:`_orm.defer`
+
+ :func:`_orm.undefer_group`
+
+ """
+ return self._set_column_strategy(
+ (key,), {"deferred": False, "instrument": True}
+ )
+
+ def undefer_group(self, name: str) -> Self:
+ """Indicate that columns within the given deferred group name should be
+ undeferred.
+
+ The columns being undeferred are set up on the mapping as
+ :func:`.deferred` attributes and include a "group" name.
+
+ E.g::
+
+ session.query(MyClass).options(undefer_group("large_attrs"))
+
+ To undefer a group of attributes on a related entity, the path can be
+ spelled out using relationship loader options, such as
+ :func:`_orm.defaultload`::
+
+ select(MyClass).options(
+ defaultload("someattr").undefer_group("large_attrs")
+ )
+
+ .. seealso::
+
+ :ref:`orm_queryguide_column_deferral` - in the
+ :ref:`queryguide_toplevel`
+
+ :func:`_orm.defer`
+
+ :func:`_orm.undefer`
+
+ """
+ return self._set_column_strategy(
+ (_WILDCARD_TOKEN,), None, {f"undefer_group_{name}": True}
+ )
+
+ def with_expression(
+ self,
+ key: _AttrType,
+ expression: _ColumnExpressionArgument[Any],
+ ) -> Self:
+ r"""Apply an ad-hoc SQL expression to a "deferred expression"
+ attribute.
+
+ This option is used in conjunction with the
+ :func:`_orm.query_expression` mapper-level construct that indicates an
+ attribute which should be the target of an ad-hoc SQL expression.
+
+ E.g.::
+
+ stmt = select(SomeClass).options(
+ with_expression(SomeClass.x_y_expr, SomeClass.x + SomeClass.y)
+ )
+
+ .. versionadded:: 1.2
+
+ :param key: Attribute to be populated
+
+ :param expr: SQL expression to be applied to the attribute.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_with_expression` - background and usage
+ examples
+
+ """
+
+ expression = _orm_full_deannotate(
+ coercions.expect(roles.LabeledColumnExprRole, expression)
+ )
+
+ return self._set_column_strategy(
+ (key,), {"query_expression": True}, extra_criteria=(expression,)
+ )
+
+ def selectin_polymorphic(self, classes: Iterable[Type[Any]]) -> Self:
+ """Indicate an eager load should take place for all attributes
+ specific to a subclass.
+
+ This uses an additional SELECT with IN against all matched primary
+ key values, and is the per-query analogue to the ``"selectin"``
+ setting on the :paramref:`.mapper.polymorphic_load` parameter.
+
+ .. versionadded:: 1.2
+
+ .. seealso::
+
+ :ref:`polymorphic_selectin`
+
+ """
+ self = self._set_class_strategy(
+ {"selectinload_polymorphic": True},
+ opts={
+ "entities": tuple(
+ sorted((inspect(cls) for cls in classes), key=id)
+ )
+ },
+ )
+ return self
+
+ @overload
+ def _coerce_strat(self, strategy: _StrategySpec) -> _StrategyKey: ...
+
+ @overload
+ def _coerce_strat(self, strategy: Literal[None]) -> None: ...
+
+ def _coerce_strat(
+ self, strategy: Optional[_StrategySpec]
+ ) -> Optional[_StrategyKey]:
+ if strategy is not None:
+ strategy_key = tuple(sorted(strategy.items()))
+ else:
+ strategy_key = None
+ return strategy_key
+
+ @_generative
+ def _set_relationship_strategy(
+ self,
+ attr: _AttrType,
+ strategy: Optional[_StrategySpec],
+ propagate_to_loaders: bool = True,
+ opts: Optional[_OptsType] = None,
+ _reconcile_to_other: Optional[bool] = None,
+ ) -> Self:
+ strategy_key = self._coerce_strat(strategy)
+
+ self._clone_for_bind_strategy(
+ (attr,),
+ strategy_key,
+ _RELATIONSHIP_TOKEN,
+ opts=opts,
+ propagate_to_loaders=propagate_to_loaders,
+ reconcile_to_other=_reconcile_to_other,
+ )
+ return self
+
+ @_generative
+ def _set_column_strategy(
+ self,
+ attrs: Tuple[_AttrType, ...],
+ strategy: Optional[_StrategySpec],
+ opts: Optional[_OptsType] = None,
+ extra_criteria: Optional[Tuple[Any, ...]] = None,
+ ) -> Self:
+ strategy_key = self._coerce_strat(strategy)
+
+ self._clone_for_bind_strategy(
+ attrs,
+ strategy_key,
+ _COLUMN_TOKEN,
+ opts=opts,
+ attr_group=attrs,
+ extra_criteria=extra_criteria,
+ )
+ return self
+
+ @_generative
+ def _set_generic_strategy(
+ self,
+ attrs: Tuple[_AttrType, ...],
+ strategy: _StrategySpec,
+ _reconcile_to_other: Optional[bool] = None,
+ ) -> Self:
+ strategy_key = self._coerce_strat(strategy)
+ self._clone_for_bind_strategy(
+ attrs,
+ strategy_key,
+ None,
+ propagate_to_loaders=True,
+ reconcile_to_other=_reconcile_to_other,
+ )
+ return self
+
+ @_generative
+ def _set_class_strategy(
+ self, strategy: _StrategySpec, opts: _OptsType
+ ) -> Self:
+ strategy_key = self._coerce_strat(strategy)
+
+ self._clone_for_bind_strategy(None, strategy_key, None, opts=opts)
+ return self
+
+ def _apply_to_parent(self, parent: Load) -> None:
+ """apply this :class:`_orm._AbstractLoad` object as a sub-option o
+ a :class:`_orm.Load` object.
+
+ Implementation is provided by subclasses.
+
+ """
+ raise NotImplementedError()
+
+ def options(self, *opts: _AbstractLoad) -> Self:
+ r"""Apply a series of options as sub-options to this
+ :class:`_orm._AbstractLoad` object.
+
+ Implementation is provided by subclasses.
+
+ """
+ raise NotImplementedError()
+
+ def _clone_for_bind_strategy(
+ self,
+ attrs: Optional[Tuple[_AttrType, ...]],
+ strategy: Optional[_StrategyKey],
+ wildcard_key: Optional[_WildcardKeyType],
+ opts: Optional[_OptsType] = None,
+ attr_group: Optional[_AttrGroupType] = None,
+ propagate_to_loaders: bool = True,
+ reconcile_to_other: Optional[bool] = None,
+ extra_criteria: Optional[Tuple[Any, ...]] = None,
+ ) -> Self:
+ raise NotImplementedError()
+
+ def process_compile_state_replaced_entities(
+ self,
+ compile_state: ORMCompileState,
+ mapper_entities: Sequence[_MapperEntity],
+ ) -> None:
+ if not compile_state.compile_options._enable_eagerloads:
+ return
+
+ # process is being run here so that the options given are validated
+ # against what the lead entities were, as well as to accommodate
+ # for the entities having been replaced with equivalents
+ self._process(
+ compile_state,
+ mapper_entities,
+ not bool(compile_state.current_path),
+ )
+
+ def process_compile_state(self, compile_state: ORMCompileState) -> None:
+ if not compile_state.compile_options._enable_eagerloads:
+ return
+
+ self._process(
+ compile_state,
+ compile_state._lead_mapper_entities,
+ not bool(compile_state.current_path)
+ and not compile_state.compile_options._for_refresh_state,
+ )
+
+ def _process(
+ self,
+ compile_state: ORMCompileState,
+ mapper_entities: Sequence[_MapperEntity],
+ raiseerr: bool,
+ ) -> None:
+ """implemented by subclasses"""
+ raise NotImplementedError()
+
+ @classmethod
+ def _chop_path(
+ cls,
+ to_chop: _PathRepresentation,
+ path: PathRegistry,
+ debug: bool = False,
+ ) -> Optional[_PathRepresentation]:
+ i = -1
+
+ for i, (c_token, p_token) in enumerate(
+ zip(to_chop, path.natural_path)
+ ):
+ if isinstance(c_token, str):
+ if i == 0 and (
+ c_token.endswith(f":{_DEFAULT_TOKEN}")
+ or c_token.endswith(f":{_WILDCARD_TOKEN}")
+ ):
+ return to_chop
+ elif (
+ c_token != f"{_RELATIONSHIP_TOKEN}:{_WILDCARD_TOKEN}"
+ and c_token != p_token.key # type: ignore
+ ):
+ return None
+
+ if c_token is p_token:
+ continue
+ elif (
+ isinstance(c_token, InspectionAttr)
+ and insp_is_mapper(c_token)
+ and insp_is_mapper(p_token)
+ and c_token.isa(p_token)
+ ):
+ continue
+
+ else:
+ return None
+ return to_chop[i + 1 :]
+
+
+class Load(_AbstractLoad):
+ """Represents loader options which modify the state of a
+ ORM-enabled :class:`_sql.Select` or a legacy :class:`_query.Query` in
+ order to affect how various mapped attributes are loaded.
+
+ The :class:`_orm.Load` object is in most cases used implicitly behind the
+ scenes when one makes use of a query option like :func:`_orm.joinedload`,
+ :func:`_orm.defer`, or similar. It typically is not instantiated directly
+ except for in some very specific cases.
+
+ .. seealso::
+
+ :ref:`orm_queryguide_relationship_per_entity_wildcard` - illustrates an
+ example where direct use of :class:`_orm.Load` may be useful
+
+ """
+
+ __slots__ = (
+ "path",
+ "context",
+ "additional_source_entities",
+ )
+
+ _traverse_internals = [
+ ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key),
+ (
+ "context",
+ visitors.InternalTraversal.dp_has_cache_key_list,
+ ),
+ ("propagate_to_loaders", visitors.InternalTraversal.dp_boolean),
+ (
+ "additional_source_entities",
+ visitors.InternalTraversal.dp_has_cache_key_list,
+ ),
+ ]
+ _cache_key_traversal = None
+
+ path: PathRegistry
+ context: Tuple[_LoadElement, ...]
+ additional_source_entities: Tuple[_InternalEntityType[Any], ...]
+
+ def __init__(self, entity: _EntityType[Any]):
+ insp = cast("Union[Mapper[Any], AliasedInsp[Any]]", inspect(entity))
+ insp._post_inspect
+
+ self.path = insp._path_registry
+ self.context = ()
+ self.propagate_to_loaders = False
+ self.additional_source_entities = ()
+
+ def __str__(self) -> str:
+ return f"Load({self.path[0]})"
+
+ @classmethod
+ def _construct_for_existing_path(
+ cls, path: AbstractEntityRegistry
+ ) -> Load:
+ load = cls.__new__(cls)
+ load.path = path
+ load.context = ()
+ load.propagate_to_loaders = False
+ load.additional_source_entities = ()
+ return load
+
+ def _adapt_cached_option_to_uncached_option(
+ self, context: QueryContext, uncached_opt: ORMOption
+ ) -> ORMOption:
+ if uncached_opt is self:
+ return self
+ return self._adjust_for_extra_criteria(context)
+
+ def _prepend_path(self, path: PathRegistry) -> Load:
+ cloned = self._clone()
+ cloned.context = tuple(
+ element._prepend_path(path) for element in self.context
+ )
+ return cloned
+
+ def _adjust_for_extra_criteria(self, context: QueryContext) -> Load:
+ """Apply the current bound parameters in a QueryContext to all
+ occurrences "extra_criteria" stored within this ``Load`` object,
+ returning a new instance of this ``Load`` object.
+
+ """
+
+ # avoid generating cache keys for the queries if we don't
+ # actually have any extra_criteria options, which is the
+ # common case
+ for value in self.context:
+ if value._extra_criteria:
+ break
+ else:
+ return self
+
+ replacement_cache_key = context.query._generate_cache_key()
+
+ if replacement_cache_key is None:
+ return self
+
+ orig_query = context.compile_state.select_statement
+ orig_cache_key = orig_query._generate_cache_key()
+ assert orig_cache_key is not None
+
+ def process(
+ opt: _LoadElement,
+ replacement_cache_key: CacheKey,
+ orig_cache_key: CacheKey,
+ ) -> _LoadElement:
+ cloned_opt = opt._clone()
+
+ cloned_opt._extra_criteria = tuple(
+ replacement_cache_key._apply_params_to_element(
+ orig_cache_key, crit
+ )
+ for crit in cloned_opt._extra_criteria
+ )
+
+ return cloned_opt
+
+ cloned = self._clone()
+ cloned.context = tuple(
+ (
+ process(value, replacement_cache_key, orig_cache_key)
+ if value._extra_criteria
+ else value
+ )
+ for value in self.context
+ )
+ return cloned
+
+ def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr):
+ """called at process time to allow adjustment of the root
+ entity inside of _LoadElement objects.
+
+ """
+ path = self.path
+
+ ezero = None
+ for ent in mapper_entities:
+ ezero = ent.entity_zero
+ if ezero and orm_util._entity_corresponds_to(
+ # technically this can be a token also, but this is
+ # safe to pass to _entity_corresponds_to()
+ ezero,
+ cast("_InternalEntityType[Any]", path[0]),
+ ):
+ return ezero
+
+ return None
+
+ def _process(
+ self,
+ compile_state: ORMCompileState,
+ mapper_entities: Sequence[_MapperEntity],
+ raiseerr: bool,
+ ) -> None:
+ reconciled_lead_entity = self._reconcile_query_entities_with_us(
+ mapper_entities, raiseerr
+ )
+
+ for loader in self.context:
+ loader.process_compile_state(
+ self,
+ compile_state,
+ mapper_entities,
+ reconciled_lead_entity,
+ raiseerr,
+ )
+
+ def _apply_to_parent(self, parent: Load) -> None:
+ """apply this :class:`_orm.Load` object as a sub-option of another
+ :class:`_orm.Load` object.
+
+ This method is used by the :meth:`_orm.Load.options` method.
+
+ """
+ cloned = self._generate()
+
+ assert cloned.propagate_to_loaders == self.propagate_to_loaders
+
+ if not any(
+ orm_util._entity_corresponds_to_use_path_impl(
+ elem, cloned.path.odd_element(0)
+ )
+ for elem in (parent.path.odd_element(-1),)
+ + parent.additional_source_entities
+ ):
+ if len(cloned.path) > 1:
+ attrname = cloned.path[1]
+ parent_entity = cloned.path[0]
+ else:
+ attrname = cloned.path[0]
+ parent_entity = cloned.path[0]
+ _raise_for_does_not_link(parent.path, attrname, parent_entity)
+
+ cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:])
+
+ if self.context:
+ cloned.context = tuple(
+ value._prepend_path_from(parent) for value in self.context
+ )
+
+ if cloned.context:
+ parent.context += cloned.context
+ parent.additional_source_entities += (
+ cloned.additional_source_entities
+ )
+
+ @_generative
+ def options(self, *opts: _AbstractLoad) -> Self:
+ r"""Apply a series of options as sub-options to this
+ :class:`_orm.Load`
+ object.
+
+ E.g.::
+
+ query = session.query(Author)
+ query = query.options(
+ joinedload(Author.book).options(
+ load_only(Book.summary, Book.excerpt),
+ joinedload(Book.citations).options(
+ joinedload(Citation.author)
+ )
+ )
+ )
+
+ :param \*opts: A series of loader option objects (ultimately
+ :class:`_orm.Load` objects) which should be applied to the path
+ specified by this :class:`_orm.Load` object.
+
+ .. versionadded:: 1.3.6
+
+ .. seealso::
+
+ :func:`.defaultload`
+
+ :ref:`orm_queryguide_relationship_sub_options`
+
+ """
+ for opt in opts:
+ try:
+ opt._apply_to_parent(self)
+ except AttributeError as ae:
+ if not isinstance(opt, _AbstractLoad):
+ raise sa_exc.ArgumentError(
+ f"Loader option {opt} is not compatible with the "
+ "Load.options() method."
+ ) from ae
+ else:
+ raise
+ return self
+
+ def _clone_for_bind_strategy(
+ self,
+ attrs: Optional[Tuple[_AttrType, ...]],
+ strategy: Optional[_StrategyKey],
+ wildcard_key: Optional[_WildcardKeyType],
+ opts: Optional[_OptsType] = None,
+ attr_group: Optional[_AttrGroupType] = None,
+ propagate_to_loaders: bool = True,
+ reconcile_to_other: Optional[bool] = None,
+ extra_criteria: Optional[Tuple[Any, ...]] = None,
+ ) -> Self:
+ # for individual strategy that needs to propagate, set the whole
+ # Load container to also propagate, so that it shows up in
+ # InstanceState.load_options
+ if propagate_to_loaders:
+ self.propagate_to_loaders = True
+
+ if self.path.is_token:
+ raise sa_exc.ArgumentError(
+ "Wildcard token cannot be followed by another entity"
+ )
+
+ elif path_is_property(self.path):
+ # re-use the lookup which will raise a nicely formatted
+ # LoaderStrategyException
+ if strategy:
+ self.path.prop._strategy_lookup(self.path.prop, strategy[0])
+ else:
+ raise sa_exc.ArgumentError(
+ f"Mapped attribute '{self.path.prop}' does not "
+ "refer to a mapped entity"
+ )
+
+ if attrs is None:
+ load_element = _ClassStrategyLoad.create(
+ self.path,
+ None,
+ strategy,
+ wildcard_key,
+ opts,
+ propagate_to_loaders,
+ attr_group=attr_group,
+ reconcile_to_other=reconcile_to_other,
+ extra_criteria=extra_criteria,
+ )
+ if load_element:
+ self.context += (load_element,)
+ assert opts is not None
+ self.additional_source_entities += cast(
+ "Tuple[_InternalEntityType[Any]]", opts["entities"]
+ )
+
+ else:
+ for attr in attrs:
+ if isinstance(attr, str):
+ load_element = _TokenStrategyLoad.create(
+ self.path,
+ attr,
+ strategy,
+ wildcard_key,
+ opts,
+ propagate_to_loaders,
+ attr_group=attr_group,
+ reconcile_to_other=reconcile_to_other,
+ extra_criteria=extra_criteria,
+ )
+ else:
+ load_element = _AttributeStrategyLoad.create(
+ self.path,
+ attr,
+ strategy,
+ wildcard_key,
+ opts,
+ propagate_to_loaders,
+ attr_group=attr_group,
+ reconcile_to_other=reconcile_to_other,
+ extra_criteria=extra_criteria,
+ )
+
+ if load_element:
+ # for relationship options, update self.path on this Load
+ # object with the latest path.
+ if wildcard_key is _RELATIONSHIP_TOKEN:
+ self.path = load_element.path
+ self.context += (load_element,)
+
+ # this seems to be effective for selectinloader,
+ # giving the extra match to one more level deep.
+ # but does not work for immediateloader, which still
+ # must add additional options at load time
+ if load_element.local_opts.get("recursion_depth", False):
+ r1 = load_element._recurse()
+ self.context += (r1,)
+
+ return self
+
+ def __getstate__(self):
+ d = self._shallow_to_dict()
+ d["path"] = self.path.serialize()
+ return d
+
+ def __setstate__(self, state):
+ state["path"] = PathRegistry.deserialize(state["path"])
+ self._shallow_from_dict(state)
+
+
+class _WildcardLoad(_AbstractLoad):
+ """represent a standalone '*' load operation"""
+
+ __slots__ = ("strategy", "path", "local_opts")
+
+ _traverse_internals = [
+ ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
+ ("path", visitors.ExtendedInternalTraversal.dp_plain_obj),
+ (
+ "local_opts",
+ visitors.ExtendedInternalTraversal.dp_string_multi_dict,
+ ),
+ ]
+ cache_key_traversal: _CacheKeyTraversalType = None
+
+ strategy: Optional[Tuple[Any, ...]]
+ local_opts: _OptsType
+ path: Union[Tuple[()], Tuple[str]]
+ propagate_to_loaders = False
+
+ def __init__(self) -> None:
+ self.path = ()
+ self.strategy = None
+ self.local_opts = util.EMPTY_DICT
+
+ def _clone_for_bind_strategy(
+ self,
+ attrs,
+ strategy,
+ wildcard_key,
+ opts=None,
+ attr_group=None,
+ propagate_to_loaders=True,
+ reconcile_to_other=None,
+ extra_criteria=None,
+ ):
+ assert attrs is not None
+ attr = attrs[0]
+ assert (
+ wildcard_key
+ and isinstance(attr, str)
+ and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN)
+ )
+
+ attr = f"{wildcard_key}:{attr}"
+
+ self.strategy = strategy
+ self.path = (attr,)
+ if opts:
+ self.local_opts = util.immutabledict(opts)
+
+ assert extra_criteria is None
+
+ def options(self, *opts: _AbstractLoad) -> Self:
+ raise NotImplementedError("Star option does not support sub-options")
+
+ def _apply_to_parent(self, parent: Load) -> None:
+ """apply this :class:`_orm._WildcardLoad` object as a sub-option of
+ a :class:`_orm.Load` object.
+
+ This method is used by the :meth:`_orm.Load.options` method. Note
+ that :class:`_orm.WildcardLoad` itself can't have sub-options, but
+ it may be used as the sub-option of a :class:`_orm.Load` object.
+
+ """
+ assert self.path
+ attr = self.path[0]
+ if attr.endswith(_DEFAULT_TOKEN):
+ attr = f"{attr.split(':')[0]}:{_WILDCARD_TOKEN}"
+
+ effective_path = cast(AbstractEntityRegistry, parent.path).token(attr)
+
+ assert effective_path.is_token
+
+ loader = _TokenStrategyLoad.create(
+ effective_path,
+ None,
+ self.strategy,
+ None,
+ self.local_opts,
+ self.propagate_to_loaders,
+ )
+
+ parent.context += (loader,)
+
+ def _process(self, compile_state, mapper_entities, raiseerr):
+ is_refresh = compile_state.compile_options._for_refresh_state
+
+ if is_refresh and not self.propagate_to_loaders:
+ return
+
+ entities = [ent.entity_zero for ent in mapper_entities]
+ current_path = compile_state.current_path
+
+ start_path: _PathRepresentation = self.path
+
+ if current_path:
+ # TODO: no cases in test suite where we actually get
+ # None back here
+ new_path = self._chop_path(start_path, current_path)
+ if new_path is None:
+ return
+
+ # chop_path does not actually "chop" a wildcard token path,
+ # just returns it
+ assert new_path == start_path
+
+ # start_path is a single-token tuple
+ assert start_path and len(start_path) == 1
+
+ token = start_path[0]
+ assert isinstance(token, str)
+ entity = self._find_entity_basestring(entities, token, raiseerr)
+
+ if not entity:
+ return
+
+ path_element = entity
+
+ # transfer our entity-less state into a Load() object
+ # with a real entity path. Start with the lead entity
+ # we just located, then go through the rest of our path
+ # tokens and populate into the Load().
+
+ assert isinstance(token, str)
+ loader = _TokenStrategyLoad.create(
+ path_element._path_registry,
+ token,
+ self.strategy,
+ None,
+ self.local_opts,
+ self.propagate_to_loaders,
+ raiseerr=raiseerr,
+ )
+ if not loader:
+ return
+
+ assert loader.path.is_token
+
+ # don't pass a reconciled lead entity here
+ loader.process_compile_state(
+ self, compile_state, mapper_entities, None, raiseerr
+ )
+
+ return loader
+
+ def _find_entity_basestring(
+ self,
+ entities: Iterable[_InternalEntityType[Any]],
+ token: str,
+ raiseerr: bool,
+ ) -> Optional[_InternalEntityType[Any]]:
+ if token.endswith(f":{_WILDCARD_TOKEN}"):
+ if len(list(entities)) != 1:
+ if raiseerr:
+ raise sa_exc.ArgumentError(
+ "Can't apply wildcard ('*') or load_only() "
+ f"loader option to multiple entities "
+ f"{', '.join(str(ent) for ent in entities)}. Specify "
+ "loader options for each entity individually, such as "
+ f"""{
+ ", ".join(
+ f"Load({ent}).some_option('*')"
+ for ent in entities
+ )
+ }."""
+ )
+ elif token.endswith(_DEFAULT_TOKEN):
+ raiseerr = False
+
+ for ent in entities:
+ # return only the first _MapperEntity when searching
+ # based on string prop name. Ideally object
+ # attributes are used to specify more exactly.
+ return ent
+ else:
+ if raiseerr:
+ raise sa_exc.ArgumentError(
+ "Query has only expression-based entities - "
+ f'can\'t find property named "{token}".'
+ )
+ else:
+ return None
+
+ def __getstate__(self) -> Dict[str, Any]:
+ d = self._shallow_to_dict()
+ return d
+
+ def __setstate__(self, state: Dict[str, Any]) -> None:
+ self._shallow_from_dict(state)
+
+
+class _LoadElement(
+ cache_key.HasCacheKey, traversals.HasShallowCopy, visitors.Traversible
+):
+ """represents strategy information to select for a LoaderStrategy
+ and pass options to it.
+
+ :class:`._LoadElement` objects provide the inner datastructure
+ stored by a :class:`_orm.Load` object and are also the object passed
+ to methods like :meth:`.LoaderStrategy.setup_query`.
+
+ .. versionadded:: 2.0
+
+ """
+
+ __slots__ = (
+ "path",
+ "strategy",
+ "propagate_to_loaders",
+ "local_opts",
+ "_extra_criteria",
+ "_reconcile_to_other",
+ )
+ __visit_name__ = "load_element"
+
+ _traverse_internals = [
+ ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key),
+ ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
+ (
+ "local_opts",
+ visitors.ExtendedInternalTraversal.dp_string_multi_dict,
+ ),
+ ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
+ ("propagate_to_loaders", visitors.InternalTraversal.dp_plain_obj),
+ ("_reconcile_to_other", visitors.InternalTraversal.dp_plain_obj),
+ ]
+ _cache_key_traversal = None
+
+ _extra_criteria: Tuple[Any, ...]
+
+ _reconcile_to_other: Optional[bool]
+ strategy: Optional[_StrategyKey]
+ path: PathRegistry
+ propagate_to_loaders: bool
+
+ local_opts: util.immutabledict[str, Any]
+
+ is_token_strategy: bool
+ is_class_strategy: bool
+
+ def __hash__(self) -> int:
+ return id(self)
+
+ def __eq__(self, other):
+ return traversals.compare(self, other)
+
+ @property
+ def is_opts_only(self) -> bool:
+ return bool(self.local_opts and self.strategy is None)
+
+ def _clone(self, **kw: Any) -> _LoadElement:
+ cls = self.__class__
+ s = cls.__new__(cls)
+
+ self._shallow_copy_to(s)
+ return s
+
+ def _update_opts(self, **kw: Any) -> _LoadElement:
+ new = self._clone()
+ new.local_opts = new.local_opts.union(kw)
+ return new
+
+ def __getstate__(self) -> Dict[str, Any]:
+ d = self._shallow_to_dict()
+ d["path"] = self.path.serialize()
+ return d
+
+ def __setstate__(self, state: Dict[str, Any]) -> None:
+ state["path"] = PathRegistry.deserialize(state["path"])
+ self._shallow_from_dict(state)
+
+ def _raise_for_no_match(self, parent_loader, mapper_entities):
+ path = parent_loader.path
+
+ found_entities = False
+ for ent in mapper_entities:
+ ezero = ent.entity_zero
+ if ezero:
+ found_entities = True
+ break
+
+ if not found_entities:
+ raise sa_exc.ArgumentError(
+ "Query has only expression-based entities; "
+ f"attribute loader options for {path[0]} can't "
+ "be applied here."
+ )
+ else:
+ raise sa_exc.ArgumentError(
+ f"Mapped class {path[0]} does not apply to any of the "
+ f"root entities in this query, e.g. "
+ f"""{
+ ", ".join(
+ str(x.entity_zero)
+ for x in mapper_entities if x.entity_zero
+ )}. Please """
+ "specify the full path "
+ "from one of the root entities to the target "
+ "attribute. "
+ )
+
+ def _adjust_effective_path_for_current_path(
+ self, effective_path: PathRegistry, current_path: PathRegistry
+ ) -> Optional[PathRegistry]:
+ """receives the 'current_path' entry from an :class:`.ORMCompileState`
+ instance, which is set during lazy loads and secondary loader strategy
+ loads, and adjusts the given path to be relative to the
+ current_path.
+
+ E.g. given a loader path and current path::
+
+ lp: User -> orders -> Order -> items -> Item -> keywords -> Keyword
+
+ cp: User -> orders -> Order -> items
+
+ The adjusted path would be::
+
+ Item -> keywords -> Keyword
+
+
+ """
+ chopped_start_path = Load._chop_path(
+ effective_path.natural_path, current_path
+ )
+ if not chopped_start_path:
+ return None
+
+ tokens_removed_from_start_path = len(effective_path) - len(
+ chopped_start_path
+ )
+
+ loader_lead_path_element = self.path[tokens_removed_from_start_path]
+
+ effective_path = PathRegistry.coerce(
+ (loader_lead_path_element,) + chopped_start_path[1:]
+ )
+
+ return effective_path
+
+ def _init_path(
+ self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
+ ):
+ """Apply ORM attributes and/or wildcard to an existing path, producing
+ a new path.
+
+ This method is used within the :meth:`.create` method to initialize
+ a :class:`._LoadElement` object.
+
+ """
+ raise NotImplementedError()
+
+ def _prepare_for_compile_state(
+ self,
+ parent_loader,
+ compile_state,
+ mapper_entities,
+ reconciled_lead_entity,
+ raiseerr,
+ ):
+ """implemented by subclasses."""
+ raise NotImplementedError()
+
+ def process_compile_state(
+ self,
+ parent_loader,
+ compile_state,
+ mapper_entities,
+ reconciled_lead_entity,
+ raiseerr,
+ ):
+ """populate ORMCompileState.attributes with loader state for this
+ _LoadElement.
+
+ """
+ keys = self._prepare_for_compile_state(
+ parent_loader,
+ compile_state,
+ mapper_entities,
+ reconciled_lead_entity,
+ raiseerr,
+ )
+ for key in keys:
+ if key in compile_state.attributes:
+ compile_state.attributes[key] = _LoadElement._reconcile(
+ self, compile_state.attributes[key]
+ )
+ else:
+ compile_state.attributes[key] = self
+
+ @classmethod
+ def create(
+ cls,
+ path: PathRegistry,
+ attr: Union[_AttrType, _StrPathToken, None],
+ strategy: Optional[_StrategyKey],
+ wildcard_key: Optional[_WildcardKeyType],
+ local_opts: Optional[_OptsType],
+ propagate_to_loaders: bool,
+ raiseerr: bool = True,
+ attr_group: Optional[_AttrGroupType] = None,
+ reconcile_to_other: Optional[bool] = None,
+ extra_criteria: Optional[Tuple[Any, ...]] = None,
+ ) -> _LoadElement:
+ """Create a new :class:`._LoadElement` object."""
+
+ opt = cls.__new__(cls)
+ opt.path = path
+ opt.strategy = strategy
+ opt.propagate_to_loaders = propagate_to_loaders
+ opt.local_opts = (
+ util.immutabledict(local_opts) if local_opts else util.EMPTY_DICT
+ )
+ opt._extra_criteria = ()
+
+ if reconcile_to_other is not None:
+ opt._reconcile_to_other = reconcile_to_other
+ elif strategy is None and not local_opts:
+ opt._reconcile_to_other = True
+ else:
+ opt._reconcile_to_other = None
+
+ path = opt._init_path(
+ path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
+ )
+
+ if not path:
+ return None # type: ignore
+
+ assert opt.is_token_strategy == path.is_token
+
+ opt.path = path
+ return opt
+
+ def __init__(self) -> None:
+ raise NotImplementedError()
+
+ def _recurse(self) -> _LoadElement:
+ cloned = self._clone()
+ cloned.path = PathRegistry.coerce(self.path[:] + self.path[-2:])
+
+ return cloned
+
+ def _prepend_path_from(self, parent: Load) -> _LoadElement:
+ """adjust the path of this :class:`._LoadElement` to be
+ a subpath of that of the given parent :class:`_orm.Load` object's
+ path.
+
+ This is used by the :meth:`_orm.Load._apply_to_parent` method,
+ which is in turn part of the :meth:`_orm.Load.options` method.
+
+ """
+
+ if not any(
+ orm_util._entity_corresponds_to_use_path_impl(
+ elem,
+ self.path.odd_element(0),
+ )
+ for elem in (parent.path.odd_element(-1),)
+ + parent.additional_source_entities
+ ):
+ raise sa_exc.ArgumentError(
+ f'Attribute "{self.path[1]}" does not link '
+ f'from element "{parent.path[-1]}".'
+ )
+
+ return self._prepend_path(parent.path)
+
+ def _prepend_path(self, path: PathRegistry) -> _LoadElement:
+ cloned = self._clone()
+
+ assert cloned.strategy == self.strategy
+ assert cloned.local_opts == self.local_opts
+ assert cloned.is_class_strategy == self.is_class_strategy
+
+ cloned.path = PathRegistry.coerce(path[0:-1] + cloned.path[:])
+
+ return cloned
+
+ @staticmethod
+ def _reconcile(
+ replacement: _LoadElement, existing: _LoadElement
+ ) -> _LoadElement:
+ """define behavior for when two Load objects are to be put into
+ the context.attributes under the same key.
+
+ :param replacement: ``_LoadElement`` that seeks to replace the
+ existing one
+
+ :param existing: ``_LoadElement`` that is already present.
+
+ """
+ # mapper inheritance loading requires fine-grained "block other
+ # options" / "allow these options to be overridden" behaviors
+ # see test_poly_loading.py
+
+ if replacement._reconcile_to_other:
+ return existing
+ elif replacement._reconcile_to_other is False:
+ return replacement
+ elif existing._reconcile_to_other:
+ return replacement
+ elif existing._reconcile_to_other is False:
+ return existing
+
+ if existing is replacement:
+ return replacement
+ elif (
+ existing.strategy == replacement.strategy
+ and existing.local_opts == replacement.local_opts
+ ):
+ return replacement
+ elif replacement.is_opts_only:
+ existing = existing._clone()
+ existing.local_opts = existing.local_opts.union(
+ replacement.local_opts
+ )
+ existing._extra_criteria += replacement._extra_criteria
+ return existing
+ elif existing.is_opts_only:
+ replacement = replacement._clone()
+ replacement.local_opts = replacement.local_opts.union(
+ existing.local_opts
+ )
+ replacement._extra_criteria += existing._extra_criteria
+ return replacement
+ elif replacement.path.is_token:
+ # use 'last one wins' logic for wildcard options. this is also
+ # kind of inconsistent vs. options that are specific paths which
+ # will raise as below
+ return replacement
+
+ raise sa_exc.InvalidRequestError(
+ f"Loader strategies for {replacement.path} conflict"
+ )
+
+
+class _AttributeStrategyLoad(_LoadElement):
+ """Loader strategies against specific relationship or column paths.
+
+ e.g.::
+
+ joinedload(User.addresses)
+ defer(Order.name)
+ selectinload(User.orders).lazyload(Order.items)
+
+ """
+
+ __slots__ = ("_of_type", "_path_with_polymorphic_path")
+
+ __visit_name__ = "attribute_strategy_load_element"
+
+ _traverse_internals = _LoadElement._traverse_internals + [
+ ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
+ (
+ "_path_with_polymorphic_path",
+ visitors.ExtendedInternalTraversal.dp_has_cache_key,
+ ),
+ ]
+
+ _of_type: Union[Mapper[Any], AliasedInsp[Any], None]
+ _path_with_polymorphic_path: Optional[PathRegistry]
+
+ is_class_strategy = False
+ is_token_strategy = False
+
+ def _init_path(
+ self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
+ ):
+ assert attr is not None
+ self._of_type = None
+ self._path_with_polymorphic_path = None
+ insp, _, prop = _parse_attr_argument(attr)
+
+ if insp.is_property:
+ # direct property can be sent from internal strategy logic
+ # that sets up specific loaders, such as
+ # emit_lazyload->_lazyload_reverse
+ # prop = found_property = attr
+ prop = attr
+ path = path[prop]
+
+ if path.has_entity:
+ path = path.entity_path
+ return path
+
+ elif not insp.is_attribute:
+ # should not reach here;
+ assert False
+
+ # here we assume we have user-passed InstrumentedAttribute
+ if not orm_util._entity_corresponds_to_use_path_impl(
+ path[-1], attr.parent
+ ):
+ if raiseerr:
+ if attr_group and attr is not attr_group[0]:
+ raise sa_exc.ArgumentError(
+ "Can't apply wildcard ('*') or load_only() "
+ "loader option to multiple entities in the "
+ "same option. Use separate options per entity."
+ )
+ else:
+ _raise_for_does_not_link(path, str(attr), attr.parent)
+ else:
+ return None
+
+ # note the essential logic of this attribute was very different in
+ # 1.4, where there were caching failures in e.g.
+ # test_relationship_criteria.py::RelationshipCriteriaTest::
+ # test_selectinload_nested_criteria[True] if an existing
+ # "_extra_criteria" on a Load object were replaced with that coming
+ # from an attribute. This appears to have been an artifact of how
+ # _UnboundLoad / Load interacted together, which was opaque and
+ # poorly defined.
+ if extra_criteria:
+ assert not attr._extra_criteria
+ self._extra_criteria = extra_criteria
+ else:
+ self._extra_criteria = attr._extra_criteria
+
+ if getattr(attr, "_of_type", None):
+ ac = attr._of_type
+ ext_info = inspect(ac)
+ self._of_type = ext_info
+
+ self._path_with_polymorphic_path = path.entity_path[prop]
+
+ path = path[prop][ext_info]
+
+ else:
+ path = path[prop]
+
+ if path.has_entity:
+ path = path.entity_path
+
+ return path
+
+ def _generate_extra_criteria(self, context):
+ """Apply the current bound parameters in a QueryContext to the
+ immediate "extra_criteria" stored with this Load object.
+
+ Load objects are typically pulled from the cached version of
+ the statement from a QueryContext. The statement currently being
+ executed will have new values (and keys) for bound parameters in the
+ extra criteria which need to be applied by loader strategies when
+ they handle this criteria for a result set.
+
+ """
+
+ assert (
+ self._extra_criteria
+ ), "this should only be called if _extra_criteria is present"
+
+ orig_query = context.compile_state.select_statement
+ current_query = context.query
+
+ # NOTE: while it seems like we should not do the "apply" operation
+ # here if orig_query is current_query, skipping it in the "optimized"
+ # case causes the query to be different from a cache key perspective,
+ # because we are creating a copy of the criteria which is no longer
+ # the same identity of the _extra_criteria in the loader option
+ # itself. cache key logic produces a different key for
+ # (A, copy_of_A) vs. (A, A), because in the latter case it shortens
+ # the second part of the key to just indicate on identity.
+
+ # if orig_query is current_query:
+ # not cached yet. just do the and_()
+ # return and_(*self._extra_criteria)
+
+ k1 = orig_query._generate_cache_key()
+ k2 = current_query._generate_cache_key()
+
+ return k2._apply_params_to_element(k1, and_(*self._extra_criteria))
+
+ def _set_of_type_info(self, context, current_path):
+ assert self._path_with_polymorphic_path
+
+ pwpi = self._of_type
+ assert pwpi
+ if not pwpi.is_aliased_class:
+ pwpi = inspect(
+ orm_util.AliasedInsp._with_polymorphic_factory(
+ pwpi.mapper.base_mapper,
+ (pwpi.mapper,),
+ aliased=True,
+ _use_mapper_path=True,
+ )
+ )
+ start_path = self._path_with_polymorphic_path
+ if current_path:
+ new_path = self._adjust_effective_path_for_current_path(
+ start_path, current_path
+ )
+ if new_path is None:
+ return
+ start_path = new_path
+
+ key = ("path_with_polymorphic", start_path.natural_path)
+ if key in context:
+ existing_aliased_insp = context[key]
+ this_aliased_insp = pwpi
+ new_aliased_insp = existing_aliased_insp._merge_with(
+ this_aliased_insp
+ )
+ context[key] = new_aliased_insp
+ else:
+ context[key] = pwpi
+
+ def _prepare_for_compile_state(
+ self,
+ parent_loader,
+ compile_state,
+ mapper_entities,
+ reconciled_lead_entity,
+ raiseerr,
+ ):
+ # _AttributeStrategyLoad
+
+ current_path = compile_state.current_path
+ is_refresh = compile_state.compile_options._for_refresh_state
+ assert not self.path.is_token
+
+ if is_refresh and not self.propagate_to_loaders:
+ return []
+
+ if self._of_type:
+ # apply additional with_polymorphic alias that may have been
+ # generated. this has to happen even if this is a defaultload
+ self._set_of_type_info(compile_state.attributes, current_path)
+
+ # omit setting loader attributes for a "defaultload" type of option
+ if not self.strategy and not self.local_opts:
+ return []
+
+ if raiseerr and not reconciled_lead_entity:
+ self._raise_for_no_match(parent_loader, mapper_entities)
+
+ if self.path.has_entity:
+ effective_path = self.path.parent
+ else:
+ effective_path = self.path
+
+ if current_path:
+ assert effective_path is not None
+ effective_path = self._adjust_effective_path_for_current_path(
+ effective_path, current_path
+ )
+ if effective_path is None:
+ return []
+
+ return [("loader", cast(PathRegistry, effective_path).natural_path)]
+
+ def __getstate__(self):
+ d = super().__getstate__()
+
+ # can't pickle this. See
+ # test_pickled.py -> test_lazyload_extra_criteria_not_supported
+ # where we should be emitting a warning for the usual case where this
+ # would be non-None
+ d["_extra_criteria"] = ()
+
+ if self._path_with_polymorphic_path:
+ d["_path_with_polymorphic_path"] = (
+ self._path_with_polymorphic_path.serialize()
+ )
+
+ if self._of_type:
+ if self._of_type.is_aliased_class:
+ d["_of_type"] = None
+ elif self._of_type.is_mapper:
+ d["_of_type"] = self._of_type.class_
+ else:
+ assert False, "unexpected object for _of_type"
+
+ return d
+
+ def __setstate__(self, state):
+ super().__setstate__(state)
+
+ if state.get("_path_with_polymorphic_path", None):
+ self._path_with_polymorphic_path = PathRegistry.deserialize(
+ state["_path_with_polymorphic_path"]
+ )
+ else:
+ self._path_with_polymorphic_path = None
+
+ if state.get("_of_type", None):
+ self._of_type = inspect(state["_of_type"])
+ else:
+ self._of_type = None
+
+
+class _TokenStrategyLoad(_LoadElement):
+ """Loader strategies against wildcard attributes
+
+ e.g.::
+
+ raiseload('*')
+ Load(User).lazyload('*')
+ defer('*')
+ load_only(User.name, User.email) # will create a defer('*')
+ joinedload(User.addresses).raiseload('*')
+
+ """
+
+ __visit_name__ = "token_strategy_load_element"
+
+ inherit_cache = True
+ is_class_strategy = False
+ is_token_strategy = True
+
+ def _init_path(
+ self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
+ ):
+ # assert isinstance(attr, str) or attr is None
+ if attr is not None:
+ default_token = attr.endswith(_DEFAULT_TOKEN)
+ if attr.endswith(_WILDCARD_TOKEN) or default_token:
+ if wildcard_key:
+ attr = f"{wildcard_key}:{attr}"
+
+ path = path.token(attr)
+ return path
+ else:
+ raise sa_exc.ArgumentError(
+ "Strings are not accepted for attribute names in loader "
+ "options; please use class-bound attributes directly."
+ )
+ return path
+
+ def _prepare_for_compile_state(
+ self,
+ parent_loader,
+ compile_state,
+ mapper_entities,
+ reconciled_lead_entity,
+ raiseerr,
+ ):
+ # _TokenStrategyLoad
+
+ current_path = compile_state.current_path
+ is_refresh = compile_state.compile_options._for_refresh_state
+
+ assert self.path.is_token
+
+ if is_refresh and not self.propagate_to_loaders:
+ return []
+
+ # omit setting attributes for a "defaultload" type of option
+ if not self.strategy and not self.local_opts:
+ return []
+
+ effective_path = self.path
+ if reconciled_lead_entity:
+ effective_path = PathRegistry.coerce(
+ (reconciled_lead_entity,) + effective_path.path[1:]
+ )
+
+ if current_path:
+ new_effective_path = self._adjust_effective_path_for_current_path(
+ effective_path, current_path
+ )
+ if new_effective_path is None:
+ return []
+ effective_path = new_effective_path
+
+ # for a wildcard token, expand out the path we set
+ # to encompass everything from the query entity on
+ # forward. not clear if this is necessary when current_path
+ # is set.
+
+ return [
+ ("loader", natural_path)
+ for natural_path in (
+ cast(
+ TokenRegistry, effective_path
+ )._generate_natural_for_superclasses()
+ )
+ ]
+
+
+class _ClassStrategyLoad(_LoadElement):
+ """Loader strategies that deals with a class as a target, not
+ an attribute path
+
+ e.g.::
+
+ q = s.query(Person).options(
+ selectin_polymorphic(Person, [Engineer, Manager])
+ )
+
+ """
+
+ inherit_cache = True
+ is_class_strategy = True
+ is_token_strategy = False
+
+ __visit_name__ = "class_strategy_load_element"
+
+ def _init_path(
+ self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
+ ):
+ return path
+
+ def _prepare_for_compile_state(
+ self,
+ parent_loader,
+ compile_state,
+ mapper_entities,
+ reconciled_lead_entity,
+ raiseerr,
+ ):
+ # _ClassStrategyLoad
+
+ current_path = compile_state.current_path
+ is_refresh = compile_state.compile_options._for_refresh_state
+
+ if is_refresh and not self.propagate_to_loaders:
+ return []
+
+ # omit setting attributes for a "defaultload" type of option
+ if not self.strategy and not self.local_opts:
+ return []
+
+ effective_path = self.path
+
+ if current_path:
+ new_effective_path = self._adjust_effective_path_for_current_path(
+ effective_path, current_path
+ )
+ if new_effective_path is None:
+ return []
+ effective_path = new_effective_path
+
+ return [("loader", effective_path.natural_path)]
+
+
+def _generate_from_keys(
+ meth: Callable[..., _AbstractLoad],
+ keys: Tuple[_AttrType, ...],
+ chained: bool,
+ kw: Any,
+) -> _AbstractLoad:
+ lead_element: Optional[_AbstractLoad] = None
+
+ attr: Any
+ for is_default, _keys in (True, keys[0:-1]), (False, keys[-1:]):
+ for attr in _keys:
+ if isinstance(attr, str):
+ if attr.startswith("." + _WILDCARD_TOKEN):
+ util.warn_deprecated(
+ "The undocumented `.{WILDCARD}` format is "
+ "deprecated "
+ "and will be removed in a future version as "
+ "it is "
+ "believed to be unused. "
+ "If you have been using this functionality, "
+ "please "
+ "comment on Issue #4390 on the SQLAlchemy project "
+ "tracker.",
+ version="1.4",
+ )
+ attr = attr[1:]
+
+ if attr == _WILDCARD_TOKEN:
+ if is_default:
+ raise sa_exc.ArgumentError(
+ "Wildcard token cannot be followed by "
+ "another entity",
+ )
+
+ if lead_element is None:
+ lead_element = _WildcardLoad()
+
+ lead_element = meth(lead_element, _DEFAULT_TOKEN, **kw)
+
+ else:
+ raise sa_exc.ArgumentError(
+ "Strings are not accepted for attribute names in "
+ "loader options; please use class-bound "
+ "attributes directly.",
+ )
+ else:
+ if lead_element is None:
+ _, lead_entity, _ = _parse_attr_argument(attr)
+ lead_element = Load(lead_entity)
+
+ if is_default:
+ if not chained:
+ lead_element = lead_element.defaultload(attr)
+ else:
+ lead_element = meth(
+ lead_element, attr, _is_chain=True, **kw
+ )
+ else:
+ lead_element = meth(lead_element, attr, **kw)
+
+ assert lead_element
+ return lead_element
+
+
+def _parse_attr_argument(
+ attr: _AttrType,
+) -> Tuple[InspectionAttr, _InternalEntityType[Any], MapperProperty[Any]]:
+ """parse an attribute or wildcard argument to produce an
+ :class:`._AbstractLoad` instance.
+
+ This is used by the standalone loader strategy functions like
+ ``joinedload()``, ``defer()``, etc. to produce :class:`_orm.Load` or
+ :class:`._WildcardLoad` objects.
+
+ """
+ try:
+ # TODO: need to figure out this None thing being returned by
+ # inspect(), it should not have None as an option in most cases
+ # if at all
+ insp: InspectionAttr = inspect(attr) # type: ignore
+ except sa_exc.NoInspectionAvailable as err:
+ raise sa_exc.ArgumentError(
+ "expected ORM mapped attribute for loader strategy argument"
+ ) from err
+
+ lead_entity: _InternalEntityType[Any]
+
+ if insp_is_mapper_property(insp):
+ lead_entity = insp.parent
+ prop = insp
+ elif insp_is_attribute(insp):
+ lead_entity = insp.parent
+ prop = insp.prop
+ else:
+ raise sa_exc.ArgumentError(
+ "expected ORM mapped attribute for loader strategy argument"
+ )
+
+ return insp, lead_entity, prop
+
+
+def loader_unbound_fn(fn: _FN) -> _FN:
+ """decorator that applies docstrings between standalone loader functions
+ and the loader methods on :class:`._AbstractLoad`.
+
+ """
+ bound_fn = getattr(_AbstractLoad, fn.__name__)
+ fn_doc = bound_fn.__doc__
+ bound_fn.__doc__ = f"""Produce a new :class:`_orm.Load` object with the
+:func:`_orm.{fn.__name__}` option applied.
+
+See :func:`_orm.{fn.__name__}` for usage examples.
+
+"""
+
+ fn.__doc__ = fn_doc
+ return fn
+
+
+# standalone functions follow. docstrings are filled in
+# by the ``@loader_unbound_fn`` decorator.
+
+
+@loader_unbound_fn
+def contains_eager(*keys: _AttrType, **kw: Any) -> _AbstractLoad:
+ return _generate_from_keys(Load.contains_eager, keys, True, kw)
+
+
+@loader_unbound_fn
+def load_only(*attrs: _AttrType, raiseload: bool = False) -> _AbstractLoad:
+ # TODO: attrs against different classes. we likely have to
+ # add some extra state to Load of some kind
+ _, lead_element, _ = _parse_attr_argument(attrs[0])
+ return Load(lead_element).load_only(*attrs, raiseload=raiseload)
+
+
+@loader_unbound_fn
+def joinedload(*keys: _AttrType, **kw: Any) -> _AbstractLoad:
+ return _generate_from_keys(Load.joinedload, keys, False, kw)
+
+
+@loader_unbound_fn
+def subqueryload(*keys: _AttrType) -> _AbstractLoad:
+ return _generate_from_keys(Load.subqueryload, keys, False, {})
+
+
+@loader_unbound_fn
+def selectinload(
+ *keys: _AttrType, recursion_depth: Optional[int] = None
+) -> _AbstractLoad:
+ return _generate_from_keys(
+ Load.selectinload, keys, False, {"recursion_depth": recursion_depth}
+ )
+
+
+@loader_unbound_fn
+def lazyload(*keys: _AttrType) -> _AbstractLoad:
+ return _generate_from_keys(Load.lazyload, keys, False, {})
+
+
+@loader_unbound_fn
+def immediateload(
+ *keys: _AttrType, recursion_depth: Optional[int] = None
+) -> _AbstractLoad:
+ return _generate_from_keys(
+ Load.immediateload, keys, False, {"recursion_depth": recursion_depth}
+ )
+
+
+@loader_unbound_fn
+def noload(*keys: _AttrType) -> _AbstractLoad:
+ return _generate_from_keys(Load.noload, keys, False, {})
+
+
+@loader_unbound_fn
+def raiseload(*keys: _AttrType, **kw: Any) -> _AbstractLoad:
+ return _generate_from_keys(Load.raiseload, keys, False, kw)
+
+
+@loader_unbound_fn
+def defaultload(*keys: _AttrType) -> _AbstractLoad:
+ return _generate_from_keys(Load.defaultload, keys, False, {})
+
+
+@loader_unbound_fn
+def defer(
+ key: _AttrType, *addl_attrs: _AttrType, raiseload: bool = False
+) -> _AbstractLoad:
+ if addl_attrs:
+ util.warn_deprecated(
+ "The *addl_attrs on orm.defer is deprecated. Please use "
+ "method chaining in conjunction with defaultload() to "
+ "indicate a path.",
+ version="1.3",
+ )
+
+ if raiseload:
+ kw = {"raiseload": raiseload}
+ else:
+ kw = {}
+
+ return _generate_from_keys(Load.defer, (key,) + addl_attrs, False, kw)
+
+
+@loader_unbound_fn
+def undefer(key: _AttrType, *addl_attrs: _AttrType) -> _AbstractLoad:
+ if addl_attrs:
+ util.warn_deprecated(
+ "The *addl_attrs on orm.undefer is deprecated. Please use "
+ "method chaining in conjunction with defaultload() to "
+ "indicate a path.",
+ version="1.3",
+ )
+ return _generate_from_keys(Load.undefer, (key,) + addl_attrs, False, {})
+
+
+@loader_unbound_fn
+def undefer_group(name: str) -> _AbstractLoad:
+ element = _WildcardLoad()
+ return element.undefer_group(name)
+
+
+@loader_unbound_fn
+def with_expression(
+ key: _AttrType, expression: _ColumnExpressionArgument[Any]
+) -> _AbstractLoad:
+ return _generate_from_keys(
+ Load.with_expression, (key,), False, {"expression": expression}
+ )
+
+
+@loader_unbound_fn
+def selectin_polymorphic(
+ base_cls: _EntityType[Any], classes: Iterable[Type[Any]]
+) -> _AbstractLoad:
+ ul = Load(base_cls)
+ return ul.selectin_polymorphic(classes)
+
+
+def _raise_for_does_not_link(path, attrname, parent_entity):
+ if len(path) > 1:
+ path_is_of_type = path[-1].entity is not path[-2].mapper.class_
+ if insp_is_aliased_class(parent_entity):
+ parent_entity_str = str(parent_entity)
+ else:
+ parent_entity_str = parent_entity.class_.__name__
+
+ raise sa_exc.ArgumentError(
+ f'ORM mapped entity or attribute "{attrname}" does not '
+ f'link from relationship "{path[-2]}%s".%s'
+ % (
+ f".of_type({path[-1]})" if path_is_of_type else "",
+ (
+ " Did you mean to use "
+ f'"{path[-2]}'
+ f'.of_type({parent_entity_str})" or "loadopt.options('
+ f"selectin_polymorphic({path[-2].mapper.class_.__name__}, "
+ f'[{parent_entity_str}]), ...)" ?'
+ if not path_is_of_type
+ and not path[-1].is_aliased_class
+ and orm_util._entity_corresponds_to(
+ path.entity, inspect(parent_entity).mapper
+ )
+ else ""
+ ),
+ )
+ )
+ else:
+ raise sa_exc.ArgumentError(
+ f'ORM mapped attribute "{attrname}" does not '
+ f'link mapped class "{path[-1]}"'
+ )
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/sync.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/sync.py
new file mode 100644
index 0000000..db09a3e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/sync.py
@@ -0,0 +1,164 @@
+# orm/sync.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: allow-untyped-defs, allow-untyped-calls
+
+
+"""private module containing functions used for copying data
+between instances based on join conditions.
+
+"""
+
+from __future__ import annotations
+
+from . import exc
+from . import util as orm_util
+from .base import PassiveFlag
+
+
+def populate(
+ source,
+ source_mapper,
+ dest,
+ dest_mapper,
+ synchronize_pairs,
+ uowcommit,
+ flag_cascaded_pks,
+):
+ source_dict = source.dict
+ dest_dict = dest.dict
+
+ for l, r in synchronize_pairs:
+ try:
+ # inline of source_mapper._get_state_attr_by_column
+ prop = source_mapper._columntoproperty[l]
+ value = source.manager[prop.key].impl.get(
+ source, source_dict, PassiveFlag.PASSIVE_OFF
+ )
+ except exc.UnmappedColumnError as err:
+ _raise_col_to_prop(False, source_mapper, l, dest_mapper, r, err)
+
+ try:
+ # inline of dest_mapper._set_state_attr_by_column
+ prop = dest_mapper._columntoproperty[r]
+ dest.manager[prop.key].impl.set(dest, dest_dict, value, None)
+ except exc.UnmappedColumnError as err:
+ _raise_col_to_prop(True, source_mapper, l, dest_mapper, r, err)
+
+ # technically the "r.primary_key" check isn't
+ # needed here, but we check for this condition to limit
+ # how often this logic is invoked for memory/performance
+ # reasons, since we only need this info for a primary key
+ # destination.
+ if (
+ flag_cascaded_pks
+ and l.primary_key
+ and r.primary_key
+ and r.references(l)
+ ):
+ uowcommit.attributes[("pk_cascaded", dest, r)] = True
+
+
+def bulk_populate_inherit_keys(source_dict, source_mapper, synchronize_pairs):
+ # a simplified version of populate() used by bulk insert mode
+ for l, r in synchronize_pairs:
+ try:
+ prop = source_mapper._columntoproperty[l]
+ value = source_dict[prop.key]
+ except exc.UnmappedColumnError as err:
+ _raise_col_to_prop(False, source_mapper, l, source_mapper, r, err)
+
+ try:
+ prop = source_mapper._columntoproperty[r]
+ source_dict[prop.key] = value
+ except exc.UnmappedColumnError as err:
+ _raise_col_to_prop(True, source_mapper, l, source_mapper, r, err)
+
+
+def clear(dest, dest_mapper, synchronize_pairs):
+ for l, r in synchronize_pairs:
+ if (
+ r.primary_key
+ and dest_mapper._get_state_attr_by_column(dest, dest.dict, r)
+ not in orm_util._none_set
+ ):
+ raise AssertionError(
+ f"Dependency rule on column '{l}' "
+ "tried to blank-out primary key "
+ f"column '{r}' on instance '{orm_util.state_str(dest)}'"
+ )
+ try:
+ dest_mapper._set_state_attr_by_column(dest, dest.dict, r, None)
+ except exc.UnmappedColumnError as err:
+ _raise_col_to_prop(True, None, l, dest_mapper, r, err)
+
+
+def update(source, source_mapper, dest, old_prefix, synchronize_pairs):
+ for l, r in synchronize_pairs:
+ try:
+ oldvalue = source_mapper._get_committed_attr_by_column(
+ source.obj(), l
+ )
+ value = source_mapper._get_state_attr_by_column(
+ source, source.dict, l, passive=PassiveFlag.PASSIVE_OFF
+ )
+ except exc.UnmappedColumnError as err:
+ _raise_col_to_prop(False, source_mapper, l, None, r, err)
+ dest[r.key] = value
+ dest[old_prefix + r.key] = oldvalue
+
+
+def populate_dict(source, source_mapper, dict_, synchronize_pairs):
+ for l, r in synchronize_pairs:
+ try:
+ value = source_mapper._get_state_attr_by_column(
+ source, source.dict, l, passive=PassiveFlag.PASSIVE_OFF
+ )
+ except exc.UnmappedColumnError as err:
+ _raise_col_to_prop(False, source_mapper, l, None, r, err)
+
+ dict_[r.key] = value
+
+
+def source_modified(uowcommit, source, source_mapper, synchronize_pairs):
+ """return true if the source object has changes from an old to a
+ new value on the given synchronize pairs
+
+ """
+ for l, r in synchronize_pairs:
+ try:
+ prop = source_mapper._columntoproperty[l]
+ except exc.UnmappedColumnError as err:
+ _raise_col_to_prop(False, source_mapper, l, None, r, err)
+ history = uowcommit.get_attribute_history(
+ source, prop.key, PassiveFlag.PASSIVE_NO_INITIALIZE
+ )
+ if bool(history.deleted):
+ return True
+ else:
+ return False
+
+
+def _raise_col_to_prop(
+ isdest, source_mapper, source_column, dest_mapper, dest_column, err
+):
+ if isdest:
+ raise exc.UnmappedColumnError(
+ "Can't execute sync rule for "
+ "destination column '%s'; mapper '%s' does not map "
+ "this column. Try using an explicit `foreign_keys` "
+ "collection which does not include this column (or use "
+ "a viewonly=True relation)." % (dest_column, dest_mapper)
+ ) from err
+ else:
+ raise exc.UnmappedColumnError(
+ "Can't execute sync rule for "
+ "source column '%s'; mapper '%s' does not map this "
+ "column. Try using an explicit `foreign_keys` "
+ "collection which does not include destination column "
+ "'%s' (or use a viewonly=True relation)."
+ % (source_column, source_mapper, dest_column)
+ ) from err
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/unitofwork.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/unitofwork.py
new file mode 100644
index 0000000..7e2df2b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/unitofwork.py
@@ -0,0 +1,796 @@
+# orm/unitofwork.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: ignore-errors
+
+
+"""The internals for the unit of work system.
+
+The session's flush() process passes objects to a contextual object
+here, which assembles flush tasks based on mappers and their properties,
+organizes them in order of dependency, and executes.
+
+"""
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Dict
+from typing import Optional
+from typing import Set
+from typing import TYPE_CHECKING
+
+from . import attributes
+from . import exc as orm_exc
+from . import util as orm_util
+from .. import event
+from .. import util
+from ..util import topological
+
+
+if TYPE_CHECKING:
+ from .dependency import DependencyProcessor
+ from .interfaces import MapperProperty
+ from .mapper import Mapper
+ from .session import Session
+ from .session import SessionTransaction
+ from .state import InstanceState
+
+
+def track_cascade_events(descriptor, prop):
+ """Establish event listeners on object attributes which handle
+ cascade-on-set/append.
+
+ """
+ key = prop.key
+
+ def append(state, item, initiator, **kw):
+ # process "save_update" cascade rules for when
+ # an instance is appended to the list of another instance
+
+ if item is None:
+ return
+
+ sess = state.session
+ if sess:
+ if sess._warn_on_events:
+ sess._flush_warning("collection append")
+
+ prop = state.manager.mapper._props[key]
+ item_state = attributes.instance_state(item)
+
+ if (
+ prop._cascade.save_update
+ and (key == initiator.key)
+ and not sess._contains_state(item_state)
+ ):
+ sess._save_or_update_state(item_state)
+ return item
+
+ def remove(state, item, initiator, **kw):
+ if item is None:
+ return
+
+ sess = state.session
+
+ prop = state.manager.mapper._props[key]
+
+ if sess and sess._warn_on_events:
+ sess._flush_warning(
+ "collection remove"
+ if prop.uselist
+ else "related attribute delete"
+ )
+
+ if (
+ item is not None
+ and item is not attributes.NEVER_SET
+ and item is not attributes.PASSIVE_NO_RESULT
+ and prop._cascade.delete_orphan
+ ):
+ # expunge pending orphans
+ item_state = attributes.instance_state(item)
+
+ if prop.mapper._is_orphan(item_state):
+ if sess and item_state in sess._new:
+ sess.expunge(item)
+ else:
+ # the related item may or may not itself be in a
+ # Session, however the parent for which we are catching
+ # the event is not in a session, so memoize this on the
+ # item
+ item_state._orphaned_outside_of_session = True
+
+ def set_(state, newvalue, oldvalue, initiator, **kw):
+ # process "save_update" cascade rules for when an instance
+ # is attached to another instance
+ if oldvalue is newvalue:
+ return newvalue
+
+ sess = state.session
+ if sess:
+ if sess._warn_on_events:
+ sess._flush_warning("related attribute set")
+
+ prop = state.manager.mapper._props[key]
+ if newvalue is not None:
+ newvalue_state = attributes.instance_state(newvalue)
+ if (
+ prop._cascade.save_update
+ and (key == initiator.key)
+ and not sess._contains_state(newvalue_state)
+ ):
+ sess._save_or_update_state(newvalue_state)
+
+ if (
+ oldvalue is not None
+ and oldvalue is not attributes.NEVER_SET
+ and oldvalue is not attributes.PASSIVE_NO_RESULT
+ and prop._cascade.delete_orphan
+ ):
+ # possible to reach here with attributes.NEVER_SET ?
+ oldvalue_state = attributes.instance_state(oldvalue)
+
+ if oldvalue_state in sess._new and prop.mapper._is_orphan(
+ oldvalue_state
+ ):
+ sess.expunge(oldvalue)
+ return newvalue
+
+ event.listen(
+ descriptor, "append_wo_mutation", append, raw=True, include_key=True
+ )
+ event.listen(
+ descriptor, "append", append, raw=True, retval=True, include_key=True
+ )
+ event.listen(
+ descriptor, "remove", remove, raw=True, retval=True, include_key=True
+ )
+ event.listen(
+ descriptor, "set", set_, raw=True, retval=True, include_key=True
+ )
+
+
+class UOWTransaction:
+ session: Session
+ transaction: SessionTransaction
+ attributes: Dict[str, Any]
+ deps: util.defaultdict[Mapper[Any], Set[DependencyProcessor]]
+ mappers: util.defaultdict[Mapper[Any], Set[InstanceState[Any]]]
+
+ def __init__(self, session: Session):
+ self.session = session
+
+ # dictionary used by external actors to
+ # store arbitrary state information.
+ self.attributes = {}
+
+ # dictionary of mappers to sets of
+ # DependencyProcessors, which are also
+ # set to be part of the sorted flush actions,
+ # which have that mapper as a parent.
+ self.deps = util.defaultdict(set)
+
+ # dictionary of mappers to sets of InstanceState
+ # items pending for flush which have that mapper
+ # as a parent.
+ self.mappers = util.defaultdict(set)
+
+ # a dictionary of Preprocess objects, which gather
+ # additional states impacted by the flush
+ # and determine if a flush action is needed
+ self.presort_actions = {}
+
+ # dictionary of PostSortRec objects, each
+ # one issues work during the flush within
+ # a certain ordering.
+ self.postsort_actions = {}
+
+ # a set of 2-tuples, each containing two
+ # PostSortRec objects where the second
+ # is dependent on the first being executed
+ # first
+ self.dependencies = set()
+
+ # dictionary of InstanceState-> (isdelete, listonly)
+ # tuples, indicating if this state is to be deleted
+ # or insert/updated, or just refreshed
+ self.states = {}
+
+ # tracks InstanceStates which will be receiving
+ # a "post update" call. Keys are mappers,
+ # values are a set of states and a set of the
+ # columns which should be included in the update.
+ self.post_update_states = util.defaultdict(lambda: (set(), set()))
+
+ @property
+ def has_work(self):
+ return bool(self.states)
+
+ def was_already_deleted(self, state):
+ """Return ``True`` if the given state is expired and was deleted
+ previously.
+ """
+ if state.expired:
+ try:
+ state._load_expired(state, attributes.PASSIVE_OFF)
+ except orm_exc.ObjectDeletedError:
+ self.session._remove_newly_deleted([state])
+ return True
+ return False
+
+ def is_deleted(self, state):
+ """Return ``True`` if the given state is marked as deleted
+ within this uowtransaction."""
+
+ return state in self.states and self.states[state][0]
+
+ def memo(self, key, callable_):
+ if key in self.attributes:
+ return self.attributes[key]
+ else:
+ self.attributes[key] = ret = callable_()
+ return ret
+
+ def remove_state_actions(self, state):
+ """Remove pending actions for a state from the uowtransaction."""
+
+ isdelete = self.states[state][0]
+
+ self.states[state] = (isdelete, True)
+
+ def get_attribute_history(
+ self, state, key, passive=attributes.PASSIVE_NO_INITIALIZE
+ ):
+ """Facade to attributes.get_state_history(), including
+ caching of results."""
+
+ hashkey = ("history", state, key)
+
+ # cache the objects, not the states; the strong reference here
+ # prevents newly loaded objects from being dereferenced during the
+ # flush process
+
+ if hashkey in self.attributes:
+ history, state_history, cached_passive = self.attributes[hashkey]
+ # if the cached lookup was "passive" and now
+ # we want non-passive, do a non-passive lookup and re-cache
+
+ if (
+ not cached_passive & attributes.SQL_OK
+ and passive & attributes.SQL_OK
+ ):
+ impl = state.manager[key].impl
+ history = impl.get_history(
+ state,
+ state.dict,
+ attributes.PASSIVE_OFF
+ | attributes.LOAD_AGAINST_COMMITTED
+ | attributes.NO_RAISE,
+ )
+ if history and impl.uses_objects:
+ state_history = history.as_state()
+ else:
+ state_history = history
+ self.attributes[hashkey] = (history, state_history, passive)
+ else:
+ impl = state.manager[key].impl
+ # TODO: store the history as (state, object) tuples
+ # so we don't have to keep converting here
+ history = impl.get_history(
+ state,
+ state.dict,
+ passive
+ | attributes.LOAD_AGAINST_COMMITTED
+ | attributes.NO_RAISE,
+ )
+ if history and impl.uses_objects:
+ state_history = history.as_state()
+ else:
+ state_history = history
+ self.attributes[hashkey] = (history, state_history, passive)
+
+ return state_history
+
+ def has_dep(self, processor):
+ return (processor, True) in self.presort_actions
+
+ def register_preprocessor(self, processor, fromparent):
+ key = (processor, fromparent)
+ if key not in self.presort_actions:
+ self.presort_actions[key] = Preprocess(processor, fromparent)
+
+ def register_object(
+ self,
+ state: InstanceState[Any],
+ isdelete: bool = False,
+ listonly: bool = False,
+ cancel_delete: bool = False,
+ operation: Optional[str] = None,
+ prop: Optional[MapperProperty] = None,
+ ) -> bool:
+ if not self.session._contains_state(state):
+ # this condition is normal when objects are registered
+ # as part of a relationship cascade operation. it should
+ # not occur for the top-level register from Session.flush().
+ if not state.deleted and operation is not None:
+ util.warn(
+ "Object of type %s not in session, %s operation "
+ "along '%s' will not proceed"
+ % (orm_util.state_class_str(state), operation, prop)
+ )
+ return False
+
+ if state not in self.states:
+ mapper = state.manager.mapper
+
+ if mapper not in self.mappers:
+ self._per_mapper_flush_actions(mapper)
+
+ self.mappers[mapper].add(state)
+ self.states[state] = (isdelete, listonly)
+ else:
+ if not listonly and (isdelete or cancel_delete):
+ self.states[state] = (isdelete, False)
+ return True
+
+ def register_post_update(self, state, post_update_cols):
+ mapper = state.manager.mapper.base_mapper
+ states, cols = self.post_update_states[mapper]
+ states.add(state)
+ cols.update(post_update_cols)
+
+ def _per_mapper_flush_actions(self, mapper):
+ saves = SaveUpdateAll(self, mapper.base_mapper)
+ deletes = DeleteAll(self, mapper.base_mapper)
+ self.dependencies.add((saves, deletes))
+
+ for dep in mapper._dependency_processors:
+ dep.per_property_preprocessors(self)
+
+ for prop in mapper.relationships:
+ if prop.viewonly:
+ continue
+ dep = prop._dependency_processor
+ dep.per_property_preprocessors(self)
+
+ @util.memoized_property
+ def _mapper_for_dep(self):
+ """return a dynamic mapping of (Mapper, DependencyProcessor) to
+ True or False, indicating if the DependencyProcessor operates
+ on objects of that Mapper.
+
+ The result is stored in the dictionary persistently once
+ calculated.
+
+ """
+ return util.PopulateDict(
+ lambda tup: tup[0]._props.get(tup[1].key) is tup[1].prop
+ )
+
+ def filter_states_for_dep(self, dep, states):
+ """Filter the given list of InstanceStates to those relevant to the
+ given DependencyProcessor.
+
+ """
+ mapper_for_dep = self._mapper_for_dep
+ return [s for s in states if mapper_for_dep[(s.manager.mapper, dep)]]
+
+ def states_for_mapper_hierarchy(self, mapper, isdelete, listonly):
+ checktup = (isdelete, listonly)
+ for mapper in mapper.base_mapper.self_and_descendants:
+ for state in self.mappers[mapper]:
+ if self.states[state] == checktup:
+ yield state
+
+ def _generate_actions(self):
+ """Generate the full, unsorted collection of PostSortRecs as
+ well as dependency pairs for this UOWTransaction.
+
+ """
+ # execute presort_actions, until all states
+ # have been processed. a presort_action might
+ # add new states to the uow.
+ while True:
+ ret = False
+ for action in list(self.presort_actions.values()):
+ if action.execute(self):
+ ret = True
+ if not ret:
+ break
+
+ # see if the graph of mapper dependencies has cycles.
+ self.cycles = cycles = topological.find_cycles(
+ self.dependencies, list(self.postsort_actions.values())
+ )
+
+ if cycles:
+ # if yes, break the per-mapper actions into
+ # per-state actions
+ convert = {
+ rec: set(rec.per_state_flush_actions(self)) for rec in cycles
+ }
+
+ # rewrite the existing dependencies to point to
+ # the per-state actions for those per-mapper actions
+ # that were broken up.
+ for edge in list(self.dependencies):
+ if (
+ None in edge
+ or edge[0].disabled
+ or edge[1].disabled
+ or cycles.issuperset(edge)
+ ):
+ self.dependencies.remove(edge)
+ elif edge[0] in cycles:
+ self.dependencies.remove(edge)
+ for dep in convert[edge[0]]:
+ self.dependencies.add((dep, edge[1]))
+ elif edge[1] in cycles:
+ self.dependencies.remove(edge)
+ for dep in convert[edge[1]]:
+ self.dependencies.add((edge[0], dep))
+
+ return {
+ a for a in self.postsort_actions.values() if not a.disabled
+ }.difference(cycles)
+
+ def execute(self) -> None:
+ postsort_actions = self._generate_actions()
+
+ postsort_actions = sorted(
+ postsort_actions,
+ key=lambda item: item.sort_key,
+ )
+ # sort = topological.sort(self.dependencies, postsort_actions)
+ # print "--------------"
+ # print "\ndependencies:", self.dependencies
+ # print "\ncycles:", self.cycles
+ # print "\nsort:", list(sort)
+ # print "\nCOUNT OF POSTSORT ACTIONS", len(postsort_actions)
+
+ # execute
+ if self.cycles:
+ for subset in topological.sort_as_subsets(
+ self.dependencies, postsort_actions
+ ):
+ set_ = set(subset)
+ while set_:
+ n = set_.pop()
+ n.execute_aggregate(self, set_)
+ else:
+ for rec in topological.sort(self.dependencies, postsort_actions):
+ rec.execute(self)
+
+ def finalize_flush_changes(self) -> None:
+ """Mark processed objects as clean / deleted after a successful
+ flush().
+
+ This method is called within the flush() method after the
+ execute() method has succeeded and the transaction has been committed.
+
+ """
+ if not self.states:
+ return
+
+ states = set(self.states)
+ isdel = {
+ s for (s, (isdelete, listonly)) in self.states.items() if isdelete
+ }
+ other = states.difference(isdel)
+ if isdel:
+ self.session._remove_newly_deleted(isdel)
+ if other:
+ self.session._register_persistent(other)
+
+
+class IterateMappersMixin:
+ __slots__ = ()
+
+ def _mappers(self, uow):
+ if self.fromparent:
+ return iter(
+ m
+ for m in self.dependency_processor.parent.self_and_descendants
+ if uow._mapper_for_dep[(m, self.dependency_processor)]
+ )
+ else:
+ return self.dependency_processor.mapper.self_and_descendants
+
+
+class Preprocess(IterateMappersMixin):
+ __slots__ = (
+ "dependency_processor",
+ "fromparent",
+ "processed",
+ "setup_flush_actions",
+ )
+
+ def __init__(self, dependency_processor, fromparent):
+ self.dependency_processor = dependency_processor
+ self.fromparent = fromparent
+ self.processed = set()
+ self.setup_flush_actions = False
+
+ def execute(self, uow):
+ delete_states = set()
+ save_states = set()
+
+ for mapper in self._mappers(uow):
+ for state in uow.mappers[mapper].difference(self.processed):
+ (isdelete, listonly) = uow.states[state]
+ if not listonly:
+ if isdelete:
+ delete_states.add(state)
+ else:
+ save_states.add(state)
+
+ if delete_states:
+ self.dependency_processor.presort_deletes(uow, delete_states)
+ self.processed.update(delete_states)
+ if save_states:
+ self.dependency_processor.presort_saves(uow, save_states)
+ self.processed.update(save_states)
+
+ if delete_states or save_states:
+ if not self.setup_flush_actions and (
+ self.dependency_processor.prop_has_changes(
+ uow, delete_states, True
+ )
+ or self.dependency_processor.prop_has_changes(
+ uow, save_states, False
+ )
+ ):
+ self.dependency_processor.per_property_flush_actions(uow)
+ self.setup_flush_actions = True
+ return True
+ else:
+ return False
+
+
+class PostSortRec:
+ __slots__ = ("disabled",)
+
+ def __new__(cls, uow, *args):
+ key = (cls,) + args
+ if key in uow.postsort_actions:
+ return uow.postsort_actions[key]
+ else:
+ uow.postsort_actions[key] = ret = object.__new__(cls)
+ ret.disabled = False
+ return ret
+
+ def execute_aggregate(self, uow, recs):
+ self.execute(uow)
+
+
+class ProcessAll(IterateMappersMixin, PostSortRec):
+ __slots__ = "dependency_processor", "isdelete", "fromparent", "sort_key"
+
+ def __init__(self, uow, dependency_processor, isdelete, fromparent):
+ self.dependency_processor = dependency_processor
+ self.sort_key = (
+ "ProcessAll",
+ self.dependency_processor.sort_key,
+ isdelete,
+ )
+ self.isdelete = isdelete
+ self.fromparent = fromparent
+ uow.deps[dependency_processor.parent.base_mapper].add(
+ dependency_processor
+ )
+
+ def execute(self, uow):
+ states = self._elements(uow)
+ if self.isdelete:
+ self.dependency_processor.process_deletes(uow, states)
+ else:
+ self.dependency_processor.process_saves(uow, states)
+
+ def per_state_flush_actions(self, uow):
+ # this is handled by SaveUpdateAll and DeleteAll,
+ # since a ProcessAll should unconditionally be pulled
+ # into per-state if either the parent/child mappers
+ # are part of a cycle
+ return iter([])
+
+ def __repr__(self):
+ return "%s(%s, isdelete=%s)" % (
+ self.__class__.__name__,
+ self.dependency_processor,
+ self.isdelete,
+ )
+
+ def _elements(self, uow):
+ for mapper in self._mappers(uow):
+ for state in uow.mappers[mapper]:
+ (isdelete, listonly) = uow.states[state]
+ if isdelete == self.isdelete and not listonly:
+ yield state
+
+
+class PostUpdateAll(PostSortRec):
+ __slots__ = "mapper", "isdelete", "sort_key"
+
+ def __init__(self, uow, mapper, isdelete):
+ self.mapper = mapper
+ self.isdelete = isdelete
+ self.sort_key = ("PostUpdateAll", mapper._sort_key, isdelete)
+
+ @util.preload_module("sqlalchemy.orm.persistence")
+ def execute(self, uow):
+ persistence = util.preloaded.orm_persistence
+ states, cols = uow.post_update_states[self.mapper]
+ states = [s for s in states if uow.states[s][0] == self.isdelete]
+
+ persistence.post_update(self.mapper, states, uow, cols)
+
+
+class SaveUpdateAll(PostSortRec):
+ __slots__ = ("mapper", "sort_key")
+
+ def __init__(self, uow, mapper):
+ self.mapper = mapper
+ self.sort_key = ("SaveUpdateAll", mapper._sort_key)
+ assert mapper is mapper.base_mapper
+
+ @util.preload_module("sqlalchemy.orm.persistence")
+ def execute(self, uow):
+ util.preloaded.orm_persistence.save_obj(
+ self.mapper,
+ uow.states_for_mapper_hierarchy(self.mapper, False, False),
+ uow,
+ )
+
+ def per_state_flush_actions(self, uow):
+ states = list(
+ uow.states_for_mapper_hierarchy(self.mapper, False, False)
+ )
+ base_mapper = self.mapper.base_mapper
+ delete_all = DeleteAll(uow, base_mapper)
+ for state in states:
+ # keep saves before deletes -
+ # this ensures 'row switch' operations work
+ action = SaveUpdateState(uow, state)
+ uow.dependencies.add((action, delete_all))
+ yield action
+
+ for dep in uow.deps[self.mapper]:
+ states_for_prop = uow.filter_states_for_dep(dep, states)
+ dep.per_state_flush_actions(uow, states_for_prop, False)
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self.mapper)
+
+
+class DeleteAll(PostSortRec):
+ __slots__ = ("mapper", "sort_key")
+
+ def __init__(self, uow, mapper):
+ self.mapper = mapper
+ self.sort_key = ("DeleteAll", mapper._sort_key)
+ assert mapper is mapper.base_mapper
+
+ @util.preload_module("sqlalchemy.orm.persistence")
+ def execute(self, uow):
+ util.preloaded.orm_persistence.delete_obj(
+ self.mapper,
+ uow.states_for_mapper_hierarchy(self.mapper, True, False),
+ uow,
+ )
+
+ def per_state_flush_actions(self, uow):
+ states = list(
+ uow.states_for_mapper_hierarchy(self.mapper, True, False)
+ )
+ base_mapper = self.mapper.base_mapper
+ save_all = SaveUpdateAll(uow, base_mapper)
+ for state in states:
+ # keep saves before deletes -
+ # this ensures 'row switch' operations work
+ action = DeleteState(uow, state)
+ uow.dependencies.add((save_all, action))
+ yield action
+
+ for dep in uow.deps[self.mapper]:
+ states_for_prop = uow.filter_states_for_dep(dep, states)
+ dep.per_state_flush_actions(uow, states_for_prop, True)
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self.mapper)
+
+
+class ProcessState(PostSortRec):
+ __slots__ = "dependency_processor", "isdelete", "state", "sort_key"
+
+ def __init__(self, uow, dependency_processor, isdelete, state):
+ self.dependency_processor = dependency_processor
+ self.sort_key = ("ProcessState", dependency_processor.sort_key)
+ self.isdelete = isdelete
+ self.state = state
+
+ def execute_aggregate(self, uow, recs):
+ cls_ = self.__class__
+ dependency_processor = self.dependency_processor
+ isdelete = self.isdelete
+ our_recs = [
+ r
+ for r in recs
+ if r.__class__ is cls_
+ and r.dependency_processor is dependency_processor
+ and r.isdelete is isdelete
+ ]
+ recs.difference_update(our_recs)
+ states = [self.state] + [r.state for r in our_recs]
+ if isdelete:
+ dependency_processor.process_deletes(uow, states)
+ else:
+ dependency_processor.process_saves(uow, states)
+
+ def __repr__(self):
+ return "%s(%s, %s, delete=%s)" % (
+ self.__class__.__name__,
+ self.dependency_processor,
+ orm_util.state_str(self.state),
+ self.isdelete,
+ )
+
+
+class SaveUpdateState(PostSortRec):
+ __slots__ = "state", "mapper", "sort_key"
+
+ def __init__(self, uow, state):
+ self.state = state
+ self.mapper = state.mapper.base_mapper
+ self.sort_key = ("ProcessState", self.mapper._sort_key)
+
+ @util.preload_module("sqlalchemy.orm.persistence")
+ def execute_aggregate(self, uow, recs):
+ persistence = util.preloaded.orm_persistence
+ cls_ = self.__class__
+ mapper = self.mapper
+ our_recs = [
+ r for r in recs if r.__class__ is cls_ and r.mapper is mapper
+ ]
+ recs.difference_update(our_recs)
+ persistence.save_obj(
+ mapper, [self.state] + [r.state for r in our_recs], uow
+ )
+
+ def __repr__(self):
+ return "%s(%s)" % (
+ self.__class__.__name__,
+ orm_util.state_str(self.state),
+ )
+
+
+class DeleteState(PostSortRec):
+ __slots__ = "state", "mapper", "sort_key"
+
+ def __init__(self, uow, state):
+ self.state = state
+ self.mapper = state.mapper.base_mapper
+ self.sort_key = ("DeleteState", self.mapper._sort_key)
+
+ @util.preload_module("sqlalchemy.orm.persistence")
+ def execute_aggregate(self, uow, recs):
+ persistence = util.preloaded.orm_persistence
+ cls_ = self.__class__
+ mapper = self.mapper
+ our_recs = [
+ r for r in recs if r.__class__ is cls_ and r.mapper is mapper
+ ]
+ recs.difference_update(our_recs)
+ states = [self.state] + [r.state for r in our_recs]
+ persistence.delete_obj(
+ mapper, [s for s in states if uow.states[s][0]], uow
+ )
+
+ def __repr__(self):
+ return "%s(%s)" % (
+ self.__class__.__name__,
+ orm_util.state_str(self.state),
+ )
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/util.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/util.py
new file mode 100644
index 0000000..8e153e6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/util.py
@@ -0,0 +1,2416 @@
+# orm/util.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+# mypy: allow-untyped-defs, allow-untyped-calls
+
+from __future__ import annotations
+
+import enum
+import functools
+import re
+import types
+import typing
+from typing import AbstractSet
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import FrozenSet
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Match
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import weakref
+
+from . import attributes # noqa
+from . import exc
+from ._typing import _O
+from ._typing import insp_is_aliased_class
+from ._typing import insp_is_mapper
+from ._typing import prop_is_relationship
+from .base import _class_to_mapper as _class_to_mapper
+from .base import _MappedAnnotationBase
+from .base import _never_set as _never_set # noqa: F401
+from .base import _none_set as _none_set # noqa: F401
+from .base import attribute_str as attribute_str # noqa: F401
+from .base import class_mapper as class_mapper
+from .base import DynamicMapped
+from .base import InspectionAttr as InspectionAttr
+from .base import instance_str as instance_str # noqa: F401
+from .base import Mapped
+from .base import object_mapper as object_mapper
+from .base import object_state as object_state # noqa: F401
+from .base import opt_manager_of_class
+from .base import ORMDescriptor
+from .base import state_attribute_str as state_attribute_str # noqa: F401
+from .base import state_class_str as state_class_str # noqa: F401
+from .base import state_str as state_str # noqa: F401
+from .base import WriteOnlyMapped
+from .interfaces import CriteriaOption
+from .interfaces import MapperProperty as MapperProperty
+from .interfaces import ORMColumnsClauseRole
+from .interfaces import ORMEntityColumnsClauseRole
+from .interfaces import ORMFromClauseRole
+from .path_registry import PathRegistry as PathRegistry
+from .. import event
+from .. import exc as sa_exc
+from .. import inspection
+from .. import sql
+from .. import util
+from ..engine.result import result_tuple
+from ..sql import coercions
+from ..sql import expression
+from ..sql import lambdas
+from ..sql import roles
+from ..sql import util as sql_util
+from ..sql import visitors
+from ..sql._typing import is_selectable
+from ..sql.annotation import SupportsCloneAnnotations
+from ..sql.base import ColumnCollection
+from ..sql.cache_key import HasCacheKey
+from ..sql.cache_key import MemoizedHasCacheKey
+from ..sql.elements import ColumnElement
+from ..sql.elements import KeyedColumnElement
+from ..sql.selectable import FromClause
+from ..util.langhelpers import MemoizedSlots
+from ..util.typing import de_stringify_annotation as _de_stringify_annotation
+from ..util.typing import (
+ de_stringify_union_elements as _de_stringify_union_elements,
+)
+from ..util.typing import eval_name_only as _eval_name_only
+from ..util.typing import is_origin_of_cls
+from ..util.typing import Literal
+from ..util.typing import Protocol
+from ..util.typing import typing_get_origin
+
+if typing.TYPE_CHECKING:
+ from ._typing import _EntityType
+ from ._typing import _IdentityKeyType
+ from ._typing import _InternalEntityType
+ from ._typing import _ORMCOLEXPR
+ from .context import _MapperEntity
+ from .context import ORMCompileState
+ from .mapper import Mapper
+ from .path_registry import AbstractEntityRegistry
+ from .query import Query
+ from .relationships import RelationshipProperty
+ from ..engine import Row
+ from ..engine import RowMapping
+ from ..sql._typing import _CE
+ from ..sql._typing import _ColumnExpressionArgument
+ from ..sql._typing import _EquivalentColumnMap
+ from ..sql._typing import _FromClauseArgument
+ from ..sql._typing import _OnClauseArgument
+ from ..sql._typing import _PropagateAttrsType
+ from ..sql.annotation import _SA
+ from ..sql.base import ReadOnlyColumnCollection
+ from ..sql.elements import BindParameter
+ from ..sql.selectable import _ColumnsClauseElement
+ from ..sql.selectable import Select
+ from ..sql.selectable import Selectable
+ from ..sql.visitors import anon_map
+ from ..util.typing import _AnnotationScanType
+ from ..util.typing import ArgsTypeProcotol
+
+_T = TypeVar("_T", bound=Any)
+
+all_cascades = frozenset(
+ (
+ "delete",
+ "delete-orphan",
+ "all",
+ "merge",
+ "expunge",
+ "save-update",
+ "refresh-expire",
+ "none",
+ )
+)
+
+
+_de_stringify_partial = functools.partial(
+ functools.partial,
+ locals_=util.immutabledict(
+ {
+ "Mapped": Mapped,
+ "WriteOnlyMapped": WriteOnlyMapped,
+ "DynamicMapped": DynamicMapped,
+ }
+ ),
+)
+
+# partial is practically useless as we have to write out the whole
+# function and maintain the signature anyway
+
+
+class _DeStringifyAnnotation(Protocol):
+ def __call__(
+ self,
+ cls: Type[Any],
+ annotation: _AnnotationScanType,
+ originating_module: str,
+ *,
+ str_cleanup_fn: Optional[Callable[[str, str], str]] = None,
+ include_generic: bool = False,
+ ) -> Type[Any]: ...
+
+
+de_stringify_annotation = cast(
+ _DeStringifyAnnotation, _de_stringify_partial(_de_stringify_annotation)
+)
+
+
+class _DeStringifyUnionElements(Protocol):
+ def __call__(
+ self,
+ cls: Type[Any],
+ annotation: ArgsTypeProcotol,
+ originating_module: str,
+ *,
+ str_cleanup_fn: Optional[Callable[[str, str], str]] = None,
+ ) -> Type[Any]: ...
+
+
+de_stringify_union_elements = cast(
+ _DeStringifyUnionElements,
+ _de_stringify_partial(_de_stringify_union_elements),
+)
+
+
+class _EvalNameOnly(Protocol):
+ def __call__(self, name: str, module_name: str) -> Any: ...
+
+
+eval_name_only = cast(_EvalNameOnly, _de_stringify_partial(_eval_name_only))
+
+
+class CascadeOptions(FrozenSet[str]):
+ """Keeps track of the options sent to
+ :paramref:`.relationship.cascade`"""
+
+ _add_w_all_cascades = all_cascades.difference(
+ ["all", "none", "delete-orphan"]
+ )
+ _allowed_cascades = all_cascades
+
+ _viewonly_cascades = ["expunge", "all", "none", "refresh-expire", "merge"]
+
+ __slots__ = (
+ "save_update",
+ "delete",
+ "refresh_expire",
+ "merge",
+ "expunge",
+ "delete_orphan",
+ )
+
+ save_update: bool
+ delete: bool
+ refresh_expire: bool
+ merge: bool
+ expunge: bool
+ delete_orphan: bool
+
+ def __new__(
+ cls, value_list: Optional[Union[Iterable[str], str]]
+ ) -> CascadeOptions:
+ if isinstance(value_list, str) or value_list is None:
+ return cls.from_string(value_list) # type: ignore
+ values = set(value_list)
+ if values.difference(cls._allowed_cascades):
+ raise sa_exc.ArgumentError(
+ "Invalid cascade option(s): %s"
+ % ", ".join(
+ [
+ repr(x)
+ for x in sorted(
+ values.difference(cls._allowed_cascades)
+ )
+ ]
+ )
+ )
+
+ if "all" in values:
+ values.update(cls._add_w_all_cascades)
+ if "none" in values:
+ values.clear()
+ values.discard("all")
+
+ self = super().__new__(cls, values)
+ self.save_update = "save-update" in values
+ self.delete = "delete" in values
+ self.refresh_expire = "refresh-expire" in values
+ self.merge = "merge" in values
+ self.expunge = "expunge" in values
+ self.delete_orphan = "delete-orphan" in values
+
+ if self.delete_orphan and not self.delete:
+ util.warn("The 'delete-orphan' cascade option requires 'delete'.")
+ return self
+
+ def __repr__(self):
+ return "CascadeOptions(%r)" % (",".join([x for x in sorted(self)]))
+
+ @classmethod
+ def from_string(cls, arg):
+ values = [c for c in re.split(r"\s*,\s*", arg or "") if c]
+ return cls(values)
+
+
+def _validator_events(desc, key, validator, include_removes, include_backrefs):
+ """Runs a validation method on an attribute value to be set or
+ appended.
+ """
+
+ if not include_backrefs:
+
+ def detect_is_backref(state, initiator):
+ impl = state.manager[key].impl
+ return initiator.impl is not impl
+
+ if include_removes:
+
+ def append(state, value, initiator):
+ if initiator.op is not attributes.OP_BULK_REPLACE and (
+ include_backrefs or not detect_is_backref(state, initiator)
+ ):
+ return validator(state.obj(), key, value, False)
+ else:
+ return value
+
+ def bulk_set(state, values, initiator):
+ if include_backrefs or not detect_is_backref(state, initiator):
+ obj = state.obj()
+ values[:] = [
+ validator(obj, key, value, False) for value in values
+ ]
+
+ def set_(state, value, oldvalue, initiator):
+ if include_backrefs or not detect_is_backref(state, initiator):
+ return validator(state.obj(), key, value, False)
+ else:
+ return value
+
+ def remove(state, value, initiator):
+ if include_backrefs or not detect_is_backref(state, initiator):
+ validator(state.obj(), key, value, True)
+
+ else:
+
+ def append(state, value, initiator):
+ if initiator.op is not attributes.OP_BULK_REPLACE and (
+ include_backrefs or not detect_is_backref(state, initiator)
+ ):
+ return validator(state.obj(), key, value)
+ else:
+ return value
+
+ def bulk_set(state, values, initiator):
+ if include_backrefs or not detect_is_backref(state, initiator):
+ obj = state.obj()
+ values[:] = [validator(obj, key, value) for value in values]
+
+ def set_(state, value, oldvalue, initiator):
+ if include_backrefs or not detect_is_backref(state, initiator):
+ return validator(state.obj(), key, value)
+ else:
+ return value
+
+ event.listen(desc, "append", append, raw=True, retval=True)
+ event.listen(desc, "bulk_replace", bulk_set, raw=True)
+ event.listen(desc, "set", set_, raw=True, retval=True)
+ if include_removes:
+ event.listen(desc, "remove", remove, raw=True, retval=True)
+
+
+def polymorphic_union(
+ table_map, typecolname, aliasname="p_union", cast_nulls=True
+):
+ """Create a ``UNION`` statement used by a polymorphic mapper.
+
+ See :ref:`concrete_inheritance` for an example of how
+ this is used.
+
+ :param table_map: mapping of polymorphic identities to
+ :class:`_schema.Table` objects.
+ :param typecolname: string name of a "discriminator" column, which will be
+ derived from the query, producing the polymorphic identity for
+ each row. If ``None``, no polymorphic discriminator is generated.
+ :param aliasname: name of the :func:`~sqlalchemy.sql.expression.alias()`
+ construct generated.
+ :param cast_nulls: if True, non-existent columns, which are represented
+ as labeled NULLs, will be passed into CAST. This is a legacy behavior
+ that is problematic on some backends such as Oracle - in which case it
+ can be set to False.
+
+ """
+
+ colnames: util.OrderedSet[str] = util.OrderedSet()
+ colnamemaps = {}
+ types = {}
+ for key in table_map:
+ table = table_map[key]
+
+ table = coercions.expect(
+ roles.StrictFromClauseRole, table, allow_select=True
+ )
+ table_map[key] = table
+
+ m = {}
+ for c in table.c:
+ if c.key == typecolname:
+ raise sa_exc.InvalidRequestError(
+ "Polymorphic union can't use '%s' as the discriminator "
+ "column due to mapped column %r; please apply the "
+ "'typecolname' "
+ "argument; this is available on "
+ "ConcreteBase as '_concrete_discriminator_name'"
+ % (typecolname, c)
+ )
+ colnames.add(c.key)
+ m[c.key] = c
+ types[c.key] = c.type
+ colnamemaps[table] = m
+
+ def col(name, table):
+ try:
+ return colnamemaps[table][name]
+ except KeyError:
+ if cast_nulls:
+ return sql.cast(sql.null(), types[name]).label(name)
+ else:
+ return sql.type_coerce(sql.null(), types[name]).label(name)
+
+ result = []
+ for type_, table in table_map.items():
+ if typecolname is not None:
+ result.append(
+ sql.select(
+ *(
+ [col(name, table) for name in colnames]
+ + [
+ sql.literal_column(
+ sql_util._quote_ddl_expr(type_)
+ ).label(typecolname)
+ ]
+ )
+ ).select_from(table)
+ )
+ else:
+ result.append(
+ sql.select(
+ *[col(name, table) for name in colnames]
+ ).select_from(table)
+ )
+ return sql.union_all(*result).alias(aliasname)
+
+
+def identity_key(
+ class_: Optional[Type[_T]] = None,
+ ident: Union[Any, Tuple[Any, ...]] = None,
+ *,
+ instance: Optional[_T] = None,
+ row: Optional[Union[Row[Any], RowMapping]] = None,
+ identity_token: Optional[Any] = None,
+) -> _IdentityKeyType[_T]:
+ r"""Generate "identity key" tuples, as are used as keys in the
+ :attr:`.Session.identity_map` dictionary.
+
+ This function has several call styles:
+
+ * ``identity_key(class, ident, identity_token=token)``
+
+ This form receives a mapped class and a primary key scalar or
+ tuple as an argument.
+
+ E.g.::
+
+ >>> identity_key(MyClass, (1, 2))
+ (<class '__main__.MyClass'>, (1, 2), None)
+
+ :param class: mapped class (must be a positional argument)
+ :param ident: primary key, may be a scalar or tuple argument.
+ :param identity_token: optional identity token
+
+ .. versionadded:: 1.2 added identity_token
+
+
+ * ``identity_key(instance=instance)``
+
+ This form will produce the identity key for a given instance. The
+ instance need not be persistent, only that its primary key attributes
+ are populated (else the key will contain ``None`` for those missing
+ values).
+
+ E.g.::
+
+ >>> instance = MyClass(1, 2)
+ >>> identity_key(instance=instance)
+ (<class '__main__.MyClass'>, (1, 2), None)
+
+ In this form, the given instance is ultimately run though
+ :meth:`_orm.Mapper.identity_key_from_instance`, which will have the
+ effect of performing a database check for the corresponding row
+ if the object is expired.
+
+ :param instance: object instance (must be given as a keyword arg)
+
+ * ``identity_key(class, row=row, identity_token=token)``
+
+ This form is similar to the class/tuple form, except is passed a
+ database result row as a :class:`.Row` or :class:`.RowMapping` object.
+
+ E.g.::
+
+ >>> row = engine.execute(\
+ text("select * from table where a=1 and b=2")\
+ ).first()
+ >>> identity_key(MyClass, row=row)
+ (<class '__main__.MyClass'>, (1, 2), None)
+
+ :param class: mapped class (must be a positional argument)
+ :param row: :class:`.Row` row returned by a :class:`_engine.CursorResult`
+ (must be given as a keyword arg)
+ :param identity_token: optional identity token
+
+ .. versionadded:: 1.2 added identity_token
+
+ """
+ if class_ is not None:
+ mapper = class_mapper(class_)
+ if row is None:
+ if ident is None:
+ raise sa_exc.ArgumentError("ident or row is required")
+ return mapper.identity_key_from_primary_key(
+ tuple(util.to_list(ident)), identity_token=identity_token
+ )
+ else:
+ return mapper.identity_key_from_row(
+ row, identity_token=identity_token
+ )
+ elif instance is not None:
+ mapper = object_mapper(instance)
+ return mapper.identity_key_from_instance(instance)
+ else:
+ raise sa_exc.ArgumentError("class or instance is required")
+
+
+class _TraceAdaptRole(enum.Enum):
+ """Enumeration of all the use cases for ORMAdapter.
+
+ ORMAdapter remains one of the most complicated aspects of the ORM, as it is
+ used for in-place adaption of column expressions to be applied to a SELECT,
+ replacing :class:`.Table` and other objects that are mapped to classes with
+ aliases of those tables in the case of joined eager loading, or in the case
+ of polymorphic loading as used with concrete mappings or other custom "with
+ polymorphic" parameters, with whole user-defined subqueries. The
+ enumerations provide an overview of all the use cases used by ORMAdapter, a
+ layer of formality as to the introduction of new ORMAdapter use cases (of
+ which none are anticipated), as well as a means to trace the origins of a
+ particular ORMAdapter within runtime debugging.
+
+ SQLAlchemy 2.0 has greatly scaled back ORM features which relied heavily on
+ open-ended statement adaption, including the ``Query.with_polymorphic()``
+ method and the ``Query.select_from_entity()`` methods, favoring
+ user-explicit aliasing schemes using the ``aliased()`` and
+ ``with_polymorphic()`` standalone constructs; these still use adaption,
+ however the adaption is applied in a narrower scope.
+
+ """
+
+ # aliased() use that is used to adapt individual attributes at query
+ # construction time
+ ALIASED_INSP = enum.auto()
+
+ # joinedload cases; typically adapt an ON clause of a relationship
+ # join
+ JOINEDLOAD_USER_DEFINED_ALIAS = enum.auto()
+ JOINEDLOAD_PATH_WITH_POLYMORPHIC = enum.auto()
+ JOINEDLOAD_MEMOIZED_ADAPTER = enum.auto()
+
+ # polymorphic cases - these are complex ones that replace FROM
+ # clauses, replacing tables with subqueries
+ MAPPER_POLYMORPHIC_ADAPTER = enum.auto()
+ WITH_POLYMORPHIC_ADAPTER = enum.auto()
+ WITH_POLYMORPHIC_ADAPTER_RIGHT_JOIN = enum.auto()
+ DEPRECATED_JOIN_ADAPT_RIGHT_SIDE = enum.auto()
+
+ # the from_statement() case, used only to adapt individual attributes
+ # from a given statement to local ORM attributes at result fetching
+ # time. assigned to ORMCompileState._from_obj_alias
+ ADAPT_FROM_STATEMENT = enum.auto()
+
+ # the joinedload for queries that have LIMIT/OFFSET/DISTINCT case;
+ # the query is placed inside of a subquery with the LIMIT/OFFSET/etc.,
+ # joinedloads are then placed on the outside.
+ # assigned to ORMCompileState.compound_eager_adapter
+ COMPOUND_EAGER_STATEMENT = enum.auto()
+
+ # the legacy Query._set_select_from() case.
+ # this is needed for Query's set operations (i.e. UNION, etc. )
+ # as well as "legacy from_self()", which while removed from 2.0 as
+ # public API, is used for the Query.count() method. this one
+ # still does full statement traversal
+ # assigned to ORMCompileState._from_obj_alias
+ LEGACY_SELECT_FROM_ALIAS = enum.auto()
+
+
+class ORMStatementAdapter(sql_util.ColumnAdapter):
+ """ColumnAdapter which includes a role attribute."""
+
+ __slots__ = ("role",)
+
+ def __init__(
+ self,
+ role: _TraceAdaptRole,
+ selectable: Selectable,
+ *,
+ equivalents: Optional[_EquivalentColumnMap] = None,
+ adapt_required: bool = False,
+ allow_label_resolve: bool = True,
+ anonymize_labels: bool = False,
+ adapt_on_names: bool = False,
+ adapt_from_selectables: Optional[AbstractSet[FromClause]] = None,
+ ):
+ self.role = role
+ super().__init__(
+ selectable,
+ equivalents=equivalents,
+ adapt_required=adapt_required,
+ allow_label_resolve=allow_label_resolve,
+ anonymize_labels=anonymize_labels,
+ adapt_on_names=adapt_on_names,
+ adapt_from_selectables=adapt_from_selectables,
+ )
+
+
+class ORMAdapter(sql_util.ColumnAdapter):
+ """ColumnAdapter subclass which excludes adaptation of entities from
+ non-matching mappers.
+
+ """
+
+ __slots__ = ("role", "mapper", "is_aliased_class", "aliased_insp")
+
+ is_aliased_class: bool
+ aliased_insp: Optional[AliasedInsp[Any]]
+
+ def __init__(
+ self,
+ role: _TraceAdaptRole,
+ entity: _InternalEntityType[Any],
+ *,
+ equivalents: Optional[_EquivalentColumnMap] = None,
+ adapt_required: bool = False,
+ allow_label_resolve: bool = True,
+ anonymize_labels: bool = False,
+ selectable: Optional[Selectable] = None,
+ limit_on_entity: bool = True,
+ adapt_on_names: bool = False,
+ adapt_from_selectables: Optional[AbstractSet[FromClause]] = None,
+ ):
+ self.role = role
+ self.mapper = entity.mapper
+ if selectable is None:
+ selectable = entity.selectable
+ if insp_is_aliased_class(entity):
+ self.is_aliased_class = True
+ self.aliased_insp = entity
+ else:
+ self.is_aliased_class = False
+ self.aliased_insp = None
+
+ super().__init__(
+ selectable,
+ equivalents,
+ adapt_required=adapt_required,
+ allow_label_resolve=allow_label_resolve,
+ anonymize_labels=anonymize_labels,
+ include_fn=self._include_fn if limit_on_entity else None,
+ adapt_on_names=adapt_on_names,
+ adapt_from_selectables=adapt_from_selectables,
+ )
+
+ def _include_fn(self, elem):
+ entity = elem._annotations.get("parentmapper", None)
+
+ return not entity or entity.isa(self.mapper) or self.mapper.isa(entity)
+
+
+class AliasedClass(
+ inspection.Inspectable["AliasedInsp[_O]"], ORMColumnsClauseRole[_O]
+):
+ r"""Represents an "aliased" form of a mapped class for usage with Query.
+
+ The ORM equivalent of a :func:`~sqlalchemy.sql.expression.alias`
+ construct, this object mimics the mapped class using a
+ ``__getattr__`` scheme and maintains a reference to a
+ real :class:`~sqlalchemy.sql.expression.Alias` object.
+
+ A primary purpose of :class:`.AliasedClass` is to serve as an alternate
+ within a SQL statement generated by the ORM, such that an existing
+ mapped entity can be used in multiple contexts. A simple example::
+
+ # find all pairs of users with the same name
+ user_alias = aliased(User)
+ session.query(User, user_alias).\
+ join((user_alias, User.id > user_alias.id)).\
+ filter(User.name == user_alias.name)
+
+ :class:`.AliasedClass` is also capable of mapping an existing mapped
+ class to an entirely new selectable, provided this selectable is column-
+ compatible with the existing mapped selectable, and it can also be
+ configured in a mapping as the target of a :func:`_orm.relationship`.
+ See the links below for examples.
+
+ The :class:`.AliasedClass` object is constructed typically using the
+ :func:`_orm.aliased` function. It also is produced with additional
+ configuration when using the :func:`_orm.with_polymorphic` function.
+
+ The resulting object is an instance of :class:`.AliasedClass`.
+ This object implements an attribute scheme which produces the
+ same attribute and method interface as the original mapped
+ class, allowing :class:`.AliasedClass` to be compatible
+ with any attribute technique which works on the original class,
+ including hybrid attributes (see :ref:`hybrids_toplevel`).
+
+ The :class:`.AliasedClass` can be inspected for its underlying
+ :class:`_orm.Mapper`, aliased selectable, and other information
+ using :func:`_sa.inspect`::
+
+ from sqlalchemy import inspect
+ my_alias = aliased(MyClass)
+ insp = inspect(my_alias)
+
+ The resulting inspection object is an instance of :class:`.AliasedInsp`.
+
+
+ .. seealso::
+
+ :func:`.aliased`
+
+ :func:`.with_polymorphic`
+
+ :ref:`relationship_aliased_class`
+
+ :ref:`relationship_to_window_function`
+
+
+ """
+
+ __name__: str
+
+ def __init__(
+ self,
+ mapped_class_or_ac: _EntityType[_O],
+ alias: Optional[FromClause] = None,
+ name: Optional[str] = None,
+ flat: bool = False,
+ adapt_on_names: bool = False,
+ with_polymorphic_mappers: Optional[Sequence[Mapper[Any]]] = None,
+ with_polymorphic_discriminator: Optional[ColumnElement[Any]] = None,
+ base_alias: Optional[AliasedInsp[Any]] = None,
+ use_mapper_path: bool = False,
+ represents_outer_join: bool = False,
+ ):
+ insp = cast(
+ "_InternalEntityType[_O]", inspection.inspect(mapped_class_or_ac)
+ )
+ mapper = insp.mapper
+
+ nest_adapters = False
+
+ if alias is None:
+ if insp.is_aliased_class and insp.selectable._is_subquery:
+ alias = insp.selectable.alias()
+ else:
+ alias = (
+ mapper._with_polymorphic_selectable._anonymous_fromclause(
+ name=name,
+ flat=flat,
+ )
+ )
+ elif insp.is_aliased_class:
+ nest_adapters = True
+
+ assert alias is not None
+ self._aliased_insp = AliasedInsp(
+ self,
+ insp,
+ alias,
+ name,
+ (
+ with_polymorphic_mappers
+ if with_polymorphic_mappers
+ else mapper.with_polymorphic_mappers
+ ),
+ (
+ with_polymorphic_discriminator
+ if with_polymorphic_discriminator is not None
+ else mapper.polymorphic_on
+ ),
+ base_alias,
+ use_mapper_path,
+ adapt_on_names,
+ represents_outer_join,
+ nest_adapters,
+ )
+
+ self.__name__ = f"aliased({mapper.class_.__name__})"
+
+ @classmethod
+ def _reconstitute_from_aliased_insp(
+ cls, aliased_insp: AliasedInsp[_O]
+ ) -> AliasedClass[_O]:
+ obj = cls.__new__(cls)
+ obj.__name__ = f"aliased({aliased_insp.mapper.class_.__name__})"
+ obj._aliased_insp = aliased_insp
+
+ if aliased_insp._is_with_polymorphic:
+ for sub_aliased_insp in aliased_insp._with_polymorphic_entities:
+ if sub_aliased_insp is not aliased_insp:
+ ent = AliasedClass._reconstitute_from_aliased_insp(
+ sub_aliased_insp
+ )
+ setattr(obj, sub_aliased_insp.class_.__name__, ent)
+
+ return obj
+
+ def __getattr__(self, key: str) -> Any:
+ try:
+ _aliased_insp = self.__dict__["_aliased_insp"]
+ except KeyError:
+ raise AttributeError()
+ else:
+ target = _aliased_insp._target
+ # maintain all getattr mechanics
+ attr = getattr(target, key)
+
+ # attribute is a method, that will be invoked against a
+ # "self"; so just return a new method with the same function and
+ # new self
+ if hasattr(attr, "__call__") and hasattr(attr, "__self__"):
+ return types.MethodType(attr.__func__, self)
+
+ # attribute is a descriptor, that will be invoked against a
+ # "self"; so invoke the descriptor against this self
+ if hasattr(attr, "__get__"):
+ attr = attr.__get__(None, self)
+
+ # attributes within the QueryableAttribute system will want this
+ # to be invoked so the object can be adapted
+ if hasattr(attr, "adapt_to_entity"):
+ attr = attr.adapt_to_entity(_aliased_insp)
+ setattr(self, key, attr)
+
+ return attr
+
+ def _get_from_serialized(
+ self, key: str, mapped_class: _O, aliased_insp: AliasedInsp[_O]
+ ) -> Any:
+ # this method is only used in terms of the
+ # sqlalchemy.ext.serializer extension
+ attr = getattr(mapped_class, key)
+ if hasattr(attr, "__call__") and hasattr(attr, "__self__"):
+ return types.MethodType(attr.__func__, self)
+
+ # attribute is a descriptor, that will be invoked against a
+ # "self"; so invoke the descriptor against this self
+ if hasattr(attr, "__get__"):
+ attr = attr.__get__(None, self)
+
+ # attributes within the QueryableAttribute system will want this
+ # to be invoked so the object can be adapted
+ if hasattr(attr, "adapt_to_entity"):
+ aliased_insp._weak_entity = weakref.ref(self)
+ attr = attr.adapt_to_entity(aliased_insp)
+ setattr(self, key, attr)
+
+ return attr
+
+ def __repr__(self) -> str:
+ return "<AliasedClass at 0x%x; %s>" % (
+ id(self),
+ self._aliased_insp._target.__name__,
+ )
+
+ def __str__(self) -> str:
+ return str(self._aliased_insp)
+
+
+@inspection._self_inspects
+class AliasedInsp(
+ ORMEntityColumnsClauseRole[_O],
+ ORMFromClauseRole,
+ HasCacheKey,
+ InspectionAttr,
+ MemoizedSlots,
+ inspection.Inspectable["AliasedInsp[_O]"],
+ Generic[_O],
+):
+ """Provide an inspection interface for an
+ :class:`.AliasedClass` object.
+
+ The :class:`.AliasedInsp` object is returned
+ given an :class:`.AliasedClass` using the
+ :func:`_sa.inspect` function::
+
+ from sqlalchemy import inspect
+ from sqlalchemy.orm import aliased
+
+ my_alias = aliased(MyMappedClass)
+ insp = inspect(my_alias)
+
+ Attributes on :class:`.AliasedInsp`
+ include:
+
+ * ``entity`` - the :class:`.AliasedClass` represented.
+ * ``mapper`` - the :class:`_orm.Mapper` mapping the underlying class.
+ * ``selectable`` - the :class:`_expression.Alias`
+ construct which ultimately
+ represents an aliased :class:`_schema.Table` or
+ :class:`_expression.Select`
+ construct.
+ * ``name`` - the name of the alias. Also is used as the attribute
+ name when returned in a result tuple from :class:`_query.Query`.
+ * ``with_polymorphic_mappers`` - collection of :class:`_orm.Mapper`
+ objects
+ indicating all those mappers expressed in the select construct
+ for the :class:`.AliasedClass`.
+ * ``polymorphic_on`` - an alternate column or SQL expression which
+ will be used as the "discriminator" for a polymorphic load.
+
+ .. seealso::
+
+ :ref:`inspection_toplevel`
+
+ """
+
+ __slots__ = (
+ "__weakref__",
+ "_weak_entity",
+ "mapper",
+ "selectable",
+ "name",
+ "_adapt_on_names",
+ "with_polymorphic_mappers",
+ "polymorphic_on",
+ "_use_mapper_path",
+ "_base_alias",
+ "represents_outer_join",
+ "persist_selectable",
+ "local_table",
+ "_is_with_polymorphic",
+ "_with_polymorphic_entities",
+ "_adapter",
+ "_target",
+ "__clause_element__",
+ "_memoized_values",
+ "_all_column_expressions",
+ "_nest_adapters",
+ )
+
+ _cache_key_traversal = [
+ ("name", visitors.ExtendedInternalTraversal.dp_string),
+ ("_adapt_on_names", visitors.ExtendedInternalTraversal.dp_boolean),
+ ("_use_mapper_path", visitors.ExtendedInternalTraversal.dp_boolean),
+ ("_target", visitors.ExtendedInternalTraversal.dp_inspectable),
+ ("selectable", visitors.ExtendedInternalTraversal.dp_clauseelement),
+ (
+ "with_polymorphic_mappers",
+ visitors.InternalTraversal.dp_has_cache_key_list,
+ ),
+ ("polymorphic_on", visitors.InternalTraversal.dp_clauseelement),
+ ]
+
+ mapper: Mapper[_O]
+ selectable: FromClause
+ _adapter: ORMAdapter
+ with_polymorphic_mappers: Sequence[Mapper[Any]]
+ _with_polymorphic_entities: Sequence[AliasedInsp[Any]]
+
+ _weak_entity: weakref.ref[AliasedClass[_O]]
+ """the AliasedClass that refers to this AliasedInsp"""
+
+ _target: Union[Type[_O], AliasedClass[_O]]
+ """the thing referenced by the AliasedClass/AliasedInsp.
+
+ In the vast majority of cases, this is the mapped class. However
+ it may also be another AliasedClass (alias of alias).
+
+ """
+
+ def __init__(
+ self,
+ entity: AliasedClass[_O],
+ inspected: _InternalEntityType[_O],
+ selectable: FromClause,
+ name: Optional[str],
+ with_polymorphic_mappers: Optional[Sequence[Mapper[Any]]],
+ polymorphic_on: Optional[ColumnElement[Any]],
+ _base_alias: Optional[AliasedInsp[Any]],
+ _use_mapper_path: bool,
+ adapt_on_names: bool,
+ represents_outer_join: bool,
+ nest_adapters: bool,
+ ):
+ mapped_class_or_ac = inspected.entity
+ mapper = inspected.mapper
+
+ self._weak_entity = weakref.ref(entity)
+ self.mapper = mapper
+ self.selectable = self.persist_selectable = self.local_table = (
+ selectable
+ )
+ self.name = name
+ self.polymorphic_on = polymorphic_on
+ self._base_alias = weakref.ref(_base_alias or self)
+ self._use_mapper_path = _use_mapper_path
+ self.represents_outer_join = represents_outer_join
+ self._nest_adapters = nest_adapters
+
+ if with_polymorphic_mappers:
+ self._is_with_polymorphic = True
+ self.with_polymorphic_mappers = with_polymorphic_mappers
+ self._with_polymorphic_entities = []
+ for poly in self.with_polymorphic_mappers:
+ if poly is not mapper:
+ ent = AliasedClass(
+ poly.class_,
+ selectable,
+ base_alias=self,
+ adapt_on_names=adapt_on_names,
+ use_mapper_path=_use_mapper_path,
+ )
+
+ setattr(self.entity, poly.class_.__name__, ent)
+ self._with_polymorphic_entities.append(ent._aliased_insp)
+
+ else:
+ self._is_with_polymorphic = False
+ self.with_polymorphic_mappers = [mapper]
+
+ self._adapter = ORMAdapter(
+ _TraceAdaptRole.ALIASED_INSP,
+ mapper,
+ selectable=selectable,
+ equivalents=mapper._equivalent_columns,
+ adapt_on_names=adapt_on_names,
+ anonymize_labels=True,
+ # make sure the adapter doesn't try to grab other tables that
+ # are not even the thing we are mapping, such as embedded
+ # selectables in subqueries or CTEs. See issue #6060
+ adapt_from_selectables={
+ m.selectable
+ for m in self.with_polymorphic_mappers
+ if not adapt_on_names
+ },
+ limit_on_entity=False,
+ )
+
+ if nest_adapters:
+ # supports "aliased class of aliased class" use case
+ assert isinstance(inspected, AliasedInsp)
+ self._adapter = inspected._adapter.wrap(self._adapter)
+
+ self._adapt_on_names = adapt_on_names
+ self._target = mapped_class_or_ac
+
+ @classmethod
+ def _alias_factory(
+ cls,
+ element: Union[_EntityType[_O], FromClause],
+ alias: Optional[FromClause] = None,
+ name: Optional[str] = None,
+ flat: bool = False,
+ adapt_on_names: bool = False,
+ ) -> Union[AliasedClass[_O], FromClause]:
+ if isinstance(element, FromClause):
+ if adapt_on_names:
+ raise sa_exc.ArgumentError(
+ "adapt_on_names only applies to ORM elements"
+ )
+ if name:
+ return element.alias(name=name, flat=flat)
+ else:
+ return coercions.expect(
+ roles.AnonymizedFromClauseRole, element, flat=flat
+ )
+ else:
+ return AliasedClass(
+ element,
+ alias=alias,
+ flat=flat,
+ name=name,
+ adapt_on_names=adapt_on_names,
+ )
+
+ @classmethod
+ def _with_polymorphic_factory(
+ cls,
+ base: Union[Type[_O], Mapper[_O]],
+ classes: Union[Literal["*"], Iterable[_EntityType[Any]]],
+ selectable: Union[Literal[False, None], FromClause] = False,
+ flat: bool = False,
+ polymorphic_on: Optional[ColumnElement[Any]] = None,
+ aliased: bool = False,
+ innerjoin: bool = False,
+ adapt_on_names: bool = False,
+ _use_mapper_path: bool = False,
+ ) -> AliasedClass[_O]:
+ primary_mapper = _class_to_mapper(base)
+
+ if selectable not in (None, False) and flat:
+ raise sa_exc.ArgumentError(
+ "the 'flat' and 'selectable' arguments cannot be passed "
+ "simultaneously to with_polymorphic()"
+ )
+
+ mappers, selectable = primary_mapper._with_polymorphic_args(
+ classes, selectable, innerjoin=innerjoin
+ )
+ if aliased or flat:
+ assert selectable is not None
+ selectable = selectable._anonymous_fromclause(flat=flat)
+
+ return AliasedClass(
+ base,
+ selectable,
+ with_polymorphic_mappers=mappers,
+ adapt_on_names=adapt_on_names,
+ with_polymorphic_discriminator=polymorphic_on,
+ use_mapper_path=_use_mapper_path,
+ represents_outer_join=not innerjoin,
+ )
+
+ @property
+ def entity(self) -> AliasedClass[_O]:
+ # to eliminate reference cycles, the AliasedClass is held weakly.
+ # this produces some situations where the AliasedClass gets lost,
+ # particularly when one is created internally and only the AliasedInsp
+ # is passed around.
+ # to work around this case, we just generate a new one when we need
+ # it, as it is a simple class with very little initial state on it.
+ ent = self._weak_entity()
+ if ent is None:
+ ent = AliasedClass._reconstitute_from_aliased_insp(self)
+ self._weak_entity = weakref.ref(ent)
+ return ent
+
+ is_aliased_class = True
+ "always returns True"
+
+ def _memoized_method___clause_element__(self) -> FromClause:
+ return self.selectable._annotate(
+ {
+ "parentmapper": self.mapper,
+ "parententity": self,
+ "entity_namespace": self,
+ }
+ )._set_propagate_attrs(
+ {"compile_state_plugin": "orm", "plugin_subject": self}
+ )
+
+ @property
+ def entity_namespace(self) -> AliasedClass[_O]:
+ return self.entity
+
+ @property
+ def class_(self) -> Type[_O]:
+ """Return the mapped class ultimately represented by this
+ :class:`.AliasedInsp`."""
+ return self.mapper.class_
+
+ @property
+ def _path_registry(self) -> AbstractEntityRegistry:
+ if self._use_mapper_path:
+ return self.mapper._path_registry
+ else:
+ return PathRegistry.per_mapper(self)
+
+ def __getstate__(self) -> Dict[str, Any]:
+ return {
+ "entity": self.entity,
+ "mapper": self.mapper,
+ "alias": self.selectable,
+ "name": self.name,
+ "adapt_on_names": self._adapt_on_names,
+ "with_polymorphic_mappers": self.with_polymorphic_mappers,
+ "with_polymorphic_discriminator": self.polymorphic_on,
+ "base_alias": self._base_alias(),
+ "use_mapper_path": self._use_mapper_path,
+ "represents_outer_join": self.represents_outer_join,
+ "nest_adapters": self._nest_adapters,
+ }
+
+ def __setstate__(self, state: Dict[str, Any]) -> None:
+ self.__init__( # type: ignore
+ state["entity"],
+ state["mapper"],
+ state["alias"],
+ state["name"],
+ state["with_polymorphic_mappers"],
+ state["with_polymorphic_discriminator"],
+ state["base_alias"],
+ state["use_mapper_path"],
+ state["adapt_on_names"],
+ state["represents_outer_join"],
+ state["nest_adapters"],
+ )
+
+ def _merge_with(self, other: AliasedInsp[_O]) -> AliasedInsp[_O]:
+ # assert self._is_with_polymorphic
+ # assert other._is_with_polymorphic
+
+ primary_mapper = other.mapper
+
+ assert self.mapper is primary_mapper
+
+ our_classes = util.to_set(
+ mp.class_ for mp in self.with_polymorphic_mappers
+ )
+ new_classes = {mp.class_ for mp in other.with_polymorphic_mappers}
+ if our_classes == new_classes:
+ return other
+ else:
+ classes = our_classes.union(new_classes)
+
+ mappers, selectable = primary_mapper._with_polymorphic_args(
+ classes, None, innerjoin=not other.represents_outer_join
+ )
+ selectable = selectable._anonymous_fromclause(flat=True)
+ return AliasedClass(
+ primary_mapper,
+ selectable,
+ with_polymorphic_mappers=mappers,
+ with_polymorphic_discriminator=other.polymorphic_on,
+ use_mapper_path=other._use_mapper_path,
+ represents_outer_join=other.represents_outer_join,
+ )._aliased_insp
+
+ def _adapt_element(
+ self, expr: _ORMCOLEXPR, key: Optional[str] = None
+ ) -> _ORMCOLEXPR:
+ assert isinstance(expr, ColumnElement)
+ d: Dict[str, Any] = {
+ "parententity": self,
+ "parentmapper": self.mapper,
+ }
+ if key:
+ d["proxy_key"] = key
+
+ # IMO mypy should see this one also as returning the same type
+ # we put into it, but it's not
+ return (
+ self._adapter.traverse(expr)
+ ._annotate(d)
+ ._set_propagate_attrs(
+ {"compile_state_plugin": "orm", "plugin_subject": self}
+ )
+ )
+
+ if TYPE_CHECKING:
+ # establish compatibility with the _ORMAdapterProto protocol,
+ # which in turn is compatible with _CoreAdapterProto.
+
+ def _orm_adapt_element(
+ self,
+ obj: _CE,
+ key: Optional[str] = None,
+ ) -> _CE: ...
+
+ else:
+ _orm_adapt_element = _adapt_element
+
+ def _entity_for_mapper(self, mapper):
+ self_poly = self.with_polymorphic_mappers
+ if mapper in self_poly:
+ if mapper is self.mapper:
+ return self
+ else:
+ return getattr(
+ self.entity, mapper.class_.__name__
+ )._aliased_insp
+ elif mapper.isa(self.mapper):
+ return self
+ else:
+ assert False, "mapper %s doesn't correspond to %s" % (mapper, self)
+
+ def _memoized_attr__get_clause(self):
+ onclause, replacemap = self.mapper._get_clause
+ return (
+ self._adapter.traverse(onclause),
+ {
+ self._adapter.traverse(col): param
+ for col, param in replacemap.items()
+ },
+ )
+
+ def _memoized_attr__memoized_values(self):
+ return {}
+
+ def _memoized_attr__all_column_expressions(self):
+ if self._is_with_polymorphic:
+ cols_plus_keys = self.mapper._columns_plus_keys(
+ [ent.mapper for ent in self._with_polymorphic_entities]
+ )
+ else:
+ cols_plus_keys = self.mapper._columns_plus_keys()
+
+ cols_plus_keys = [
+ (key, self._adapt_element(col)) for key, col in cols_plus_keys
+ ]
+
+ return ColumnCollection(cols_plus_keys)
+
+ def _memo(self, key, callable_, *args, **kw):
+ if key in self._memoized_values:
+ return self._memoized_values[key]
+ else:
+ self._memoized_values[key] = value = callable_(*args, **kw)
+ return value
+
+ def __repr__(self):
+ if self.with_polymorphic_mappers:
+ with_poly = "(%s)" % ", ".join(
+ mp.class_.__name__ for mp in self.with_polymorphic_mappers
+ )
+ else:
+ with_poly = ""
+ return "<AliasedInsp at 0x%x; %s%s>" % (
+ id(self),
+ self.class_.__name__,
+ with_poly,
+ )
+
+ def __str__(self):
+ if self._is_with_polymorphic:
+ return "with_polymorphic(%s, [%s])" % (
+ self._target.__name__,
+ ", ".join(
+ mp.class_.__name__
+ for mp in self.with_polymorphic_mappers
+ if mp is not self.mapper
+ ),
+ )
+ else:
+ return "aliased(%s)" % (self._target.__name__,)
+
+
+class _WrapUserEntity:
+ """A wrapper used within the loader_criteria lambda caller so that
+ we can bypass declared_attr descriptors on unmapped mixins, which
+ normally emit a warning for such use.
+
+ might also be useful for other per-lambda instrumentations should
+ the need arise.
+
+ """
+
+ __slots__ = ("subject",)
+
+ def __init__(self, subject):
+ self.subject = subject
+
+ @util.preload_module("sqlalchemy.orm.decl_api")
+ def __getattribute__(self, name):
+ decl_api = util.preloaded.orm.decl_api
+
+ subject = object.__getattribute__(self, "subject")
+ if name in subject.__dict__ and isinstance(
+ subject.__dict__[name], decl_api.declared_attr
+ ):
+ return subject.__dict__[name].fget(subject)
+ else:
+ return getattr(subject, name)
+
+
+class LoaderCriteriaOption(CriteriaOption):
+ """Add additional WHERE criteria to the load for all occurrences of
+ a particular entity.
+
+ :class:`_orm.LoaderCriteriaOption` is invoked using the
+ :func:`_orm.with_loader_criteria` function; see that function for
+ details.
+
+ .. versionadded:: 1.4
+
+ """
+
+ __slots__ = (
+ "root_entity",
+ "entity",
+ "deferred_where_criteria",
+ "where_criteria",
+ "_where_crit_orig",
+ "include_aliases",
+ "propagate_to_loaders",
+ )
+
+ _traverse_internals = [
+ ("root_entity", visitors.ExtendedInternalTraversal.dp_plain_obj),
+ ("entity", visitors.ExtendedInternalTraversal.dp_has_cache_key),
+ ("where_criteria", visitors.InternalTraversal.dp_clauseelement),
+ ("include_aliases", visitors.InternalTraversal.dp_boolean),
+ ("propagate_to_loaders", visitors.InternalTraversal.dp_boolean),
+ ]
+
+ root_entity: Optional[Type[Any]]
+ entity: Optional[_InternalEntityType[Any]]
+ where_criteria: Union[ColumnElement[bool], lambdas.DeferredLambdaElement]
+ deferred_where_criteria: bool
+ include_aliases: bool
+ propagate_to_loaders: bool
+
+ _where_crit_orig: Any
+
+ def __init__(
+ self,
+ entity_or_base: _EntityType[Any],
+ where_criteria: Union[
+ _ColumnExpressionArgument[bool],
+ Callable[[Any], _ColumnExpressionArgument[bool]],
+ ],
+ loader_only: bool = False,
+ include_aliases: bool = False,
+ propagate_to_loaders: bool = True,
+ track_closure_variables: bool = True,
+ ):
+ entity = cast(
+ "_InternalEntityType[Any]",
+ inspection.inspect(entity_or_base, False),
+ )
+ if entity is None:
+ self.root_entity = cast("Type[Any]", entity_or_base)
+ self.entity = None
+ else:
+ self.root_entity = None
+ self.entity = entity
+
+ self._where_crit_orig = where_criteria
+ if callable(where_criteria):
+ if self.root_entity is not None:
+ wrap_entity = self.root_entity
+ else:
+ assert entity is not None
+ wrap_entity = entity.entity
+
+ self.deferred_where_criteria = True
+ self.where_criteria = lambdas.DeferredLambdaElement(
+ where_criteria,
+ roles.WhereHavingRole,
+ lambda_args=(_WrapUserEntity(wrap_entity),),
+ opts=lambdas.LambdaOptions(
+ track_closure_variables=track_closure_variables
+ ),
+ )
+ else:
+ self.deferred_where_criteria = False
+ self.where_criteria = coercions.expect(
+ roles.WhereHavingRole, where_criteria
+ )
+
+ self.include_aliases = include_aliases
+ self.propagate_to_loaders = propagate_to_loaders
+
+ @classmethod
+ def _unreduce(
+ cls, entity, where_criteria, include_aliases, propagate_to_loaders
+ ):
+ return LoaderCriteriaOption(
+ entity,
+ where_criteria,
+ include_aliases=include_aliases,
+ propagate_to_loaders=propagate_to_loaders,
+ )
+
+ def __reduce__(self):
+ return (
+ LoaderCriteriaOption._unreduce,
+ (
+ self.entity.class_ if self.entity else self.root_entity,
+ self._where_crit_orig,
+ self.include_aliases,
+ self.propagate_to_loaders,
+ ),
+ )
+
+ def _all_mappers(self) -> Iterator[Mapper[Any]]:
+ if self.entity:
+ yield from self.entity.mapper.self_and_descendants
+ else:
+ assert self.root_entity
+ stack = list(self.root_entity.__subclasses__())
+ while stack:
+ subclass = stack.pop(0)
+ ent = cast(
+ "_InternalEntityType[Any]",
+ inspection.inspect(subclass, raiseerr=False),
+ )
+ if ent:
+ yield from ent.mapper.self_and_descendants
+ else:
+ stack.extend(subclass.__subclasses__())
+
+ def _should_include(self, compile_state: ORMCompileState) -> bool:
+ if (
+ compile_state.select_statement._annotations.get(
+ "for_loader_criteria", None
+ )
+ is self
+ ):
+ return False
+ return True
+
+ def _resolve_where_criteria(
+ self, ext_info: _InternalEntityType[Any]
+ ) -> ColumnElement[bool]:
+ if self.deferred_where_criteria:
+ crit = cast(
+ "ColumnElement[bool]",
+ self.where_criteria._resolve_with_args(ext_info.entity),
+ )
+ else:
+ crit = self.where_criteria # type: ignore
+ assert isinstance(crit, ColumnElement)
+ return sql_util._deep_annotate(
+ crit,
+ {"for_loader_criteria": self},
+ detect_subquery_cols=True,
+ ind_cols_on_fromclause=True,
+ )
+
+ def process_compile_state_replaced_entities(
+ self,
+ compile_state: ORMCompileState,
+ mapper_entities: Iterable[_MapperEntity],
+ ) -> None:
+ self.process_compile_state(compile_state)
+
+ def process_compile_state(self, compile_state: ORMCompileState) -> None:
+ """Apply a modification to a given :class:`.CompileState`."""
+
+ # if options to limit the criteria to immediate query only,
+ # use compile_state.attributes instead
+
+ self.get_global_criteria(compile_state.global_attributes)
+
+ def get_global_criteria(self, attributes: Dict[Any, Any]) -> None:
+ for mp in self._all_mappers():
+ load_criteria = attributes.setdefault(
+ ("additional_entity_criteria", mp), []
+ )
+
+ load_criteria.append(self)
+
+
+inspection._inspects(AliasedClass)(lambda target: target._aliased_insp)
+
+
+@inspection._inspects(type)
+def _inspect_mc(
+ class_: Type[_O],
+) -> Optional[Mapper[_O]]:
+ try:
+ class_manager = opt_manager_of_class(class_)
+ if class_manager is None or not class_manager.is_mapped:
+ return None
+ mapper = class_manager.mapper
+ except exc.NO_STATE:
+ return None
+ else:
+ return mapper
+
+
+GenericAlias = type(List[Any])
+
+
+@inspection._inspects(GenericAlias)
+def _inspect_generic_alias(
+ class_: Type[_O],
+) -> Optional[Mapper[_O]]:
+ origin = cast("Type[_O]", typing_get_origin(class_))
+ return _inspect_mc(origin)
+
+
+@inspection._self_inspects
+class Bundle(
+ ORMColumnsClauseRole[_T],
+ SupportsCloneAnnotations,
+ MemoizedHasCacheKey,
+ inspection.Inspectable["Bundle[_T]"],
+ InspectionAttr,
+):
+ """A grouping of SQL expressions that are returned by a :class:`.Query`
+ under one namespace.
+
+ The :class:`.Bundle` essentially allows nesting of the tuple-based
+ results returned by a column-oriented :class:`_query.Query` object.
+ It also
+ is extensible via simple subclassing, where the primary capability
+ to override is that of how the set of expressions should be returned,
+ allowing post-processing as well as custom return types, without
+ involving ORM identity-mapped classes.
+
+ .. seealso::
+
+ :ref:`bundles`
+
+
+ """
+
+ single_entity = False
+ """If True, queries for a single Bundle will be returned as a single
+ entity, rather than an element within a keyed tuple."""
+
+ is_clause_element = False
+
+ is_mapper = False
+
+ is_aliased_class = False
+
+ is_bundle = True
+
+ _propagate_attrs: _PropagateAttrsType = util.immutabledict()
+
+ proxy_set = util.EMPTY_SET # type: ignore
+
+ exprs: List[_ColumnsClauseElement]
+
+ def __init__(
+ self, name: str, *exprs: _ColumnExpressionArgument[Any], **kw: Any
+ ):
+ r"""Construct a new :class:`.Bundle`.
+
+ e.g.::
+
+ bn = Bundle("mybundle", MyClass.x, MyClass.y)
+
+ for row in session.query(bn).filter(
+ bn.c.x == 5).filter(bn.c.y == 4):
+ print(row.mybundle.x, row.mybundle.y)
+
+ :param name: name of the bundle.
+ :param \*exprs: columns or SQL expressions comprising the bundle.
+ :param single_entity=False: if True, rows for this :class:`.Bundle`
+ can be returned as a "single entity" outside of any enclosing tuple
+ in the same manner as a mapped entity.
+
+ """
+ self.name = self._label = name
+ coerced_exprs = [
+ coercions.expect(
+ roles.ColumnsClauseRole, expr, apply_propagate_attrs=self
+ )
+ for expr in exprs
+ ]
+ self.exprs = coerced_exprs
+
+ self.c = self.columns = ColumnCollection(
+ (getattr(col, "key", col._label), col)
+ for col in [e._annotations.get("bundle", e) for e in coerced_exprs]
+ ).as_readonly()
+ self.single_entity = kw.pop("single_entity", self.single_entity)
+
+ def _gen_cache_key(
+ self, anon_map: anon_map, bindparams: List[BindParameter[Any]]
+ ) -> Tuple[Any, ...]:
+ return (self.__class__, self.name, self.single_entity) + tuple(
+ [expr._gen_cache_key(anon_map, bindparams) for expr in self.exprs]
+ )
+
+ @property
+ def mapper(self) -> Optional[Mapper[Any]]:
+ mp: Optional[Mapper[Any]] = self.exprs[0]._annotations.get(
+ "parentmapper", None
+ )
+ return mp
+
+ @property
+ def entity(self) -> Optional[_InternalEntityType[Any]]:
+ ie: Optional[_InternalEntityType[Any]] = self.exprs[
+ 0
+ ]._annotations.get("parententity", None)
+ return ie
+
+ @property
+ def entity_namespace(
+ self,
+ ) -> ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]:
+ return self.c
+
+ columns: ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]
+
+ """A namespace of SQL expressions referred to by this :class:`.Bundle`.
+
+ e.g.::
+
+ bn = Bundle("mybundle", MyClass.x, MyClass.y)
+
+ q = sess.query(bn).filter(bn.c.x == 5)
+
+ Nesting of bundles is also supported::
+
+ b1 = Bundle("b1",
+ Bundle('b2', MyClass.a, MyClass.b),
+ Bundle('b3', MyClass.x, MyClass.y)
+ )
+
+ q = sess.query(b1).filter(
+ b1.c.b2.c.a == 5).filter(b1.c.b3.c.y == 9)
+
+ .. seealso::
+
+ :attr:`.Bundle.c`
+
+ """
+
+ c: ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]
+ """An alias for :attr:`.Bundle.columns`."""
+
+ def _clone(self):
+ cloned = self.__class__.__new__(self.__class__)
+ cloned.__dict__.update(self.__dict__)
+ return cloned
+
+ def __clause_element__(self):
+ # ensure existing entity_namespace remains
+ annotations = {"bundle": self, "entity_namespace": self}
+ annotations.update(self._annotations)
+
+ plugin_subject = self.exprs[0]._propagate_attrs.get(
+ "plugin_subject", self.entity
+ )
+ return (
+ expression.ClauseList(
+ _literal_as_text_role=roles.ColumnsClauseRole,
+ group=False,
+ *[e._annotations.get("bundle", e) for e in self.exprs],
+ )
+ ._annotate(annotations)
+ ._set_propagate_attrs(
+ # the Bundle *must* use the orm plugin no matter what. the
+ # subject can be None but it's much better if it's not.
+ {
+ "compile_state_plugin": "orm",
+ "plugin_subject": plugin_subject,
+ }
+ )
+ )
+
+ @property
+ def clauses(self):
+ return self.__clause_element__().clauses
+
+ def label(self, name):
+ """Provide a copy of this :class:`.Bundle` passing a new label."""
+
+ cloned = self._clone()
+ cloned.name = name
+ return cloned
+
+ def create_row_processor(
+ self,
+ query: Select[Any],
+ procs: Sequence[Callable[[Row[Any]], Any]],
+ labels: Sequence[str],
+ ) -> Callable[[Row[Any]], Any]:
+ """Produce the "row processing" function for this :class:`.Bundle`.
+
+ May be overridden by subclasses to provide custom behaviors when
+ results are fetched. The method is passed the statement object and a
+ set of "row processor" functions at query execution time; these
+ processor functions when given a result row will return the individual
+ attribute value, which can then be adapted into any kind of return data
+ structure.
+
+ The example below illustrates replacing the usual :class:`.Row`
+ return structure with a straight Python dictionary::
+
+ from sqlalchemy.orm import Bundle
+
+ class DictBundle(Bundle):
+ def create_row_processor(self, query, procs, labels):
+ 'Override create_row_processor to return values as
+ dictionaries'
+
+ def proc(row):
+ return dict(
+ zip(labels, (proc(row) for proc in procs))
+ )
+ return proc
+
+ A result from the above :class:`_orm.Bundle` will return dictionary
+ values::
+
+ bn = DictBundle('mybundle', MyClass.data1, MyClass.data2)
+ for row in session.execute(select(bn)).where(bn.c.data1 == 'd1'):
+ print(row.mybundle['data1'], row.mybundle['data2'])
+
+ """
+ keyed_tuple = result_tuple(labels, [() for l in labels])
+
+ def proc(row: Row[Any]) -> Any:
+ return keyed_tuple([proc(row) for proc in procs])
+
+ return proc
+
+
+def _orm_annotate(element: _SA, exclude: Optional[Any] = None) -> _SA:
+ """Deep copy the given ClauseElement, annotating each element with the
+ "_orm_adapt" flag.
+
+ Elements within the exclude collection will be cloned but not annotated.
+
+ """
+ return sql_util._deep_annotate(element, {"_orm_adapt": True}, exclude)
+
+
+def _orm_deannotate(element: _SA) -> _SA:
+ """Remove annotations that link a column to a particular mapping.
+
+ Note this doesn't affect "remote" and "foreign" annotations
+ passed by the :func:`_orm.foreign` and :func:`_orm.remote`
+ annotators.
+
+ """
+
+ return sql_util._deep_deannotate(
+ element, values=("_orm_adapt", "parententity")
+ )
+
+
+def _orm_full_deannotate(element: _SA) -> _SA:
+ return sql_util._deep_deannotate(element)
+
+
+class _ORMJoin(expression.Join):
+ """Extend Join to support ORM constructs as input."""
+
+ __visit_name__ = expression.Join.__visit_name__
+
+ inherit_cache = True
+
+ def __init__(
+ self,
+ left: _FromClauseArgument,
+ right: _FromClauseArgument,
+ onclause: Optional[_OnClauseArgument] = None,
+ isouter: bool = False,
+ full: bool = False,
+ _left_memo: Optional[Any] = None,
+ _right_memo: Optional[Any] = None,
+ _extra_criteria: Tuple[ColumnElement[bool], ...] = (),
+ ):
+ left_info = cast(
+ "Union[FromClause, _InternalEntityType[Any]]",
+ inspection.inspect(left),
+ )
+
+ right_info = cast(
+ "Union[FromClause, _InternalEntityType[Any]]",
+ inspection.inspect(right),
+ )
+ adapt_to = right_info.selectable
+
+ # used by joined eager loader
+ self._left_memo = _left_memo
+ self._right_memo = _right_memo
+
+ if isinstance(onclause, attributes.QueryableAttribute):
+ if TYPE_CHECKING:
+ assert isinstance(
+ onclause.comparator, RelationshipProperty.Comparator
+ )
+ on_selectable = onclause.comparator._source_selectable()
+ prop = onclause.property
+ _extra_criteria += onclause._extra_criteria
+ elif isinstance(onclause, MapperProperty):
+ # used internally by joined eager loader...possibly not ideal
+ prop = onclause
+ on_selectable = prop.parent.selectable
+ else:
+ prop = None
+ on_selectable = None
+
+ left_selectable = left_info.selectable
+ if prop:
+ adapt_from: Optional[FromClause]
+ if sql_util.clause_is_present(on_selectable, left_selectable):
+ adapt_from = on_selectable
+ else:
+ assert isinstance(left_selectable, FromClause)
+ adapt_from = left_selectable
+
+ (
+ pj,
+ sj,
+ source,
+ dest,
+ secondary,
+ target_adapter,
+ ) = prop._create_joins(
+ source_selectable=adapt_from,
+ dest_selectable=adapt_to,
+ source_polymorphic=True,
+ of_type_entity=right_info,
+ alias_secondary=True,
+ extra_criteria=_extra_criteria,
+ )
+
+ if sj is not None:
+ if isouter:
+ # note this is an inner join from secondary->right
+ right = sql.join(secondary, right, sj)
+ onclause = pj
+ else:
+ left = sql.join(left, secondary, pj, isouter)
+ onclause = sj
+ else:
+ onclause = pj
+
+ self._target_adapter = target_adapter
+
+ # we don't use the normal coercions logic for _ORMJoin
+ # (probably should), so do some gymnastics to get the entity.
+ # logic here is for #8721, which was a major bug in 1.4
+ # for almost two years, not reported/fixed until 1.4.43 (!)
+ if is_selectable(left_info):
+ parententity = left_selectable._annotations.get(
+ "parententity", None
+ )
+ elif insp_is_mapper(left_info) or insp_is_aliased_class(left_info):
+ parententity = left_info
+ else:
+ parententity = None
+
+ if parententity is not None:
+ self._annotations = self._annotations.union(
+ {"parententity": parententity}
+ )
+
+ augment_onclause = bool(_extra_criteria) and not prop
+ expression.Join.__init__(self, left, right, onclause, isouter, full)
+
+ assert self.onclause is not None
+
+ if augment_onclause:
+ self.onclause &= sql.and_(*_extra_criteria)
+
+ if (
+ not prop
+ and getattr(right_info, "mapper", None)
+ and right_info.mapper.single # type: ignore
+ ):
+ right_info = cast("_InternalEntityType[Any]", right_info)
+ # if single inheritance target and we are using a manual
+ # or implicit ON clause, augment it the same way we'd augment the
+ # WHERE.
+ single_crit = right_info.mapper._single_table_criterion
+ if single_crit is not None:
+ if insp_is_aliased_class(right_info):
+ single_crit = right_info._adapter.traverse(single_crit)
+ self.onclause = self.onclause & single_crit
+
+ def _splice_into_center(self, other):
+ """Splice a join into the center.
+
+ Given join(a, b) and join(b, c), return join(a, b).join(c)
+
+ """
+ leftmost = other
+ while isinstance(leftmost, sql.Join):
+ leftmost = leftmost.left
+
+ assert self.right is leftmost
+
+ left = _ORMJoin(
+ self.left,
+ other.left,
+ self.onclause,
+ isouter=self.isouter,
+ _left_memo=self._left_memo,
+ _right_memo=other._left_memo,
+ )
+
+ return _ORMJoin(
+ left,
+ other.right,
+ other.onclause,
+ isouter=other.isouter,
+ _right_memo=other._right_memo,
+ )
+
+ def join(
+ self,
+ right: _FromClauseArgument,
+ onclause: Optional[_OnClauseArgument] = None,
+ isouter: bool = False,
+ full: bool = False,
+ ) -> _ORMJoin:
+ return _ORMJoin(self, right, onclause, full=full, isouter=isouter)
+
+ def outerjoin(
+ self,
+ right: _FromClauseArgument,
+ onclause: Optional[_OnClauseArgument] = None,
+ full: bool = False,
+ ) -> _ORMJoin:
+ return _ORMJoin(self, right, onclause, isouter=True, full=full)
+
+
+def with_parent(
+ instance: object,
+ prop: attributes.QueryableAttribute[Any],
+ from_entity: Optional[_EntityType[Any]] = None,
+) -> ColumnElement[bool]:
+ """Create filtering criterion that relates this query's primary entity
+ to the given related instance, using established
+ :func:`_orm.relationship()`
+ configuration.
+
+ E.g.::
+
+ stmt = select(Address).where(with_parent(some_user, User.addresses))
+
+
+ The SQL rendered is the same as that rendered when a lazy loader
+ would fire off from the given parent on that attribute, meaning
+ that the appropriate state is taken from the parent object in
+ Python without the need to render joins to the parent table
+ in the rendered statement.
+
+ The given property may also make use of :meth:`_orm.PropComparator.of_type`
+ to indicate the left side of the criteria::
+
+
+ a1 = aliased(Address)
+ a2 = aliased(Address)
+ stmt = select(a1, a2).where(
+ with_parent(u1, User.addresses.of_type(a2))
+ )
+
+ The above use is equivalent to using the
+ :func:`_orm.with_parent.from_entity` argument::
+
+ a1 = aliased(Address)
+ a2 = aliased(Address)
+ stmt = select(a1, a2).where(
+ with_parent(u1, User.addresses, from_entity=a2)
+ )
+
+ :param instance:
+ An instance which has some :func:`_orm.relationship`.
+
+ :param property:
+ Class-bound attribute, which indicates
+ what relationship from the instance should be used to reconcile the
+ parent/child relationship.
+
+ :param from_entity:
+ Entity in which to consider as the left side. This defaults to the
+ "zero" entity of the :class:`_query.Query` itself.
+
+ .. versionadded:: 1.2
+
+ """
+ prop_t: RelationshipProperty[Any]
+
+ if isinstance(prop, str):
+ raise sa_exc.ArgumentError(
+ "with_parent() accepts class-bound mapped attributes, not strings"
+ )
+ elif isinstance(prop, attributes.QueryableAttribute):
+ if prop._of_type:
+ from_entity = prop._of_type
+ mapper_property = prop.property
+ if mapper_property is None or not prop_is_relationship(
+ mapper_property
+ ):
+ raise sa_exc.ArgumentError(
+ f"Expected relationship property for with_parent(), "
+ f"got {mapper_property}"
+ )
+ prop_t = mapper_property
+ else:
+ prop_t = prop
+
+ return prop_t._with_parent(instance, from_entity=from_entity)
+
+
+def has_identity(object_: object) -> bool:
+ """Return True if the given object has a database
+ identity.
+
+ This typically corresponds to the object being
+ in either the persistent or detached state.
+
+ .. seealso::
+
+ :func:`.was_deleted`
+
+ """
+ state = attributes.instance_state(object_)
+ return state.has_identity
+
+
+def was_deleted(object_: object) -> bool:
+ """Return True if the given object was deleted
+ within a session flush.
+
+ This is regardless of whether or not the object is
+ persistent or detached.
+
+ .. seealso::
+
+ :attr:`.InstanceState.was_deleted`
+
+ """
+
+ state = attributes.instance_state(object_)
+ return state.was_deleted
+
+
+def _entity_corresponds_to(
+ given: _InternalEntityType[Any], entity: _InternalEntityType[Any]
+) -> bool:
+ """determine if 'given' corresponds to 'entity', in terms
+ of an entity passed to Query that would match the same entity
+ being referred to elsewhere in the query.
+
+ """
+ if insp_is_aliased_class(entity):
+ if insp_is_aliased_class(given):
+ if entity._base_alias() is given._base_alias():
+ return True
+ return False
+ elif insp_is_aliased_class(given):
+ if given._use_mapper_path:
+ return entity in given.with_polymorphic_mappers
+ else:
+ return entity is given
+
+ assert insp_is_mapper(given)
+ return entity.common_parent(given)
+
+
+def _entity_corresponds_to_use_path_impl(
+ given: _InternalEntityType[Any], entity: _InternalEntityType[Any]
+) -> bool:
+ """determine if 'given' corresponds to 'entity', in terms
+ of a path of loader options where a mapped attribute is taken to
+ be a member of a parent entity.
+
+ e.g.::
+
+ someoption(A).someoption(A.b) # -> fn(A, A) -> True
+ someoption(A).someoption(C.d) # -> fn(A, C) -> False
+
+ a1 = aliased(A)
+ someoption(a1).someoption(A.b) # -> fn(a1, A) -> False
+ someoption(a1).someoption(a1.b) # -> fn(a1, a1) -> True
+
+ wp = with_polymorphic(A, [A1, A2])
+ someoption(wp).someoption(A1.foo) # -> fn(wp, A1) -> False
+ someoption(wp).someoption(wp.A1.foo) # -> fn(wp, wp.A1) -> True
+
+
+ """
+ if insp_is_aliased_class(given):
+ return (
+ insp_is_aliased_class(entity)
+ and not entity._use_mapper_path
+ and (given is entity or entity in given._with_polymorphic_entities)
+ )
+ elif not insp_is_aliased_class(entity):
+ return given.isa(entity.mapper)
+ else:
+ return (
+ entity._use_mapper_path
+ and given in entity.with_polymorphic_mappers
+ )
+
+
+def _entity_isa(given: _InternalEntityType[Any], mapper: Mapper[Any]) -> bool:
+ """determine if 'given' "is a" mapper, in terms of the given
+ would load rows of type 'mapper'.
+
+ """
+ if given.is_aliased_class:
+ return mapper in given.with_polymorphic_mappers or given.mapper.isa(
+ mapper
+ )
+ elif given.with_polymorphic_mappers:
+ return mapper in given.with_polymorphic_mappers
+ else:
+ return given.isa(mapper)
+
+
+def _getitem(iterable_query: Query[Any], item: Any) -> Any:
+ """calculate __getitem__ in terms of an iterable query object
+ that also has a slice() method.
+
+ """
+
+ def _no_negative_indexes():
+ raise IndexError(
+ "negative indexes are not accepted by SQL "
+ "index / slice operators"
+ )
+
+ if isinstance(item, slice):
+ start, stop, step = util.decode_slice(item)
+
+ if (
+ isinstance(stop, int)
+ and isinstance(start, int)
+ and stop - start <= 0
+ ):
+ return []
+
+ elif (isinstance(start, int) and start < 0) or (
+ isinstance(stop, int) and stop < 0
+ ):
+ _no_negative_indexes()
+
+ 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:
+ _no_negative_indexes()
+ else:
+ return list(iterable_query[item : item + 1])[0]
+
+
+def _is_mapped_annotation(
+ raw_annotation: _AnnotationScanType,
+ cls: Type[Any],
+ originating_cls: Type[Any],
+) -> bool:
+ try:
+ annotated = de_stringify_annotation(
+ cls, raw_annotation, originating_cls.__module__
+ )
+ except NameError:
+ # in most cases, at least within our own tests, we can raise
+ # here, which is more accurate as it prevents us from returning
+ # false negatives. However, in the real world, try to avoid getting
+ # involved with end-user annotations that have nothing to do with us.
+ # see issue #8888 where we bypass using this function in the case
+ # that we want to detect an unresolvable Mapped[] type.
+ return False
+ else:
+ return is_origin_of_cls(annotated, _MappedAnnotationBase)
+
+
+class _CleanupError(Exception):
+ pass
+
+
+def _cleanup_mapped_str_annotation(
+ annotation: str, originating_module: str
+) -> str:
+ # fix up an annotation that comes in as the form:
+ # 'Mapped[List[Address]]' so that it instead looks like:
+ # 'Mapped[List["Address"]]' , which will allow us to get
+ # "Address" as a string
+
+ # additionally, resolve symbols for these names since this is where
+ # we'd have to do it
+
+ inner: Optional[Match[str]]
+
+ mm = re.match(r"^(.+?)\[(.+)\]$", annotation)
+
+ if not mm:
+ return annotation
+
+ # ticket #8759. Resolve the Mapped name to a real symbol.
+ # originally this just checked the name.
+ try:
+ obj = eval_name_only(mm.group(1), originating_module)
+ except NameError as ne:
+ raise _CleanupError(
+ f'For annotation "{annotation}", could not resolve '
+ f'container type "{mm.group(1)}". '
+ "Please ensure this type is imported at the module level "
+ "outside of TYPE_CHECKING blocks"
+ ) from ne
+
+ if obj is typing.ClassVar:
+ real_symbol = "ClassVar"
+ else:
+ try:
+ if issubclass(obj, _MappedAnnotationBase):
+ real_symbol = obj.__name__
+ else:
+ return annotation
+ except TypeError:
+ # avoid isinstance(obj, type) check, just catch TypeError
+ return annotation
+
+ # note: if one of the codepaths above didn't define real_symbol and
+ # then didn't return, real_symbol raises UnboundLocalError
+ # which is actually a NameError, and the calling routines don't
+ # notice this since they are catching NameError anyway. Just in case
+ # this is being modified in the future, something to be aware of.
+
+ stack = []
+ inner = mm
+ while True:
+ stack.append(real_symbol if mm is inner else inner.group(1))
+ g2 = inner.group(2)
+ inner = re.match(r"^(.+?)\[(.+)\]$", g2)
+ if inner is None:
+ stack.append(g2)
+ break
+
+ # stacks we want to rewrite, that is, quote the last entry which
+ # we think is a relationship class name:
+ #
+ # ['Mapped', 'List', 'Address']
+ # ['Mapped', 'A']
+ #
+ # stacks we dont want to rewrite, which are generally MappedColumn
+ # use cases:
+ #
+ # ['Mapped', "'Optional[Dict[str, str]]'"]
+ # ['Mapped', 'dict[str, str] | None']
+
+ if (
+ # avoid already quoted symbols such as
+ # ['Mapped', "'Optional[Dict[str, str]]'"]
+ not re.match(r"""^["'].*["']$""", stack[-1])
+ # avoid further generics like Dict[] such as
+ # ['Mapped', 'dict[str, str] | None']
+ and not re.match(r".*\[.*\]", stack[-1])
+ ):
+ stripchars = "\"' "
+ stack[-1] = ", ".join(
+ f'"{elem.strip(stripchars)}"' for elem in stack[-1].split(",")
+ )
+
+ annotation = "[".join(stack) + ("]" * (len(stack) - 1))
+
+ return annotation
+
+
+def _extract_mapped_subtype(
+ raw_annotation: Optional[_AnnotationScanType],
+ cls: type,
+ originating_module: str,
+ key: str,
+ attr_cls: Type[Any],
+ required: bool,
+ is_dataclass_field: bool,
+ expect_mapped: bool = True,
+ raiseerr: bool = True,
+) -> Optional[Tuple[Union[type, str], Optional[type]]]:
+ """given an annotation, figure out if it's ``Mapped[something]`` and if
+ so, return the ``something`` part.
+
+ Includes error raise scenarios and other options.
+
+ """
+
+ if raw_annotation is None:
+ if required:
+ raise sa_exc.ArgumentError(
+ f"Python typing annotation is required for attribute "
+ f'"{cls.__name__}.{key}" when primary argument(s) for '
+ f'"{attr_cls.__name__}" construct are None or not present'
+ )
+ return None
+
+ try:
+ annotated = de_stringify_annotation(
+ cls,
+ raw_annotation,
+ originating_module,
+ str_cleanup_fn=_cleanup_mapped_str_annotation,
+ )
+ except _CleanupError as ce:
+ raise sa_exc.ArgumentError(
+ f"Could not interpret annotation {raw_annotation}. "
+ "Check that it uses names that are correctly imported at the "
+ "module level. See chained stack trace for more hints."
+ ) from ce
+ except NameError as ne:
+ if raiseerr and "Mapped[" in raw_annotation: # type: ignore
+ raise sa_exc.ArgumentError(
+ f"Could not interpret annotation {raw_annotation}. "
+ "Check that it uses names that are correctly imported at the "
+ "module level. See chained stack trace for more hints."
+ ) from ne
+
+ annotated = raw_annotation # type: ignore
+
+ if is_dataclass_field:
+ return annotated, None
+ else:
+ if not hasattr(annotated, "__origin__") or not is_origin_of_cls(
+ annotated, _MappedAnnotationBase
+ ):
+ if expect_mapped:
+ if not raiseerr:
+ return None
+
+ origin = getattr(annotated, "__origin__", None)
+ if origin is typing.ClassVar:
+ return None
+
+ # check for other kind of ORM descriptor like AssociationProxy,
+ # don't raise for that (issue #9957)
+ elif isinstance(origin, type) and issubclass(
+ origin, ORMDescriptor
+ ):
+ return None
+
+ raise sa_exc.ArgumentError(
+ f'Type annotation for "{cls.__name__}.{key}" '
+ "can't be correctly interpreted for "
+ "Annotated Declarative Table form. ORM annotations "
+ "should normally make use of the ``Mapped[]`` generic "
+ "type, or other ORM-compatible generic type, as a "
+ "container for the actual type, which indicates the "
+ "intent that the attribute is mapped. "
+ "Class variables that are not intended to be mapped "
+ "by the ORM should use ClassVar[]. "
+ "To allow Annotated Declarative to disregard legacy "
+ "annotations which don't use Mapped[] to pass, set "
+ '"__allow_unmapped__ = True" on the class or a '
+ "superclass this class.",
+ code="zlpr",
+ )
+
+ else:
+ return annotated, None
+
+ if len(annotated.__args__) != 1:
+ raise sa_exc.ArgumentError(
+ "Expected sub-type for Mapped[] annotation"
+ )
+
+ return annotated.__args__[0], annotated.__origin__
+
+
+def _mapper_property_as_plain_name(prop: Type[Any]) -> str:
+ if hasattr(prop, "_mapper_property_name"):
+ name = prop._mapper_property_name()
+ else:
+ name = None
+ return util.clsname_as_plain_name(prop, name)
diff --git a/venv/lib/python3.11/site-packages/sqlalchemy/orm/writeonly.py b/venv/lib/python3.11/site-packages/sqlalchemy/orm/writeonly.py
new file mode 100644
index 0000000..5680cc7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/sqlalchemy/orm/writeonly.py
@@ -0,0 +1,678 @@
+# orm/writeonly.py
+# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""Write-only collection API.
+
+This is an alternate mapped attribute style that only supports single-item
+collection mutation operations. To read the collection, a select()
+object must be executed each time.
+
+.. versionadded:: 2.0
+
+
+"""
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Collection
+from typing import Dict
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import NoReturn
+from typing import Optional
+from typing import overload
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy.sql import bindparam
+from . import attributes
+from . import interfaces
+from . import relationships
+from . import strategies
+from .base import NEVER_SET
+from .base import object_mapper
+from .base import PassiveFlag
+from .base import RelationshipDirection
+from .. import exc
+from .. import inspect
+from .. import log
+from .. import util
+from ..sql import delete
+from ..sql import insert
+from ..sql import select
+from ..sql import update
+from ..sql.dml import Delete
+from ..sql.dml import Insert
+from ..sql.dml import Update
+from ..util.typing import Literal
+
+if TYPE_CHECKING:
+ from . import QueryableAttribute
+ from ._typing import _InstanceDict
+ from .attributes import AttributeEventToken
+ from .base import LoaderCallableStatus
+ from .collections import _AdaptedCollectionProtocol
+ from .collections import CollectionAdapter
+ from .mapper import Mapper
+ from .relationships import _RelationshipOrderByArg
+ from .state import InstanceState
+ from .util import AliasedClass
+ from ..event import _Dispatch
+ from ..sql.selectable import FromClause
+ from ..sql.selectable import Select
+
+_T = TypeVar("_T", bound=Any)
+
+
+class WriteOnlyHistory(Generic[_T]):
+ """Overrides AttributeHistory to receive append/remove events directly."""
+
+ unchanged_items: util.OrderedIdentitySet
+ added_items: util.OrderedIdentitySet
+ deleted_items: util.OrderedIdentitySet
+ _reconcile_collection: bool
+
+ def __init__(
+ self,
+ attr: WriteOnlyAttributeImpl,
+ state: InstanceState[_T],
+ passive: PassiveFlag,
+ apply_to: Optional[WriteOnlyHistory[_T]] = None,
+ ) -> None:
+ if apply_to:
+ if passive & PassiveFlag.SQL_OK:
+ raise exc.InvalidRequestError(
+ f"Attribute {attr} can't load the existing state from the "
+ "database for this operation; full iteration is not "
+ "permitted. If this is a delete operation, configure "
+ f"passive_deletes=True on the {attr} relationship in "
+ "order to resolve this error."
+ )
+
+ self.unchanged_items = apply_to.unchanged_items
+ self.added_items = apply_to.added_items
+ self.deleted_items = apply_to.deleted_items
+ self._reconcile_collection = apply_to._reconcile_collection
+ else:
+ self.deleted_items = util.OrderedIdentitySet()
+ self.added_items = util.OrderedIdentitySet()
+ self.unchanged_items = util.OrderedIdentitySet()
+ self._reconcile_collection = False
+
+ @property
+ def added_plus_unchanged(self) -> List[_T]:
+ return list(self.added_items.union(self.unchanged_items))
+
+ @property
+ def all_items(self) -> List[_T]:
+ return list(
+ self.added_items.union(self.unchanged_items).union(
+ self.deleted_items
+ )
+ )
+
+ def as_history(self) -> attributes.History:
+ if self._reconcile_collection:
+ added = self.added_items.difference(self.unchanged_items)
+ deleted = self.deleted_items.intersection(self.unchanged_items)
+ unchanged = self.unchanged_items.difference(deleted)
+ else:
+ added, unchanged, deleted = (
+ self.added_items,
+ self.unchanged_items,
+ self.deleted_items,
+ )
+ return attributes.History(list(added), list(unchanged), list(deleted))
+
+ def indexed(self, index: Union[int, slice]) -> Union[List[_T], _T]:
+ return list(self.added_items)[index]
+
+ def add_added(self, value: _T) -> None:
+ self.added_items.add(value)
+
+ def add_removed(self, value: _T) -> None:
+ if value in self.added_items:
+ self.added_items.remove(value)
+ else:
+ self.deleted_items.add(value)
+
+
+class WriteOnlyAttributeImpl(
+ attributes.HasCollectionAdapter, attributes.AttributeImpl
+):
+ uses_objects: bool = True
+ default_accepts_scalar_loader: bool = False
+ supports_population: bool = False
+ _supports_dynamic_iteration: bool = False
+ collection: bool = False
+ dynamic: bool = True
+ order_by: _RelationshipOrderByArg = ()
+ collection_history_cls: Type[WriteOnlyHistory[Any]] = WriteOnlyHistory
+
+ query_class: Type[WriteOnlyCollection[Any]]
+
+ def __init__(
+ self,
+ class_: Union[Type[Any], AliasedClass[Any]],
+ key: str,
+ dispatch: _Dispatch[QueryableAttribute[Any]],
+ target_mapper: Mapper[_T],
+ order_by: _RelationshipOrderByArg,
+ **kw: Any,
+ ):
+ super().__init__(class_, key, None, dispatch, **kw)
+ self.target_mapper = target_mapper
+ self.query_class = WriteOnlyCollection
+ if order_by:
+ self.order_by = tuple(order_by)
+
+ def get(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ ) -> Union[util.OrderedIdentitySet, WriteOnlyCollection[Any]]:
+ if not passive & PassiveFlag.SQL_OK:
+ return self._get_collection_history(
+ state, PassiveFlag.PASSIVE_NO_INITIALIZE
+ ).added_items
+ else:
+ return self.query_class(self, state)
+
+ @overload
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: Literal[None] = ...,
+ passive: Literal[PassiveFlag.PASSIVE_OFF] = ...,
+ ) -> CollectionAdapter: ...
+
+ @overload
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: _AdaptedCollectionProtocol = ...,
+ passive: PassiveFlag = ...,
+ ) -> CollectionAdapter: ...
+
+ @overload
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: Optional[_AdaptedCollectionProtocol] = ...,
+ passive: PassiveFlag = ...,
+ ) -> Union[
+ Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
+ ]: ...
+
+ def get_collection(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ user_data: Optional[_AdaptedCollectionProtocol] = None,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ ) -> Union[
+ Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
+ ]:
+ data: Collection[Any]
+ if not passive & PassiveFlag.SQL_OK:
+ data = self._get_collection_history(state, passive).added_items
+ else:
+ history = self._get_collection_history(state, passive)
+ data = history.added_plus_unchanged
+ return DynamicCollectionAdapter(data) # type: ignore[return-value]
+
+ @util.memoized_property
+ def _append_token( # type:ignore[override]
+ self,
+ ) -> attributes.AttributeEventToken:
+ return attributes.AttributeEventToken(self, attributes.OP_APPEND)
+
+ @util.memoized_property
+ def _remove_token( # type:ignore[override]
+ self,
+ ) -> attributes.AttributeEventToken:
+ return attributes.AttributeEventToken(self, attributes.OP_REMOVE)
+
+ def fire_append_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ collection_history: Optional[WriteOnlyHistory[Any]] = None,
+ ) -> None:
+ if collection_history is None:
+ collection_history = self._modified_event(state, dict_)
+
+ collection_history.add_added(value)
+
+ for fn in self.dispatch.append:
+ value = fn(state, value, initiator or self._append_token)
+
+ if self.trackparent and value is not None:
+ self.sethasparent(attributes.instance_state(value), state, True)
+
+ def fire_remove_event(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ collection_history: Optional[WriteOnlyHistory[Any]] = None,
+ ) -> None:
+ if collection_history is None:
+ collection_history = self._modified_event(state, dict_)
+
+ collection_history.add_removed(value)
+
+ if self.trackparent and value is not None:
+ self.sethasparent(attributes.instance_state(value), state, False)
+
+ for fn in self.dispatch.remove:
+ fn(state, value, initiator or self._remove_token)
+
+ def _modified_event(
+ self, state: InstanceState[Any], dict_: _InstanceDict
+ ) -> WriteOnlyHistory[Any]:
+ if self.key not in state.committed_state:
+ state.committed_state[self.key] = self.collection_history_cls(
+ self, state, PassiveFlag.PASSIVE_NO_FETCH
+ )
+
+ state._modified_event(dict_, self, NEVER_SET)
+
+ # this is a hack to allow the entities.ComparableEntity fixture
+ # to work
+ dict_[self.key] = True
+ return state.committed_state[self.key] # type: ignore[no-any-return]
+
+ def set(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken] = None,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
+ check_old: Any = None,
+ pop: bool = False,
+ _adapt: bool = True,
+ ) -> None:
+ if initiator and initiator.parent_token is self.parent_token:
+ return
+
+ if pop and value is None:
+ return
+
+ iterable = value
+ new_values = list(iterable)
+ if state.has_identity:
+ if not self._supports_dynamic_iteration:
+ raise exc.InvalidRequestError(
+ f'Collection "{self}" does not support implicit '
+ "iteration; collection replacement operations "
+ "can't be used"
+ )
+ old_collection = util.IdentitySet(
+ self.get(state, dict_, passive=passive)
+ )
+
+ collection_history = self._modified_event(state, dict_)
+ if not state.has_identity:
+ old_collection = collection_history.added_items
+ else:
+ old_collection = old_collection.union(
+ collection_history.added_items
+ )
+
+ constants = old_collection.intersection(new_values)
+ additions = util.IdentitySet(new_values).difference(constants)
+ removals = old_collection.difference(constants)
+
+ for member in new_values:
+ if member in additions:
+ self.fire_append_event(
+ state,
+ dict_,
+ member,
+ None,
+ collection_history=collection_history,
+ )
+
+ for member in removals:
+ self.fire_remove_event(
+ state,
+ dict_,
+ member,
+ None,
+ collection_history=collection_history,
+ )
+
+ def delete(self, *args: Any, **kwargs: Any) -> NoReturn:
+ raise NotImplementedError()
+
+ def set_committed_value(
+ self, state: InstanceState[Any], dict_: _InstanceDict, value: Any
+ ) -> NoReturn:
+ raise NotImplementedError(
+ "Dynamic attributes don't support collection population."
+ )
+
+ def get_history(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
+ ) -> attributes.History:
+ c = self._get_collection_history(state, passive)
+ return c.as_history()
+
+ def get_all_pending(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ passive: PassiveFlag = PassiveFlag.PASSIVE_NO_INITIALIZE,
+ ) -> List[Tuple[InstanceState[Any], Any]]:
+ c = self._get_collection_history(state, passive)
+ return [(attributes.instance_state(x), x) for x in c.all_items]
+
+ def _get_collection_history(
+ self, state: InstanceState[Any], passive: PassiveFlag
+ ) -> WriteOnlyHistory[Any]:
+ c: WriteOnlyHistory[Any]
+ if self.key in state.committed_state:
+ c = state.committed_state[self.key]
+ else:
+ c = self.collection_history_cls(
+ self, state, PassiveFlag.PASSIVE_NO_FETCH
+ )
+
+ if state.has_identity and (passive & PassiveFlag.INIT_OK):
+ return self.collection_history_cls(
+ self, state, passive, apply_to=c
+ )
+ else:
+ return c
+
+ def append(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
+ ) -> None:
+ if initiator is not self:
+ self.fire_append_event(state, dict_, value, initiator)
+
+ def remove(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
+ ) -> None:
+ if initiator is not self:
+ self.fire_remove_event(state, dict_, value, initiator)
+
+ def pop(
+ self,
+ state: InstanceState[Any],
+ dict_: _InstanceDict,
+ value: Any,
+ initiator: Optional[AttributeEventToken],
+ passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
+ ) -> None:
+ self.remove(state, dict_, value, initiator, passive=passive)
+
+
+@log.class_logger
+@relationships.RelationshipProperty.strategy_for(lazy="write_only")
+class WriteOnlyLoader(strategies.AbstractRelationshipLoader, log.Identified):
+ impl_class = WriteOnlyAttributeImpl
+
+ def init_class_attribute(self, mapper: Mapper[Any]) -> None:
+ self.is_class_level = True
+ if not self.uselist or self.parent_property.direction not in (
+ interfaces.ONETOMANY,
+ interfaces.MANYTOMANY,
+ ):
+ raise exc.InvalidRequestError(
+ "On relationship %s, 'dynamic' loaders cannot be used with "
+ "many-to-one/one-to-one relationships and/or "
+ "uselist=False." % self.parent_property
+ )
+
+ strategies._register_attribute( # type: ignore[no-untyped-call]
+ self.parent_property,
+ mapper,
+ useobject=True,
+ impl_class=self.impl_class,
+ target_mapper=self.parent_property.mapper,
+ order_by=self.parent_property.order_by,
+ query_class=self.parent_property.query_class,
+ )
+
+
+class DynamicCollectionAdapter:
+ """simplified CollectionAdapter for internal API consistency"""
+
+ data: Collection[Any]
+
+ def __init__(self, data: Collection[Any]):
+ self.data = data
+
+ def __iter__(self) -> Iterator[Any]:
+ return iter(self.data)
+
+ def _reset_empty(self) -> None:
+ pass
+
+ def __len__(self) -> int:
+ return len(self.data)
+
+ def __bool__(self) -> bool:
+ return True
+
+
+class AbstractCollectionWriter(Generic[_T]):
+ """Virtual collection which includes append/remove methods that synchronize
+ into the attribute event system.
+
+ """
+
+ if not TYPE_CHECKING:
+ __slots__ = ()
+
+ instance: _T
+ _from_obj: Tuple[FromClause, ...]
+
+ def __init__(self, attr: WriteOnlyAttributeImpl, state: InstanceState[_T]):
+ instance = state.obj()
+ if TYPE_CHECKING:
+ assert instance
+ self.instance = instance
+ self.attr = attr
+
+ mapper = object_mapper(instance)
+ prop = mapper._props[self.attr.key]
+
+ if prop.secondary is not None:
+ # this is a hack right now. The Query only knows how to
+ # make subsequent joins() without a given left-hand side
+ # from self._from_obj[0]. We need to ensure prop.secondary
+ # 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].
+
+ # note also, we are using the official ORM-annotated selectable
+ # from __clause_element__(), see #7868
+ self._from_obj = (prop.mapper.__clause_element__(), prop.secondary)
+ else:
+ self._from_obj = ()
+
+ self._where_criteria = (
+ prop._with_parent(instance, alias_secondary=False),
+ )
+
+ if self.attr.order_by:
+ self._order_by_clauses = self.attr.order_by
+ else:
+ self._order_by_clauses = ()
+
+ def _add_all_impl(self, iterator: Iterable[_T]) -> None:
+ for item in iterator:
+ self.attr.append(
+ attributes.instance_state(self.instance),
+ attributes.instance_dict(self.instance),
+ item,
+ None,
+ )
+
+ def _remove_impl(self, item: _T) -> None:
+ self.attr.remove(
+ attributes.instance_state(self.instance),
+ attributes.instance_dict(self.instance),
+ item,
+ None,
+ )
+
+
+class WriteOnlyCollection(AbstractCollectionWriter[_T]):
+ """Write-only collection which can synchronize changes into the
+ attribute event system.
+
+ The :class:`.WriteOnlyCollection` is used in a mapping by
+ using the ``"write_only"`` lazy loading strategy with
+ :func:`_orm.relationship`. For background on this configuration,
+ see :ref:`write_only_relationship`.
+
+ .. versionadded:: 2.0
+
+ .. seealso::
+
+ :ref:`write_only_relationship`
+
+ """
+
+ __slots__ = (
+ "instance",
+ "attr",
+ "_where_criteria",
+ "_from_obj",
+ "_order_by_clauses",
+ )
+
+ def __iter__(self) -> NoReturn:
+ raise TypeError(
+ "WriteOnly collections don't support iteration in-place; "
+ "to query for collection items, use the select() method to "
+ "produce a SQL statement and execute it with session.scalars()."
+ )
+
+ def select(self) -> Select[Tuple[_T]]:
+ """Produce a :class:`_sql.Select` construct that represents the
+ rows within this instance-local :class:`_orm.WriteOnlyCollection`.
+
+ """
+ stmt = select(self.attr.target_mapper).where(*self._where_criteria)
+ if self._from_obj:
+ stmt = stmt.select_from(*self._from_obj)
+ if self._order_by_clauses:
+ stmt = stmt.order_by(*self._order_by_clauses)
+ return stmt
+
+ def insert(self) -> Insert:
+ """For one-to-many collections, produce a :class:`_dml.Insert` which
+ will insert new rows in terms of this this instance-local
+ :class:`_orm.WriteOnlyCollection`.
+
+ This construct is only supported for a :class:`_orm.Relationship`
+ that does **not** include the :paramref:`_orm.relationship.secondary`
+ parameter. For relationships that refer to a many-to-many table,
+ use ordinary bulk insert techniques to produce new objects, then
+ use :meth:`_orm.AbstractCollectionWriter.add_all` to associate them
+ with the collection.
+
+
+ """
+
+ state = inspect(self.instance)
+ mapper = state.mapper
+ prop = mapper._props[self.attr.key]
+
+ if prop.direction is not RelationshipDirection.ONETOMANY:
+ raise exc.InvalidRequestError(
+ "Write only bulk INSERT only supported for one-to-many "
+ "collections; for many-to-many, use a separate bulk "
+ "INSERT along with add_all()."
+ )
+
+ dict_: Dict[str, Any] = {}
+
+ for l, r in prop.synchronize_pairs:
+ fn = prop._get_attr_w_warn_on_none(
+ mapper,
+ state,
+ state.dict,
+ l,
+ )
+
+ dict_[r.key] = bindparam(None, callable_=fn)
+
+ return insert(self.attr.target_mapper).values(**dict_)
+
+ def update(self) -> Update:
+ """Produce a :class:`_dml.Update` which will refer to rows in terms
+ of this instance-local :class:`_orm.WriteOnlyCollection`.
+
+ """
+ return update(self.attr.target_mapper).where(*self._where_criteria)
+
+ def delete(self) -> Delete:
+ """Produce a :class:`_dml.Delete` which will refer to rows in terms
+ of this instance-local :class:`_orm.WriteOnlyCollection`.
+
+ """
+ return delete(self.attr.target_mapper).where(*self._where_criteria)
+
+ def add_all(self, iterator: Iterable[_T]) -> None:
+ """Add an iterable of items to this :class:`_orm.WriteOnlyCollection`.
+
+ The given items will be persisted to the database in terms of
+ the parent instance's collection on the next flush.
+
+ """
+ self._add_all_impl(iterator)
+
+ def add(self, item: _T) -> None:
+ """Add an item to this :class:`_orm.WriteOnlyCollection`.
+
+ The given item will be persisted to the database in terms of
+ the parent instance's collection on the next flush.
+
+ """
+ self._add_all_impl([item])
+
+ def remove(self, item: _T) -> None:
+ """Remove an item from this :class:`_orm.WriteOnlyCollection`.
+
+ The given item will be removed from the parent instance's collection on
+ the next flush.
+
+ """
+ self._remove_impl(item)