SQLAlchemy 2.1.0__cp315-cp315t-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. sqlalchemy/__init__.py +299 -0
  2. sqlalchemy/connectors/__init__.py +18 -0
  3. sqlalchemy/connectors/aioodbc.py +171 -0
  4. sqlalchemy/connectors/asyncio.py +476 -0
  5. sqlalchemy/connectors/pyodbc.py +248 -0
  6. sqlalchemy/dialects/__init__.py +62 -0
  7. sqlalchemy/dialects/_typing.py +29 -0
  8. sqlalchemy/dialects/mssql/__init__.py +88 -0
  9. sqlalchemy/dialects/mssql/aioodbc.py +63 -0
  10. sqlalchemy/dialects/mssql/base.py +4833 -0
  11. sqlalchemy/dialects/mssql/information_schema.py +345 -0
  12. sqlalchemy/dialects/mssql/json.py +140 -0
  13. sqlalchemy/dialects/mssql/mssqlpython.py +251 -0
  14. sqlalchemy/dialects/mssql/provision.py +196 -0
  15. sqlalchemy/dialects/mssql/pymssql.py +130 -0
  16. sqlalchemy/dialects/mssql/pyodbc.py +697 -0
  17. sqlalchemy/dialects/mysql/__init__.py +106 -0
  18. sqlalchemy/dialects/mysql/_mariadb_shim.py +312 -0
  19. sqlalchemy/dialects/mysql/aiomysql.py +260 -0
  20. sqlalchemy/dialects/mysql/asyncmy.py +241 -0
  21. sqlalchemy/dialects/mysql/base.py +3896 -0
  22. sqlalchemy/dialects/mysql/cymysql.py +107 -0
  23. sqlalchemy/dialects/mysql/dml.py +279 -0
  24. sqlalchemy/dialects/mysql/enumerated.py +277 -0
  25. sqlalchemy/dialects/mysql/expression.py +146 -0
  26. sqlalchemy/dialects/mysql/json.py +92 -0
  27. sqlalchemy/dialects/mysql/mariadb.py +67 -0
  28. sqlalchemy/dialects/mysql/mariadbconnector.py +314 -0
  29. sqlalchemy/dialects/mysql/mysqlconnector.py +291 -0
  30. sqlalchemy/dialects/mysql/mysqldb.py +318 -0
  31. sqlalchemy/dialects/mysql/provision.py +153 -0
  32. sqlalchemy/dialects/mysql/pymysql.py +188 -0
  33. sqlalchemy/dialects/mysql/pyodbc.py +157 -0
  34. sqlalchemy/dialects/mysql/reflection.py +724 -0
  35. sqlalchemy/dialects/mysql/reserved_words.py +570 -0
  36. sqlalchemy/dialects/mysql/types.py +845 -0
  37. sqlalchemy/dialects/oracle/__init__.py +85 -0
  38. sqlalchemy/dialects/oracle/base.py +3847 -0
  39. sqlalchemy/dialects/oracle/cx_oracle.py +1736 -0
  40. sqlalchemy/dialects/oracle/dictionary.py +507 -0
  41. sqlalchemy/dialects/oracle/json.py +157 -0
  42. sqlalchemy/dialects/oracle/oracledb.py +921 -0
  43. sqlalchemy/dialects/oracle/provision.py +288 -0
  44. sqlalchemy/dialects/oracle/types.py +367 -0
  45. sqlalchemy/dialects/oracle/vector.py +366 -0
  46. sqlalchemy/dialects/postgresql/__init__.py +170 -0
  47. sqlalchemy/dialects/postgresql/_psycopg_common.py +232 -0
  48. sqlalchemy/dialects/postgresql/array.py +534 -0
  49. sqlalchemy/dialects/postgresql/asyncpg.py +1318 -0
  50. sqlalchemy/dialects/postgresql/base.py +5977 -0
  51. sqlalchemy/dialects/postgresql/bitstring.py +327 -0
  52. sqlalchemy/dialects/postgresql/dml.py +360 -0
  53. sqlalchemy/dialects/postgresql/ext.py +599 -0
  54. sqlalchemy/dialects/postgresql/hstore.py +422 -0
  55. sqlalchemy/dialects/postgresql/json.py +411 -0
  56. sqlalchemy/dialects/postgresql/named_types.py +535 -0
  57. sqlalchemy/dialects/postgresql/operators.py +129 -0
  58. sqlalchemy/dialects/postgresql/pg8000.py +655 -0
  59. sqlalchemy/dialects/postgresql/pg_catalog.py +345 -0
  60. sqlalchemy/dialects/postgresql/provision.py +202 -0
  61. sqlalchemy/dialects/postgresql/psycopg.py +800 -0
  62. sqlalchemy/dialects/postgresql/psycopg2.py +860 -0
  63. sqlalchemy/dialects/postgresql/psycopg2cffi.py +62 -0
  64. sqlalchemy/dialects/postgresql/ranges.py +1002 -0
  65. sqlalchemy/dialects/postgresql/types.py +388 -0
  66. sqlalchemy/dialects/sqlite/__init__.py +59 -0
  67. sqlalchemy/dialects/sqlite/aiosqlite.py +375 -0
  68. sqlalchemy/dialects/sqlite/base.py +3156 -0
  69. sqlalchemy/dialects/sqlite/dml.py +314 -0
  70. sqlalchemy/dialects/sqlite/json.py +134 -0
  71. sqlalchemy/dialects/sqlite/provision.py +237 -0
  72. sqlalchemy/dialects/sqlite/pysqlcipher.py +166 -0
  73. sqlalchemy/dialects/sqlite/pysqlite.py +959 -0
  74. sqlalchemy/dialects/type_migration_guidelines.txt +145 -0
  75. sqlalchemy/engine/__init__.py +62 -0
  76. sqlalchemy/engine/_processors_cy.cp315t-win_amd64.pyd +0 -0
  77. sqlalchemy/engine/_processors_cy.py +92 -0
  78. sqlalchemy/engine/_result_cy.cp315t-win_amd64.pyd +0 -0
  79. sqlalchemy/engine/_result_cy.py +711 -0
  80. sqlalchemy/engine/_row_cy.cp315t-win_amd64.pyd +0 -0
  81. sqlalchemy/engine/_row_cy.py +232 -0
  82. sqlalchemy/engine/_util_cy.cp315t-win_amd64.pyd +0 -0
  83. sqlalchemy/engine/_util_cy.py +136 -0
  84. sqlalchemy/engine/base.py +3357 -0
  85. sqlalchemy/engine/characteristics.py +155 -0
  86. sqlalchemy/engine/create.py +877 -0
  87. sqlalchemy/engine/cursor.py +2425 -0
  88. sqlalchemy/engine/default.py +2627 -0
  89. sqlalchemy/engine/events.py +965 -0
  90. sqlalchemy/engine/interfaces.py +3647 -0
  91. sqlalchemy/engine/mock.py +133 -0
  92. sqlalchemy/engine/processors.py +83 -0
  93. sqlalchemy/engine/reflection.py +2141 -0
  94. sqlalchemy/engine/result.py +2012 -0
  95. sqlalchemy/engine/row.py +397 -0
  96. sqlalchemy/engine/strategies.py +16 -0
  97. sqlalchemy/engine/url.py +922 -0
  98. sqlalchemy/engine/util.py +164 -0
  99. sqlalchemy/event/__init__.py +26 -0
  100. sqlalchemy/event/api.py +220 -0
  101. sqlalchemy/event/attr.py +675 -0
  102. sqlalchemy/event/base.py +473 -0
  103. sqlalchemy/event/legacy.py +259 -0
  104. sqlalchemy/event/registry.py +391 -0
  105. sqlalchemy/events.py +17 -0
  106. sqlalchemy/exc.py +939 -0
  107. sqlalchemy/ext/__init__.py +10 -0
  108. sqlalchemy/ext/associationproxy.py +2073 -0
  109. sqlalchemy/ext/asyncio/__init__.py +29 -0
  110. sqlalchemy/ext/asyncio/base.py +281 -0
  111. sqlalchemy/ext/asyncio/engine.py +1489 -0
  112. sqlalchemy/ext/asyncio/exc.py +30 -0
  113. sqlalchemy/ext/asyncio/result.py +994 -0
  114. sqlalchemy/ext/asyncio/scoping.py +1787 -0
  115. sqlalchemy/ext/asyncio/session.py +2268 -0
  116. sqlalchemy/ext/automap.py +1702 -0
  117. sqlalchemy/ext/baked.py +558 -0
  118. sqlalchemy/ext/compiler.py +601 -0
  119. sqlalchemy/ext/declarative/__init__.py +65 -0
  120. sqlalchemy/ext/declarative/extensions.py +561 -0
  121. sqlalchemy/ext/horizontal_shard.py +481 -0
  122. sqlalchemy/ext/hybrid.py +1877 -0
  123. sqlalchemy/ext/indexable.py +364 -0
  124. sqlalchemy/ext/instrumentation.py +450 -0
  125. sqlalchemy/ext/mutable.py +1081 -0
  126. sqlalchemy/ext/orderinglist.py +440 -0
  127. sqlalchemy/ext/serializer.py +184 -0
  128. sqlalchemy/future/__init__.py +17 -0
  129. sqlalchemy/future/engine.py +15 -0
  130. sqlalchemy/inspection.py +188 -0
  131. sqlalchemy/log.py +279 -0
  132. sqlalchemy/orm/__init__.py +176 -0
  133. sqlalchemy/orm/_orm_constructors.py +2711 -0
  134. sqlalchemy/orm/_typing.py +180 -0
  135. sqlalchemy/orm/attributes.py +2868 -0
  136. sqlalchemy/orm/base.py +991 -0
  137. sqlalchemy/orm/bulk_persistence.py +2168 -0
  138. sqlalchemy/orm/clsregistry.py +630 -0
  139. sqlalchemy/orm/collections.py +1569 -0
  140. sqlalchemy/orm/context.py +3475 -0
  141. sqlalchemy/orm/decl_api.py +2335 -0
  142. sqlalchemy/orm/decl_base.py +2300 -0
  143. sqlalchemy/orm/dependency.py +1306 -0
  144. sqlalchemy/orm/descriptor_props.py +1205 -0
  145. sqlalchemy/orm/dynamic.py +306 -0
  146. sqlalchemy/orm/evaluator.py +378 -0
  147. sqlalchemy/orm/events.py +3387 -0
  148. sqlalchemy/orm/exc.py +237 -0
  149. sqlalchemy/orm/identity.py +302 -0
  150. sqlalchemy/orm/instrumentation.py +749 -0
  151. sqlalchemy/orm/interfaces.py +1595 -0
  152. sqlalchemy/orm/loading.py +1712 -0
  153. sqlalchemy/orm/mapped_collection.py +557 -0
  154. sqlalchemy/orm/mapper.py +4465 -0
  155. sqlalchemy/orm/path_registry.py +907 -0
  156. sqlalchemy/orm/persistence.py +1790 -0
  157. sqlalchemy/orm/properties.py +972 -0
  158. sqlalchemy/orm/query.py +3528 -0
  159. sqlalchemy/orm/relationships.py +3615 -0
  160. sqlalchemy/orm/scoping.py +2251 -0
  161. sqlalchemy/orm/session.py +5493 -0
  162. sqlalchemy/orm/state.py +1175 -0
  163. sqlalchemy/orm/state_changes.py +196 -0
  164. sqlalchemy/orm/strategies.py +3559 -0
  165. sqlalchemy/orm/strategy_options.py +2648 -0
  166. sqlalchemy/orm/sync.py +164 -0
  167. sqlalchemy/orm/unitofwork.py +797 -0
  168. sqlalchemy/orm/util.py +2550 -0
  169. sqlalchemy/orm/writeonly.py +701 -0
  170. sqlalchemy/pool/__init__.py +41 -0
  171. sqlalchemy/pool/base.py +1544 -0
  172. sqlalchemy/pool/events.py +375 -0
  173. sqlalchemy/pool/impl.py +583 -0
  174. sqlalchemy/py.typed +0 -0
  175. sqlalchemy/schema.py +75 -0
  176. sqlalchemy/sql/__init__.py +156 -0
  177. sqlalchemy/sql/_annotated_cols.py +402 -0
  178. sqlalchemy/sql/_cache_key_cy.cp315t-win_amd64.pyd +0 -0
  179. sqlalchemy/sql/_cache_key_cy.py +363 -0
  180. sqlalchemy/sql/_dml_constructors.py +132 -0
  181. sqlalchemy/sql/_elements_constructors.py +2190 -0
  182. sqlalchemy/sql/_orm_types.py +19 -0
  183. sqlalchemy/sql/_selectable_constructors.py +840 -0
  184. sqlalchemy/sql/_typing.py +500 -0
  185. sqlalchemy/sql/_util_cy.cp315t-win_amd64.pyd +0 -0
  186. sqlalchemy/sql/_util_cy.pxd +11 -0
  187. sqlalchemy/sql/_util_cy.py +127 -0
  188. sqlalchemy/sql/annotation.py +590 -0
  189. sqlalchemy/sql/base.py +2852 -0
  190. sqlalchemy/sql/cache_key.py +915 -0
  191. sqlalchemy/sql/coercions.py +1373 -0
  192. sqlalchemy/sql/compiler.py +8469 -0
  193. sqlalchemy/sql/crud.py +1816 -0
  194. sqlalchemy/sql/ddl.py +1962 -0
  195. sqlalchemy/sql/default_comparator.py +660 -0
  196. sqlalchemy/sql/dml.py +2018 -0
  197. sqlalchemy/sql/elements.py +6062 -0
  198. sqlalchemy/sql/events.py +458 -0
  199. sqlalchemy/sql/expression.py +171 -0
  200. sqlalchemy/sql/functions.py +2380 -0
  201. sqlalchemy/sql/lambdas.py +1442 -0
  202. sqlalchemy/sql/naming.py +204 -0
  203. sqlalchemy/sql/operators.py +2909 -0
  204. sqlalchemy/sql/roles.py +332 -0
  205. sqlalchemy/sql/schema.py +7080 -0
  206. sqlalchemy/sql/selectable.py +7644 -0
  207. sqlalchemy/sql/sqltypes.py +4130 -0
  208. sqlalchemy/sql/traversals.py +1041 -0
  209. sqlalchemy/sql/type_api.py +2450 -0
  210. sqlalchemy/sql/util.py +1496 -0
  211. sqlalchemy/sql/visitors.py +1153 -0
  212. sqlalchemy/testing/__init__.py +97 -0
  213. sqlalchemy/testing/assertions.py +1007 -0
  214. sqlalchemy/testing/assertsql.py +519 -0
  215. sqlalchemy/testing/asyncio.py +128 -0
  216. sqlalchemy/testing/cancellation.py +237 -0
  217. sqlalchemy/testing/config.py +440 -0
  218. sqlalchemy/testing/engines.py +496 -0
  219. sqlalchemy/testing/entities.py +117 -0
  220. sqlalchemy/testing/exclusions.py +501 -0
  221. sqlalchemy/testing/fixtures/__init__.py +30 -0
  222. sqlalchemy/testing/fixtures/base.py +426 -0
  223. sqlalchemy/testing/fixtures/mypy.py +247 -0
  224. sqlalchemy/testing/fixtures/orm.py +227 -0
  225. sqlalchemy/testing/fixtures/sql.py +538 -0
  226. sqlalchemy/testing/pickleable.py +155 -0
  227. sqlalchemy/testing/plugin/__init__.py +6 -0
  228. sqlalchemy/testing/plugin/bootstrap.py +50 -0
  229. sqlalchemy/testing/plugin/plugin_base.py +828 -0
  230. sqlalchemy/testing/plugin/pytestplugin.py +896 -0
  231. sqlalchemy/testing/profiles_file.py +350 -0
  232. sqlalchemy/testing/profiling.py +294 -0
  233. sqlalchemy/testing/provision.py +633 -0
  234. sqlalchemy/testing/requirements.py +1990 -0
  235. sqlalchemy/testing/schema.py +198 -0
  236. sqlalchemy/testing/suite/__init__.py +19 -0
  237. sqlalchemy/testing/suite/test_cte.py +237 -0
  238. sqlalchemy/testing/suite/test_ddl.py +420 -0
  239. sqlalchemy/testing/suite/test_dialect.py +777 -0
  240. sqlalchemy/testing/suite/test_insert.py +630 -0
  241. sqlalchemy/testing/suite/test_reflection.py +3815 -0
  242. sqlalchemy/testing/suite/test_results.py +660 -0
  243. sqlalchemy/testing/suite/test_rowcount.py +258 -0
  244. sqlalchemy/testing/suite/test_select.py +2112 -0
  245. sqlalchemy/testing/suite/test_sequence.py +317 -0
  246. sqlalchemy/testing/suite/test_table_via_select.py +686 -0
  247. sqlalchemy/testing/suite/test_types.py +2271 -0
  248. sqlalchemy/testing/suite/test_unicode_ddl.py +189 -0
  249. sqlalchemy/testing/suite/test_update_delete.py +176 -0
  250. sqlalchemy/testing/util.py +575 -0
  251. sqlalchemy/testing/warnings.py +52 -0
  252. sqlalchemy/types.py +75 -0
  253. sqlalchemy/util/__init__.py +165 -0
  254. sqlalchemy/util/_collections.py +688 -0
  255. sqlalchemy/util/_collections_cy.cp315t-win_amd64.pyd +0 -0
  256. sqlalchemy/util/_collections_cy.pxd +8 -0
  257. sqlalchemy/util/_collections_cy.py +516 -0
  258. sqlalchemy/util/_has_cython.py +48 -0
  259. sqlalchemy/util/_immutabledict_cy.cp315t-win_amd64.pyd +0 -0
  260. sqlalchemy/util/_immutabledict_cy.py +240 -0
  261. sqlalchemy/util/compat.py +298 -0
  262. sqlalchemy/util/concurrency.py +272 -0
  263. sqlalchemy/util/cython.py +95 -0
  264. sqlalchemy/util/deprecations.py +401 -0
  265. sqlalchemy/util/langhelpers.py +2803 -0
  266. sqlalchemy/util/preloaded.py +153 -0
  267. sqlalchemy/util/queue.py +304 -0
  268. sqlalchemy/util/tool_support.py +202 -0
  269. sqlalchemy/util/topological.py +120 -0
  270. sqlalchemy/util/typing.py +709 -0
  271. sqlalchemy-2.1.0.dist-info/METADATA +270 -0
  272. sqlalchemy-2.1.0.dist-info/RECORD +276 -0
  273. sqlalchemy-2.1.0.dist-info/WHEEL +5 -0
  274. sqlalchemy-2.1.0.dist-info/licenses/AUTHORS +30 -0
  275. sqlalchemy-2.1.0.dist-info/licenses/LICENSE +19 -0
  276. sqlalchemy-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3559 @@
1
+ # orm/strategies.py
2
+ # Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3
+ # <see AUTHORS file>
4
+ #
5
+ # This module is part of SQLAlchemy and is released under
6
+ # the MIT License: https://www.opensource.org/licenses/mit-license.php
7
+ # mypy: ignore-errors
8
+
9
+
10
+ """sqlalchemy.orm.interfaces.LoaderStrategy
11
+ implementations, and related MapperOptions."""
12
+
13
+ from __future__ import annotations
14
+
15
+ import collections
16
+ from typing import Any
17
+ from typing import Dict
18
+ from typing import Literal
19
+ from typing import Optional
20
+ from typing import Tuple
21
+ from typing import TYPE_CHECKING
22
+ from typing import Union
23
+
24
+ from . import attributes
25
+ from . import exc as orm_exc
26
+ from . import interfaces
27
+ from . import loading
28
+ from . import path_registry
29
+ from . import properties
30
+ from . import query
31
+ from . import relationships
32
+ from . import unitofwork
33
+ from . import util as orm_util
34
+ from .base import _DEFER_FOR_STATE
35
+ from .base import _RAISE_FOR_STATE
36
+ from .base import _SET_DEFERRED_EXPIRED
37
+ from .base import ATTR_WAS_SET
38
+ from .base import LoaderCallableStatus
39
+ from .base import PASSIVE_OFF
40
+ from .base import PassiveFlag
41
+ from .context import _column_descriptions
42
+ from .context import _ORMCompileState
43
+ from .context import _ORMSelectCompileState
44
+ from .context import QueryContext
45
+ from .interfaces import LoaderStrategy
46
+ from .interfaces import StrategizedProperty
47
+ from .session import _state_session
48
+ from .state import InstanceState
49
+ from .strategy_options import Load
50
+ from .util import _none_only_set
51
+ from .util import AliasedClass
52
+ from .. import event
53
+ from .. import exc as sa_exc
54
+ from .. import inspect
55
+ from .. import log
56
+ from .. import sql
57
+ from .. import util
58
+ from ..sql import util as sql_util
59
+ from ..sql import visitors
60
+ from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
61
+ from ..sql.selectable import Select
62
+
63
+ if TYPE_CHECKING:
64
+ from .mapper import Mapper
65
+ from .relationships import RelationshipProperty
66
+ from ..sql.elements import ColumnElement
67
+
68
+
69
+ # sentinel for _LoadLazyAttribute.extra_criteria indicating that the loader
70
+ # option had no additional criteria. the attribute is otherwise a single
71
+ # element returned by Load._generate_extra_criteria(), which has no
72
+ # meaningful boolean value of its own.
73
+ _NO_EXTRA_CRITERIA = None
74
+
75
+
76
+ def _register_attribute(
77
+ prop,
78
+ mapper,
79
+ useobject,
80
+ compare_function=None,
81
+ typecallable=None,
82
+ callable_=None,
83
+ proxy_property=None,
84
+ active_history=False,
85
+ impl_class=None,
86
+ default_scalar_value=None,
87
+ **kw,
88
+ ):
89
+
90
+ # event-hook registration functions organized into "pre validate" and "post
91
+ # validate" collections. Each hook registration function generates a new
92
+ # AttributeEvents registration for this attribute. What we are controlling
93
+ # here is the order in which these attribute events are established. Hook
94
+ # registration functions invoked in the order of first pre-validate, then
95
+ # user-specific validation i.e. `@validates`, then post-validate.
96
+ # within `@validates` we are scanning for hooks from child-most classes
97
+ # first (this suits the feature added in #2943).
98
+ pre_validate_hooks = []
99
+ post_validate_hooks = []
100
+
101
+ uselist = useobject and prop.uselist
102
+
103
+ if useobject and prop.single_parent:
104
+ pre_validate_hooks.append(_single_parent_validator)
105
+
106
+ if useobject:
107
+ post_validate_hooks.append(unitofwork._track_cascade_events)
108
+
109
+ # need to assemble backref listeners
110
+ # after the singleparentvalidator, mapper validator
111
+ if useobject:
112
+ backref = prop.back_populates
113
+ if backref and prop._effective_sync_backref:
114
+ post_validate_hooks.append(
115
+ lambda desc, prop: attributes._backref_listeners(
116
+ desc, backref, uselist
117
+ )
118
+ )
119
+
120
+ # a single MapperProperty is shared down a class inheritance
121
+ # hierarchy, so we set up attribute instrumentation and backref event
122
+ # for each mapper down the hierarchy.
123
+
124
+ # typically, "mapper" is the same as prop.parent, due to the way
125
+ # the configure_mappers() process runs, however this is not strongly
126
+ # enforced, and in the case of a second configure_mappers() run the
127
+ # mapper here might not be prop.parent; also, a subclass mapper may
128
+ # be called here before a superclass mapper. That is, can't depend
129
+ # on mappers not already being set up so we have to check each one.
130
+
131
+ for m in mapper.self_and_descendants:
132
+ if prop is m._props.get(
133
+ prop.key
134
+ ) and not m.class_manager._attr_has_impl(prop.key):
135
+ desc = attributes._register_attribute_impl(
136
+ m.class_,
137
+ prop.key,
138
+ parent_token=prop,
139
+ uselist=uselist,
140
+ compare_function=compare_function,
141
+ useobject=useobject,
142
+ trackparent=useobject
143
+ and (
144
+ prop.single_parent
145
+ or prop.direction is interfaces.ONETOMANY
146
+ ),
147
+ typecallable=typecallable,
148
+ callable_=callable_,
149
+ active_history=active_history,
150
+ default_scalar_value=default_scalar_value,
151
+ impl_class=impl_class,
152
+ send_modified_events=not useobject or not prop.viewonly,
153
+ doc=prop.doc,
154
+ **kw,
155
+ )
156
+
157
+ for hook in pre_validate_hooks:
158
+ hook(desc, prop)
159
+
160
+ for super_m in m.iterate_to_root():
161
+ if prop.key in super_m.validators:
162
+ fn, opts = super_m.validators[prop.key]
163
+ orm_util._validator_events(desc, prop.key, fn, **opts)
164
+ break
165
+
166
+ for hook in post_validate_hooks:
167
+ hook(desc, prop)
168
+
169
+
170
+ @properties.ColumnProperty.strategy_for(instrument=False, deferred=False)
171
+ class _UninstrumentedColumnLoader(LoaderStrategy):
172
+ """Represent a non-instrumented MapperProperty.
173
+
174
+ The polymorphic_on argument of mapper() often results in this,
175
+ if the argument is against the with_polymorphic selectable.
176
+
177
+ """
178
+
179
+ __slots__ = ("columns",)
180
+
181
+ def __init__(self, parent, strategy_key):
182
+ super().__init__(parent, strategy_key)
183
+ self.columns = self.parent_property.columns
184
+
185
+ def setup_query(
186
+ self,
187
+ compile_state,
188
+ query_entity,
189
+ path,
190
+ loadopt,
191
+ adapter,
192
+ column_collection=None,
193
+ **kwargs,
194
+ ):
195
+ for c in self.columns:
196
+ if adapter:
197
+ c = adapter.columns[c]
198
+ compile_state._append_dedupe_col_collection(c, column_collection)
199
+
200
+ def create_row_processor(
201
+ self,
202
+ context,
203
+ query_entity,
204
+ path,
205
+ loadopt,
206
+ mapper,
207
+ result,
208
+ adapter,
209
+ populators,
210
+ ):
211
+ pass
212
+
213
+
214
+ @log.class_logger
215
+ @properties.ColumnProperty.strategy_for(instrument=True, deferred=False)
216
+ class _ColumnLoader(LoaderStrategy):
217
+ """Provide loading behavior for a :class:`.ColumnProperty`."""
218
+
219
+ __slots__ = "columns", "is_composite"
220
+
221
+ def __init__(self, parent, strategy_key):
222
+ super().__init__(parent, strategy_key)
223
+ self.columns = self.parent_property.columns
224
+ self.is_composite = hasattr(self.parent_property, "composite_class")
225
+
226
+ def setup_query(
227
+ self,
228
+ compile_state,
229
+ query_entity,
230
+ path,
231
+ loadopt,
232
+ adapter,
233
+ column_collection,
234
+ memoized_populators,
235
+ check_for_adapt=False,
236
+ **kwargs,
237
+ ):
238
+ for c in self.columns:
239
+ if adapter:
240
+ if check_for_adapt:
241
+ c = adapter.adapt_check_present(c)
242
+ if c is None:
243
+ return
244
+ else:
245
+ c = adapter.columns[c]
246
+
247
+ compile_state._append_dedupe_col_collection(c, column_collection)
248
+
249
+ fetch = self.columns[0]
250
+ if adapter:
251
+ fetch = adapter.columns[fetch]
252
+ if fetch is None:
253
+ # None happens here only for dml bulk_persistence cases
254
+ # when context.DMLReturningColFilter is used
255
+ return
256
+
257
+ memoized_populators[self.parent_property] = fetch
258
+
259
+ def init_class_attribute(self, mapper):
260
+ self.is_class_level = True
261
+ coltype = self.columns[0].type
262
+ # TODO: check all columns ? check for foreign key as well?
263
+ active_history = (
264
+ self.parent_property.active_history
265
+ or self.columns[0].primary_key
266
+ or (
267
+ mapper.version_id_col is not None
268
+ and mapper._columntoproperty.get(mapper.version_id_col, None)
269
+ is self.parent_property
270
+ )
271
+ )
272
+
273
+ _register_attribute(
274
+ self.parent_property,
275
+ mapper,
276
+ useobject=False,
277
+ compare_function=coltype.compare_values,
278
+ active_history=active_history,
279
+ default_scalar_value=self.parent_property._default_scalar_value,
280
+ )
281
+
282
+ def create_row_processor(
283
+ self,
284
+ context,
285
+ query_entity,
286
+ path,
287
+ loadopt,
288
+ mapper,
289
+ result,
290
+ adapter,
291
+ populators,
292
+ ):
293
+ # look through list of columns represented here
294
+ # to see which, if any, is present in the row.
295
+
296
+ for col in self.columns:
297
+ if adapter:
298
+ col = adapter.columns[col]
299
+ getter = result._getter(col, False)
300
+ if getter:
301
+ populators["quick"].append((self.key, getter))
302
+ break
303
+ else:
304
+ populators["expire"].append((self.key, True))
305
+
306
+
307
+ @log.class_logger
308
+ @properties.ColumnProperty.strategy_for(query_expression=True)
309
+ class _ExpressionColumnLoader(_ColumnLoader):
310
+ def __init__(self, parent, strategy_key):
311
+ super().__init__(parent, strategy_key)
312
+
313
+ # compare to the "default" expression that is mapped in
314
+ # the column. If it's sql.null, we don't need to render
315
+ # unless an expr is passed in the options.
316
+ null = sql.null().label(None)
317
+ self._have_default_expression = any(
318
+ not c.compare(null) for c in self.parent_property.columns
319
+ )
320
+
321
+ def setup_query(
322
+ self,
323
+ compile_state,
324
+ query_entity,
325
+ path,
326
+ loadopt,
327
+ adapter,
328
+ column_collection,
329
+ memoized_populators,
330
+ **kwargs,
331
+ ):
332
+ columns = None
333
+ if loadopt and loadopt._extra_criteria:
334
+ columns = loadopt._extra_criteria
335
+
336
+ elif self._have_default_expression:
337
+ columns = self.parent_property.columns
338
+
339
+ if columns is None:
340
+ return
341
+
342
+ for c in columns:
343
+ if adapter:
344
+ c = adapter.columns[c]
345
+ compile_state._append_dedupe_col_collection(c, column_collection)
346
+
347
+ fetch = columns[0]
348
+ if adapter:
349
+ fetch = adapter.columns[fetch]
350
+ if fetch is None:
351
+ # None is not expected to be the result of any
352
+ # adapter implementation here, however there may be theoretical
353
+ # usages of returning() with context.DMLReturningColFilter
354
+ return
355
+
356
+ memoized_populators[self.parent_property] = fetch
357
+
358
+ # if the column being loaded is the polymorphic discriminator,
359
+ # and we have a with_expression() providing the actual column,
360
+ # update the query_entity to use the actual column instead of
361
+ # the default expression
362
+ if (
363
+ query_entity._polymorphic_discriminator is self.columns[0]
364
+ and loadopt
365
+ and loadopt._extra_criteria
366
+ ):
367
+ query_entity._polymorphic_discriminator = columns[0]
368
+
369
+ def create_row_processor(
370
+ self,
371
+ context,
372
+ query_entity,
373
+ path,
374
+ loadopt,
375
+ mapper,
376
+ result,
377
+ adapter,
378
+ populators,
379
+ ):
380
+ # look through list of columns represented here
381
+ # to see which, if any, is present in the row.
382
+ if loadopt and loadopt._extra_criteria:
383
+ columns = loadopt._extra_criteria
384
+
385
+ for col in columns:
386
+ if adapter:
387
+ col = adapter.columns[col]
388
+ getter = result._getter(col, False)
389
+ if getter:
390
+ populators["quick"].append((self.key, getter))
391
+ break
392
+ else:
393
+ populators["expire"].append((self.key, True))
394
+
395
+ def init_class_attribute(self, mapper):
396
+ self.is_class_level = True
397
+
398
+ _register_attribute(
399
+ self.parent_property,
400
+ mapper,
401
+ useobject=False,
402
+ compare_function=self.columns[0].type.compare_values,
403
+ accepts_scalar_loader=False,
404
+ default_scalar_value=self.parent_property._default_scalar_value,
405
+ )
406
+
407
+
408
+ @log.class_logger
409
+ @properties.ColumnProperty.strategy_for(deferred=True, instrument=True)
410
+ @properties.ColumnProperty.strategy_for(
411
+ deferred=True, instrument=True, raiseload=True
412
+ )
413
+ @properties.ColumnProperty.strategy_for(do_nothing=True)
414
+ class _DeferredColumnLoader(LoaderStrategy):
415
+ """Provide loading behavior for a deferred :class:`.ColumnProperty`."""
416
+
417
+ __slots__ = "columns", "group", "raiseload"
418
+
419
+ def __init__(self, parent, strategy_key):
420
+ super().__init__(parent, strategy_key)
421
+ if hasattr(self.parent_property, "composite_class"):
422
+ raise NotImplementedError(
423
+ "Deferred loading for composite types not implemented yet"
424
+ )
425
+ self.raiseload = self.strategy_opts.get("raiseload", False)
426
+ self.columns = self.parent_property.columns
427
+ self.group = self.parent_property.group
428
+
429
+ def create_row_processor(
430
+ self,
431
+ context,
432
+ query_entity,
433
+ path,
434
+ loadopt,
435
+ mapper,
436
+ result,
437
+ adapter,
438
+ populators,
439
+ ):
440
+ # for a DeferredColumnLoader, this method is only used during a
441
+ # "row processor only" query; see test_deferred.py ->
442
+ # tests with "rowproc_only" in their name. As of the 1.0 series,
443
+ # loading._instance_processor doesn't use a "row processing" function
444
+ # to populate columns, instead it uses data in the "populators"
445
+ # dictionary. Normally, the DeferredColumnLoader.setup_query()
446
+ # sets up that data in the "memoized_populators" dictionary
447
+ # and "create_row_processor()" here is never invoked.
448
+
449
+ if (
450
+ context.refresh_state
451
+ and context.query._compile_options._only_load_props
452
+ and self.key in context.query._compile_options._only_load_props
453
+ ):
454
+ self.parent_property._get_strategy(
455
+ (("deferred", False), ("instrument", True))
456
+ ).create_row_processor(
457
+ context,
458
+ query_entity,
459
+ path,
460
+ loadopt,
461
+ mapper,
462
+ result,
463
+ adapter,
464
+ populators,
465
+ )
466
+
467
+ elif not self.is_class_level:
468
+ if self.raiseload:
469
+ set_deferred_for_local_state = (
470
+ self.parent_property._raise_column_loader
471
+ )
472
+ else:
473
+ set_deferred_for_local_state = (
474
+ self.parent_property._deferred_column_loader
475
+ )
476
+ populators["new"].append((self.key, set_deferred_for_local_state))
477
+ else:
478
+ populators["expire"].append((self.key, False))
479
+
480
+ def init_class_attribute(self, mapper):
481
+ self.is_class_level = True
482
+
483
+ _register_attribute(
484
+ self.parent_property,
485
+ mapper,
486
+ useobject=False,
487
+ compare_function=self.columns[0].type.compare_values,
488
+ callable_=self._load_for_state,
489
+ load_on_unexpire=False,
490
+ default_scalar_value=self.parent_property._default_scalar_value,
491
+ )
492
+
493
+ def setup_query(
494
+ self,
495
+ compile_state,
496
+ query_entity,
497
+ path,
498
+ loadopt,
499
+ adapter,
500
+ column_collection,
501
+ memoized_populators,
502
+ only_load_props=None,
503
+ **kw,
504
+ ):
505
+ if (
506
+ (
507
+ compile_state.compile_options._render_for_subquery
508
+ and self.parent_property._renders_in_subqueries
509
+ )
510
+ or (
511
+ loadopt
512
+ and set(self.columns).intersection(
513
+ self.parent._should_undefer_in_wildcard
514
+ )
515
+ )
516
+ or (
517
+ loadopt
518
+ and self.group
519
+ and loadopt.local_opts.get(
520
+ "undefer_group_%s" % self.group, False
521
+ )
522
+ )
523
+ or (only_load_props and self.key in only_load_props)
524
+ ):
525
+ self.parent_property._get_strategy(
526
+ (("deferred", False), ("instrument", True))
527
+ ).setup_query(
528
+ compile_state,
529
+ query_entity,
530
+ path,
531
+ loadopt,
532
+ adapter,
533
+ column_collection,
534
+ memoized_populators,
535
+ **kw,
536
+ )
537
+ elif self.is_class_level:
538
+ memoized_populators[self.parent_property] = _SET_DEFERRED_EXPIRED
539
+ elif not self.raiseload:
540
+ memoized_populators[self.parent_property] = _DEFER_FOR_STATE
541
+ else:
542
+ memoized_populators[self.parent_property] = _RAISE_FOR_STATE
543
+
544
+ def _load_for_state(self, state, passive):
545
+ if not state.key:
546
+ return LoaderCallableStatus.ATTR_EMPTY
547
+
548
+ if not passive & PassiveFlag.SQL_OK:
549
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
550
+
551
+ localparent = state.manager.mapper
552
+
553
+ if self.group:
554
+ toload = [
555
+ p.key
556
+ for p in localparent.iterate_properties
557
+ if isinstance(p, StrategizedProperty)
558
+ and isinstance(p.strategy, _DeferredColumnLoader)
559
+ and p.group == self.group
560
+ ]
561
+ else:
562
+ toload = [self.key]
563
+
564
+ # narrow the keys down to just those which have no history
565
+ group = [k for k in toload if k in state.unmodified]
566
+
567
+ session = _state_session(state)
568
+ if session is None:
569
+ raise orm_exc.DetachedInstanceError(
570
+ "Parent instance %s is not bound to a Session; "
571
+ "deferred load operation of attribute '%s' cannot proceed"
572
+ % (orm_util.state_str(state), self.key)
573
+ )
574
+
575
+ if self.raiseload:
576
+ self._invoke_raise_load(state, passive, "raise")
577
+
578
+ loading._load_scalar_attributes(
579
+ state.mapper, state, set(group), PASSIVE_OFF
580
+ )
581
+
582
+ return LoaderCallableStatus.ATTR_WAS_SET
583
+
584
+ def _invoke_raise_load(self, state, passive, lazy):
585
+ raise sa_exc.InvalidRequestError(
586
+ "'%s' is not available due to raiseload=True" % (self,)
587
+ )
588
+
589
+
590
+ class _LoadDeferredColumns:
591
+ """serializable loader object used by DeferredColumnLoader"""
592
+
593
+ def __init__(self, key: str, raiseload: bool = False):
594
+ self.key = key
595
+ self.raiseload = raiseload
596
+
597
+ def __call__(self, state, passive=attributes.PASSIVE_OFF):
598
+ key = self.key
599
+
600
+ localparent = state.manager.mapper
601
+ prop = localparent._props[key]
602
+ if self.raiseload:
603
+ strategy_key = (
604
+ ("deferred", True),
605
+ ("instrument", True),
606
+ ("raiseload", True),
607
+ )
608
+ else:
609
+ strategy_key = (("deferred", True), ("instrument", True))
610
+ strategy = prop._get_strategy(strategy_key)
611
+ return strategy._load_for_state(state, passive)
612
+
613
+
614
+ class _AbstractRelationshipLoader(LoaderStrategy):
615
+ """LoaderStratgies which deal with related objects."""
616
+
617
+ __slots__ = "mapper", "target", "uselist", "entity"
618
+
619
+ def __init__(self, parent, strategy_key):
620
+ super().__init__(parent, strategy_key)
621
+ self.mapper = self.parent_property.mapper
622
+ self.entity = self.parent_property.entity
623
+ self.target = self.parent_property.target
624
+ self.uselist = self.parent_property.uselist
625
+
626
+ def _immediateload_create_row_processor(
627
+ self,
628
+ context,
629
+ query_entity,
630
+ path,
631
+ loadopt,
632
+ mapper,
633
+ result,
634
+ adapter,
635
+ populators,
636
+ ):
637
+ return self.parent_property._get_strategy(
638
+ (("lazy", "immediate"),)
639
+ ).create_row_processor(
640
+ context,
641
+ query_entity,
642
+ path,
643
+ loadopt,
644
+ mapper,
645
+ result,
646
+ adapter,
647
+ populators,
648
+ )
649
+
650
+
651
+ @log.class_logger
652
+ @relationships.RelationshipProperty.strategy_for(do_nothing=True)
653
+ class _DoNothingLoader(LoaderStrategy):
654
+ """Relationship loader that makes no change to the object's state.
655
+
656
+ Compared to NoLoader, this loader does not initialize the
657
+ collection/attribute to empty/none; the usual default LazyLoader will
658
+ take effect.
659
+
660
+ """
661
+
662
+
663
+ @log.class_logger
664
+ @relationships.RelationshipProperty.strategy_for(lazy="noload")
665
+ @relationships.RelationshipProperty.strategy_for(lazy=None)
666
+ class _NoLoader(_AbstractRelationshipLoader):
667
+ """Provide loading behavior for a :class:`.Relationship`
668
+ with "lazy=None".
669
+
670
+ """
671
+
672
+ __slots__ = ()
673
+
674
+ @util.deprecated(
675
+ "2.1",
676
+ "The ``noload`` loader strategy is deprecated and will be removed "
677
+ "in a future release. This option "
678
+ "produces incorrect results by returning ``None`` for related "
679
+ "items.",
680
+ )
681
+ def init_class_attribute(self, mapper):
682
+ self.is_class_level = True
683
+
684
+ _register_attribute(
685
+ self.parent_property,
686
+ mapper,
687
+ useobject=True,
688
+ typecallable=self.parent_property.collection_class,
689
+ )
690
+
691
+ def create_row_processor(
692
+ self,
693
+ context,
694
+ query_entity,
695
+ path,
696
+ loadopt,
697
+ mapper,
698
+ result,
699
+ adapter,
700
+ populators,
701
+ ):
702
+ def invoke_no_load(state, dict_, row):
703
+ if self.uselist:
704
+ attributes.init_state_collection(state, dict_, self.key)
705
+ else:
706
+ dict_[self.key] = None
707
+
708
+ populators["new"].append((self.key, invoke_no_load))
709
+
710
+
711
+ @log.class_logger
712
+ @relationships.RelationshipProperty.strategy_for(lazy=True)
713
+ @relationships.RelationshipProperty.strategy_for(lazy="select")
714
+ @relationships.RelationshipProperty.strategy_for(lazy="raise")
715
+ @relationships.RelationshipProperty.strategy_for(lazy="raise_on_sql")
716
+ @relationships.RelationshipProperty.strategy_for(lazy="baked_select")
717
+ class _LazyLoader(
718
+ _AbstractRelationshipLoader, util.MemoizedSlots, log.Identified
719
+ ):
720
+ """Provide loading behavior for a :class:`.Relationship`
721
+ with "lazy=True", that is loads when first accessed.
722
+
723
+ """
724
+
725
+ __slots__ = (
726
+ "_lazywhere",
727
+ "_rev_lazywhere",
728
+ "_lazyload_reverse_option",
729
+ "_order_by",
730
+ "use_get",
731
+ "is_aliased_class",
732
+ "_bind_to_col",
733
+ "_equated_columns",
734
+ "_rev_bind_to_col",
735
+ "_rev_equated_columns",
736
+ "_simple_lazy_clause",
737
+ "_raise_always",
738
+ "_raise_on_sql",
739
+ )
740
+
741
+ _lazywhere: ColumnElement[bool]
742
+ _bind_to_col: Dict[str, ColumnElement[Any]]
743
+ _rev_lazywhere: ColumnElement[bool]
744
+ _rev_bind_to_col: Dict[str, ColumnElement[Any]]
745
+
746
+ parent_property: RelationshipProperty[Any]
747
+
748
+ def __init__(
749
+ self, parent: RelationshipProperty[Any], strategy_key: Tuple[Any, ...]
750
+ ):
751
+ super().__init__(parent, strategy_key)
752
+ self._raise_always = self.strategy_opts["lazy"] == "raise"
753
+ self._raise_on_sql = self.strategy_opts["lazy"] == "raise_on_sql"
754
+
755
+ self.is_aliased_class = inspect(self.entity).is_aliased_class
756
+
757
+ join_condition = self.parent_property._join_condition
758
+ (
759
+ self._lazywhere,
760
+ self._bind_to_col,
761
+ self._equated_columns,
762
+ ) = join_condition.create_lazy_clause()
763
+
764
+ (
765
+ self._rev_lazywhere,
766
+ self._rev_bind_to_col,
767
+ self._rev_equated_columns,
768
+ ) = join_condition.create_lazy_clause(reverse_direction=True)
769
+
770
+ if self.parent_property.order_by:
771
+ self._order_by = util.to_list(self.parent_property.order_by)
772
+ else:
773
+ self._order_by = None
774
+
775
+ self.logger.info("%s lazy loading clause %s", self, self._lazywhere)
776
+
777
+ # determine if our "lazywhere" clause is the same as the mapper's
778
+ # get() clause. then we can just use mapper.get()
779
+ #
780
+ # TODO: the "not self.uselist" can be taken out entirely; a m2o
781
+ # load that populates for a list (very unusual, but is possible with
782
+ # the API) can still set for "None" and the attribute system will
783
+ # populate as an empty list.
784
+ self.use_get = (
785
+ not self.is_aliased_class
786
+ and not self.uselist
787
+ and self.entity._get_clause[0].compare(
788
+ self._lazywhere,
789
+ use_proxies=True,
790
+ compare_keys=False,
791
+ equivalents=self.mapper._equivalent_columns,
792
+ )
793
+ )
794
+
795
+ if self.use_get:
796
+ for col in list(self._equated_columns):
797
+ if col in self.mapper._equivalent_columns:
798
+ for c in self.mapper._equivalent_columns[col]:
799
+ self._equated_columns[c] = self._equated_columns[col]
800
+
801
+ self.logger.info(
802
+ "%s will use Session.get() to optimize instance loads", self
803
+ )
804
+
805
+ def init_class_attribute(self, mapper):
806
+ self.is_class_level = True
807
+
808
+ _legacy_inactive_history_style = (
809
+ self.parent_property._legacy_inactive_history_style
810
+ )
811
+
812
+ if self.parent_property.active_history:
813
+ active_history = True
814
+ _deferred_history = False
815
+
816
+ elif (
817
+ self.parent_property.direction is not interfaces.MANYTOONE
818
+ or not self.use_get
819
+ ):
820
+ if _legacy_inactive_history_style:
821
+ active_history = True
822
+ _deferred_history = False
823
+ else:
824
+ active_history = False
825
+ _deferred_history = True
826
+ else:
827
+ active_history = _deferred_history = False
828
+
829
+ _register_attribute(
830
+ self.parent_property,
831
+ mapper,
832
+ useobject=True,
833
+ callable_=self._load_for_state,
834
+ typecallable=self.parent_property.collection_class,
835
+ active_history=active_history,
836
+ _deferred_history=_deferred_history,
837
+ )
838
+
839
+ def _memoized_attr__simple_lazy_clause(self):
840
+ lazywhere = self._lazywhere
841
+
842
+ criterion, bind_to_col = (lazywhere, self._bind_to_col)
843
+
844
+ params = []
845
+
846
+ def visit_bindparam(bindparam):
847
+ bindparam.unique = False
848
+
849
+ visitors.traverse(criterion, {}, {"bindparam": visit_bindparam})
850
+
851
+ def visit_bindparam(bindparam):
852
+ if bindparam._identifying_key in bind_to_col:
853
+ params.append(
854
+ (
855
+ bindparam.key,
856
+ bind_to_col[bindparam._identifying_key],
857
+ None,
858
+ )
859
+ )
860
+ elif bindparam.callable is None:
861
+ params.append((bindparam.key, None, bindparam.value))
862
+
863
+ criterion = visitors.cloned_traverse(
864
+ criterion, {}, {"bindparam": visit_bindparam}
865
+ )
866
+
867
+ return criterion, params
868
+
869
+ def _generate_lazy_clause(self, state, passive):
870
+ criterion, param_keys = self._simple_lazy_clause
871
+
872
+ if state is None:
873
+ return sql_util.adapt_criterion_to_null(
874
+ criterion, [key for key, ident, value in param_keys]
875
+ )
876
+
877
+ mapper = self.parent_property.parent
878
+
879
+ o = state.obj() # strong ref
880
+ dict_ = attributes.instance_dict(o)
881
+
882
+ if passive & PassiveFlag.INIT_OK:
883
+ passive ^= PassiveFlag.INIT_OK
884
+
885
+ params = {}
886
+ for key, ident, value in param_keys:
887
+ if ident is not None:
888
+ if passive and passive & PassiveFlag.LOAD_AGAINST_COMMITTED:
889
+ value = mapper._get_committed_state_attr_by_column(
890
+ state, dict_, ident, passive
891
+ )
892
+ else:
893
+ value = mapper._get_state_attr_by_column(
894
+ state, dict_, ident, passive
895
+ )
896
+
897
+ params[key] = value
898
+
899
+ return criterion, params
900
+
901
+ def _invoke_raise_load(self, state, passive, lazy):
902
+ raise sa_exc.InvalidRequestError(
903
+ "'%s' is not available due to lazy='%s'" % (self, lazy)
904
+ )
905
+
906
+ def _load_for_state(
907
+ self,
908
+ state,
909
+ passive,
910
+ loadopt=None,
911
+ extra_criteria=(),
912
+ extra_options=(),
913
+ alternate_effective_path=None,
914
+ execution_options=util.EMPTY_DICT,
915
+ ):
916
+ if not state.key and (
917
+ (
918
+ not self.parent_property.load_on_pending
919
+ and not state._load_pending
920
+ )
921
+ or not state.session_id
922
+ ):
923
+ return LoaderCallableStatus.ATTR_EMPTY
924
+
925
+ pending = not state.key
926
+ primary_key_identity = None
927
+
928
+ use_get = self.use_get and (not loadopt or not loadopt._extra_criteria)
929
+
930
+ if (not passive & PassiveFlag.SQL_OK and not use_get) or (
931
+ not passive & attributes.NON_PERSISTENT_OK and pending
932
+ ):
933
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
934
+
935
+ if (
936
+ # we were given lazy="raise"
937
+ self._raise_always
938
+ # the no_raise history-related flag was not passed
939
+ and not passive & PassiveFlag.NO_RAISE
940
+ and (
941
+ # if we are use_get and related_object_ok is disabled,
942
+ # which means we are at most looking in the identity map
943
+ # for history purposes or otherwise returning
944
+ # PASSIVE_NO_RESULT, don't raise. This is also a
945
+ # history-related flag
946
+ not use_get
947
+ or passive & PassiveFlag.RELATED_OBJECT_OK
948
+ )
949
+ ):
950
+ self._invoke_raise_load(state, passive, "raise")
951
+
952
+ session = _state_session(state)
953
+ if not session:
954
+ if passive & PassiveFlag.NO_RAISE:
955
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
956
+
957
+ raise orm_exc.DetachedInstanceError(
958
+ "Parent instance %s is not bound to a Session; "
959
+ "lazy load operation of attribute '%s' cannot proceed"
960
+ % (orm_util.state_str(state), self.key)
961
+ )
962
+
963
+ # if we have a simple primary key load, check the
964
+ # identity map without generating a Query at all
965
+ if use_get:
966
+ primary_key_identity = self._get_ident_for_use_get(
967
+ session, state, passive
968
+ )
969
+ if LoaderCallableStatus.PASSIVE_NO_RESULT in primary_key_identity:
970
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
971
+ elif LoaderCallableStatus.NEVER_SET in primary_key_identity:
972
+ return LoaderCallableStatus.NEVER_SET
973
+
974
+ # test for None alone in primary_key_identity based on
975
+ # allow_partial_pks preference. PASSIVE_NO_RESULT and NEVER_SET
976
+ # have already been tested above
977
+ if not self.mapper.allow_partial_pks:
978
+ if _none_only_set.intersection(primary_key_identity):
979
+ return None
980
+ else:
981
+ if _none_only_set.issuperset(primary_key_identity):
982
+ return None
983
+
984
+ if (
985
+ self.key in state.dict
986
+ and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD
987
+ ):
988
+ return LoaderCallableStatus.ATTR_WAS_SET
989
+
990
+ # look for this identity in the identity map. Delegate to the
991
+ # Query class in use, as it may have special rules for how it
992
+ # does this, including how it decides what the correct
993
+ # identity_token would be for this identity.
994
+
995
+ instance = session._identity_lookup(
996
+ self.entity,
997
+ primary_key_identity,
998
+ passive=passive,
999
+ lazy_loaded_from=state,
1000
+ )
1001
+
1002
+ if instance is not None:
1003
+ if instance is LoaderCallableStatus.PASSIVE_CLASS_MISMATCH:
1004
+ return None
1005
+ else:
1006
+ return instance
1007
+ elif (
1008
+ not passive & PassiveFlag.SQL_OK
1009
+ or not passive & PassiveFlag.RELATED_OBJECT_OK
1010
+ ):
1011
+ return LoaderCallableStatus.PASSIVE_NO_RESULT
1012
+
1013
+ return self._emit_lazyload(
1014
+ session,
1015
+ state,
1016
+ primary_key_identity,
1017
+ passive,
1018
+ loadopt,
1019
+ extra_criteria,
1020
+ extra_options,
1021
+ alternate_effective_path,
1022
+ execution_options,
1023
+ )
1024
+
1025
+ def _get_ident_for_use_get(self, session, state, passive):
1026
+ instance_mapper = state.manager.mapper
1027
+
1028
+ if passive & PassiveFlag.LOAD_AGAINST_COMMITTED:
1029
+ get_attr = instance_mapper._get_committed_state_attr_by_column
1030
+ else:
1031
+ get_attr = instance_mapper._get_state_attr_by_column
1032
+
1033
+ dict_ = state.dict
1034
+
1035
+ return [
1036
+ get_attr(state, dict_, self._equated_columns[pk], passive=passive)
1037
+ for pk in self.mapper.primary_key
1038
+ ]
1039
+
1040
+ @util.preload_module("sqlalchemy.orm.strategy_options")
1041
+ def _emit_lazyload(
1042
+ self,
1043
+ session,
1044
+ state,
1045
+ primary_key_identity,
1046
+ passive,
1047
+ loadopt,
1048
+ extra_criteria,
1049
+ extra_options,
1050
+ alternate_effective_path,
1051
+ execution_options,
1052
+ ):
1053
+ strategy_options = util.preloaded.orm_strategy_options
1054
+
1055
+ clauseelement = self.entity.__clause_element__()
1056
+ stmt = Select._create_raw_select(
1057
+ _raw_columns=[clauseelement],
1058
+ _propagate_attrs=clauseelement._propagate_attrs,
1059
+ _compile_options=_ORMCompileState.default_compile_options,
1060
+ )
1061
+ load_options = QueryContext.default_load_options
1062
+
1063
+ load_options += {
1064
+ "_invoke_all_eagers": False,
1065
+ "_lazy_loaded_from": state,
1066
+ }
1067
+
1068
+ if self.parent_property.secondary is not None:
1069
+ stmt = stmt.select_from(
1070
+ self.mapper, self.parent_property.secondary
1071
+ )
1072
+
1073
+ pending = not state.key
1074
+
1075
+ # don't autoflush on pending
1076
+ if pending or passive & attributes.NO_AUTOFLUSH:
1077
+ stmt._execution_options = util.immutabledict({"autoflush": False})
1078
+
1079
+ use_get = self.use_get
1080
+
1081
+ if state.load_options or (loadopt and loadopt._extra_criteria):
1082
+ if alternate_effective_path is None:
1083
+ effective_path = state.load_path[self.parent_property]
1084
+ else:
1085
+ effective_path = alternate_effective_path[self.parent_property]
1086
+
1087
+ opts = state.load_options
1088
+
1089
+ if loadopt and loadopt._extra_criteria:
1090
+ use_get = False
1091
+ opts += (
1092
+ orm_util.LoaderCriteriaOption(self.entity, extra_criteria),
1093
+ )
1094
+
1095
+ stmt._with_options = opts
1096
+ elif alternate_effective_path is None:
1097
+ # this path is used if there are not already any options
1098
+ # in the query, but an event may want to add them
1099
+ effective_path = state.mapper._path_registry[self.parent_property]
1100
+ else:
1101
+ # added by immediateloader
1102
+ effective_path = alternate_effective_path[self.parent_property]
1103
+
1104
+ if extra_options:
1105
+ stmt._with_options += extra_options
1106
+
1107
+ stmt._compile_options += {"_current_path": effective_path}
1108
+
1109
+ if use_get:
1110
+ if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE:
1111
+ self._invoke_raise_load(state, passive, "raise_on_sql")
1112
+
1113
+ return loading._load_on_pk_identity(
1114
+ session,
1115
+ stmt,
1116
+ primary_key_identity,
1117
+ load_options=load_options,
1118
+ execution_options=execution_options,
1119
+ )
1120
+
1121
+ if self._order_by:
1122
+ stmt._order_by_clauses = self._order_by
1123
+
1124
+ def _lazyload_reverse(compile_context):
1125
+ for rev in self.parent_property._reverse_property:
1126
+ # reverse props that are MANYTOONE are loading *this*
1127
+ # object from get(), so don't need to eager out to those.
1128
+ if (
1129
+ rev.direction is interfaces.MANYTOONE
1130
+ and rev._use_get
1131
+ and not isinstance(rev.strategy, _LazyLoader)
1132
+ ):
1133
+ strategy_options.Load._construct_for_existing_path(
1134
+ compile_context.compile_options._current_path[
1135
+ rev.parent
1136
+ ]
1137
+ ).lazyload(rev).process_compile_state(compile_context)
1138
+
1139
+ stmt = stmt._add_compile_state_func(
1140
+ _lazyload_reverse, self.parent_property
1141
+ )
1142
+
1143
+ lazy_clause, params = self._generate_lazy_clause(state, passive)
1144
+
1145
+ if execution_options:
1146
+ execution_options = util.EMPTY_DICT.merge_with(
1147
+ execution_options, {"_sa_orm_load_options": load_options}
1148
+ )
1149
+ else:
1150
+ execution_options = {
1151
+ "_sa_orm_load_options": load_options,
1152
+ }
1153
+
1154
+ if (
1155
+ self.key in state.dict
1156
+ and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD
1157
+ ):
1158
+ return LoaderCallableStatus.ATTR_WAS_SET
1159
+
1160
+ if pending:
1161
+ if util.has_intersection(orm_util._none_set, params.values()):
1162
+ return None
1163
+
1164
+ elif util.has_intersection(orm_util._never_set, params.values()):
1165
+ return None
1166
+
1167
+ if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE:
1168
+ self._invoke_raise_load(state, passive, "raise_on_sql")
1169
+
1170
+ stmt._where_criteria = (lazy_clause,)
1171
+
1172
+ result = session.execute(
1173
+ stmt, params, execution_options=execution_options
1174
+ )
1175
+
1176
+ result = result.unique().scalars().all()
1177
+
1178
+ if self.uselist:
1179
+ return result
1180
+ else:
1181
+ l = len(result)
1182
+ if l:
1183
+ if l > 1:
1184
+ util.warn(
1185
+ "Multiple rows returned with "
1186
+ "uselist=False for lazily-loaded attribute '%s' "
1187
+ % self.parent_property
1188
+ )
1189
+
1190
+ return result[0]
1191
+ else:
1192
+ return None
1193
+
1194
+ def create_row_processor(
1195
+ self,
1196
+ context,
1197
+ query_entity,
1198
+ path,
1199
+ loadopt,
1200
+ mapper,
1201
+ result,
1202
+ adapter,
1203
+ populators,
1204
+ ):
1205
+ key = self.key
1206
+
1207
+ if (
1208
+ context.load_options._is_user_refresh
1209
+ and context.query._compile_options._only_load_props
1210
+ and self.key in context.query._compile_options._only_load_props
1211
+ ):
1212
+ return self._immediateload_create_row_processor(
1213
+ context,
1214
+ query_entity,
1215
+ path,
1216
+ loadopt,
1217
+ mapper,
1218
+ result,
1219
+ adapter,
1220
+ populators,
1221
+ )
1222
+
1223
+ if not self.is_class_level or (loadopt and loadopt._extra_criteria):
1224
+ # we are not the primary manager for this attribute
1225
+ # on this class - set up a
1226
+ # per-instance lazyloader, which will override the
1227
+ # class-level behavior.
1228
+ # this currently only happens when using a
1229
+ # "lazyload" option on a "no load"
1230
+ # attribute - "eager" attributes always have a
1231
+ # class-level lazyloader installed.
1232
+ set_lazy_callable = (
1233
+ InstanceState._instance_level_callable_processor
1234
+ )(
1235
+ mapper.class_manager,
1236
+ _LoadLazyAttribute(
1237
+ key,
1238
+ self,
1239
+ loadopt,
1240
+ (
1241
+ loadopt._generate_extra_criteria(context)
1242
+ if loadopt._extra_criteria
1243
+ else _NO_EXTRA_CRITERIA
1244
+ ),
1245
+ ),
1246
+ key,
1247
+ )
1248
+
1249
+ populators["new"].append((self.key, set_lazy_callable))
1250
+ elif context.populate_existing or mapper.always_refresh:
1251
+
1252
+ def reset_for_lazy_callable(state, dict_, row):
1253
+ # we are the primary manager for this attribute on
1254
+ # this class - reset its
1255
+ # per-instance attribute state, so that the class-level
1256
+ # lazy loader is
1257
+ # executed when next referenced on this instance.
1258
+ # this is needed in
1259
+ # populate_existing() types of scenarios to reset
1260
+ # any existing state.
1261
+ state._reset(dict_, key)
1262
+
1263
+ populators["new"].append((self.key, reset_for_lazy_callable))
1264
+
1265
+
1266
+ class _LoadLazyAttribute:
1267
+ """semi-serializable loader object used by LazyLoader
1268
+
1269
+ Historically, this object would be carried along with instances that
1270
+ needed to run lazyloaders, so it had to be serializable to support
1271
+ cached instances.
1272
+
1273
+ this is no longer a general requirement, and the case where this object
1274
+ is used is exactly the case where we can't really serialize easily,
1275
+ which is when extra criteria in the loader option is present.
1276
+
1277
+ We can't reliably serialize that as it refers to mapped entities and
1278
+ AliasedClass objects that are local to the current process, which would
1279
+ need to be matched up on deserialize e.g. the sqlalchemy.ext.serializer
1280
+ approach.
1281
+
1282
+ """
1283
+
1284
+ def __init__(self, key, initiating_strategy, loadopt, extra_criteria):
1285
+ self.key = key
1286
+ self.strategy_key = initiating_strategy.strategy_key
1287
+ self.loadopt = loadopt
1288
+ self.extra_criteria = extra_criteria
1289
+
1290
+ def __getstate__(self):
1291
+ if self.extra_criteria is not _NO_EXTRA_CRITERIA:
1292
+ util.warn(
1293
+ "Can't reliably serialize a lazyload() option that "
1294
+ "contains additional criteria; please use eager loading "
1295
+ "for this case"
1296
+ )
1297
+ return {
1298
+ "key": self.key,
1299
+ "strategy_key": self.strategy_key,
1300
+ "loadopt": self.loadopt,
1301
+ "extra_criteria": _NO_EXTRA_CRITERIA,
1302
+ }
1303
+
1304
+ def __call__(self, state, passive=attributes.PASSIVE_OFF):
1305
+ key = self.key
1306
+ instance_mapper = state.manager.mapper
1307
+ prop = instance_mapper._props[key]
1308
+ strategy = prop._strategies[self.strategy_key]
1309
+
1310
+ return strategy._load_for_state(
1311
+ state,
1312
+ passive,
1313
+ loadopt=self.loadopt,
1314
+ extra_criteria=self.extra_criteria,
1315
+ )
1316
+
1317
+
1318
+ class _PostLoader(_AbstractRelationshipLoader):
1319
+ """A relationship loader that emits a second SELECT statement."""
1320
+
1321
+ __slots__ = ()
1322
+
1323
+ def _setup_for_recursion(self, context, path, loadopt, join_depth=None):
1324
+ effective_path = (
1325
+ context.compile_state.current_path or orm_util.PathRegistry.root
1326
+ ) + path
1327
+
1328
+ top_level_context = context._get_top_level_context()
1329
+ execution_options = util.immutabledict(
1330
+ {"sa_top_level_orm_context": top_level_context}
1331
+ )
1332
+
1333
+ if loadopt:
1334
+ recursion_depth = loadopt.local_opts.get("recursion_depth", None)
1335
+ unlimited_recursion = recursion_depth == -1
1336
+ else:
1337
+ recursion_depth = None
1338
+ unlimited_recursion = False
1339
+
1340
+ if recursion_depth is not None:
1341
+ if not self.parent_property._is_self_referential:
1342
+ raise sa_exc.InvalidRequestError(
1343
+ f"recursion_depth option on relationship "
1344
+ f"{self.parent_property} not valid for "
1345
+ "non-self-referential relationship"
1346
+ )
1347
+ recursion_depth = context.execution_options.get(
1348
+ f"_recursion_depth_{id(self)}", recursion_depth
1349
+ )
1350
+
1351
+ if not unlimited_recursion and recursion_depth < 0:
1352
+ return (
1353
+ effective_path,
1354
+ False,
1355
+ execution_options,
1356
+ recursion_depth,
1357
+ )
1358
+
1359
+ if not unlimited_recursion:
1360
+ execution_options = execution_options.union(
1361
+ {
1362
+ f"_recursion_depth_{id(self)}": recursion_depth - 1,
1363
+ }
1364
+ )
1365
+
1366
+ if loading._PostLoad.path_exists(
1367
+ context, effective_path, self.parent_property
1368
+ ):
1369
+ return effective_path, False, execution_options, recursion_depth
1370
+
1371
+ path_w_prop = path[self.parent_property]
1372
+ effective_path_w_prop = effective_path[self.parent_property]
1373
+
1374
+ if not path_w_prop.contains(context.attributes, "loader"):
1375
+ if join_depth:
1376
+ if effective_path_w_prop.length / 2 > join_depth:
1377
+ return (
1378
+ effective_path,
1379
+ False,
1380
+ execution_options,
1381
+ recursion_depth,
1382
+ )
1383
+ elif effective_path_w_prop.contains_mapper(self.mapper):
1384
+ return (
1385
+ effective_path,
1386
+ False,
1387
+ execution_options,
1388
+ recursion_depth,
1389
+ )
1390
+
1391
+ return effective_path, True, execution_options, recursion_depth
1392
+
1393
+
1394
+ @relationships.RelationshipProperty.strategy_for(lazy="immediate")
1395
+ class _ImmediateLoader(_PostLoader):
1396
+ __slots__ = ("join_depth",)
1397
+
1398
+ def __init__(self, parent, strategy_key):
1399
+ super().__init__(parent, strategy_key)
1400
+ self.join_depth = self.parent_property.join_depth
1401
+
1402
+ def init_class_attribute(self, mapper):
1403
+ self.parent_property._get_strategy(
1404
+ (("lazy", "select"),)
1405
+ ).init_class_attribute(mapper)
1406
+
1407
+ def create_row_processor(
1408
+ self,
1409
+ context,
1410
+ query_entity,
1411
+ path,
1412
+ loadopt,
1413
+ mapper,
1414
+ result,
1415
+ adapter,
1416
+ populators,
1417
+ ):
1418
+ if not context.compile_state.compile_options._enable_eagerloads:
1419
+ return
1420
+
1421
+ (
1422
+ effective_path,
1423
+ run_loader,
1424
+ execution_options,
1425
+ recursion_depth,
1426
+ ) = self._setup_for_recursion(context, path, loadopt, self.join_depth)
1427
+
1428
+ if not run_loader:
1429
+ # this will not emit SQL and will only emit for a many-to-one
1430
+ # "use get" load. the "_RELATED" part means it may return
1431
+ # instance even if its expired, since this is a mutually-recursive
1432
+ # load operation.
1433
+ flags = attributes.PASSIVE_NO_FETCH_RELATED | PassiveFlag.NO_RAISE
1434
+ else:
1435
+ flags = attributes.PASSIVE_OFF | PassiveFlag.NO_RAISE
1436
+
1437
+ loading._PostLoad.callable_for_path(
1438
+ context,
1439
+ effective_path,
1440
+ self.parent,
1441
+ self.parent_property,
1442
+ self._load_for_path,
1443
+ loadopt,
1444
+ flags,
1445
+ recursion_depth,
1446
+ execution_options,
1447
+ )
1448
+
1449
+ def _load_for_path(
1450
+ self,
1451
+ context,
1452
+ path,
1453
+ states,
1454
+ load_only,
1455
+ loadopt,
1456
+ flags,
1457
+ recursion_depth,
1458
+ execution_options,
1459
+ ):
1460
+ if recursion_depth:
1461
+ new_opt = Load(loadopt.path.entity)
1462
+ new_opt.context = (
1463
+ loadopt,
1464
+ loadopt._recurse(),
1465
+ )
1466
+ alternate_effective_path = path._truncate_recursive()
1467
+ extra_options = (new_opt,)
1468
+ else:
1469
+ alternate_effective_path = path
1470
+ extra_options = ()
1471
+
1472
+ key = self.key
1473
+ lazyloader = self.parent_property._get_strategy((("lazy", "select"),))
1474
+ for state, overwrite in states:
1475
+ dict_ = state.dict
1476
+
1477
+ if overwrite or key not in dict_:
1478
+ value = lazyloader._load_for_state(
1479
+ state,
1480
+ flags,
1481
+ extra_options=extra_options,
1482
+ alternate_effective_path=alternate_effective_path,
1483
+ execution_options=execution_options,
1484
+ )
1485
+ if value not in (
1486
+ ATTR_WAS_SET,
1487
+ LoaderCallableStatus.PASSIVE_NO_RESULT,
1488
+ ):
1489
+ state.get_impl(key).set_committed_value(
1490
+ state, dict_, value
1491
+ )
1492
+
1493
+
1494
+ @log.class_logger
1495
+ @relationships.RelationshipProperty.strategy_for(lazy="subquery")
1496
+ class _SubqueryLoader(_PostLoader):
1497
+ __slots__ = ("join_depth",)
1498
+
1499
+ def __init__(self, parent, strategy_key):
1500
+ super().__init__(parent, strategy_key)
1501
+ self.join_depth = self.parent_property.join_depth
1502
+
1503
+ def init_class_attribute(self, mapper):
1504
+ self.parent_property._get_strategy(
1505
+ (("lazy", "select"),)
1506
+ ).init_class_attribute(mapper)
1507
+
1508
+ def _get_leftmost(
1509
+ self,
1510
+ orig_query_entity_index,
1511
+ subq_path,
1512
+ current_compile_state,
1513
+ is_root,
1514
+ ):
1515
+ given_subq_path = subq_path
1516
+ subq_path = subq_path.path
1517
+ subq_mapper = orm_util._class_to_mapper(subq_path[0])
1518
+
1519
+ # determine attributes of the leftmost mapper
1520
+ if (
1521
+ self.parent.isa(subq_mapper)
1522
+ and self.parent_property is subq_path[1]
1523
+ ):
1524
+ leftmost_mapper, leftmost_prop = self.parent, self.parent_property
1525
+ else:
1526
+ leftmost_mapper, leftmost_prop = subq_mapper, subq_path[1]
1527
+
1528
+ if is_root:
1529
+ # the subq_path is also coming from cached state, so when we start
1530
+ # building up this path, it has to also be converted to be in terms
1531
+ # of the current state. this is for the specific case of the entity
1532
+ # is an AliasedClass against a subquery that's not otherwise going
1533
+ # to adapt
1534
+ new_subq_path = current_compile_state._entities[
1535
+ orig_query_entity_index
1536
+ ].entity_zero._path_registry[leftmost_prop]
1537
+ additional = len(subq_path) - len(new_subq_path)
1538
+ if additional:
1539
+ new_subq_path += path_registry.PathRegistry.coerce(
1540
+ subq_path[-additional:]
1541
+ )
1542
+ else:
1543
+ new_subq_path = given_subq_path
1544
+
1545
+ leftmost_cols = leftmost_prop.local_columns
1546
+
1547
+ leftmost_attr = [
1548
+ getattr(
1549
+ new_subq_path.path[0].entity,
1550
+ leftmost_mapper._columntoproperty[c].key,
1551
+ )
1552
+ for c in leftmost_cols
1553
+ ]
1554
+
1555
+ return leftmost_mapper, leftmost_attr, leftmost_prop, new_subq_path
1556
+
1557
+ def _generate_from_original_query(
1558
+ self,
1559
+ orig_compile_state,
1560
+ orig_query,
1561
+ leftmost_mapper,
1562
+ leftmost_attr,
1563
+ leftmost_relationship,
1564
+ orig_entity,
1565
+ ):
1566
+ # reformat the original query
1567
+ # to look only for significant columns
1568
+ q = orig_query._clone().correlate(None)
1569
+
1570
+ # LEGACY: make a Query back from the select() !!
1571
+ # This suits at least two legacy cases:
1572
+ # 1. applications which expect before_compile() to be called
1573
+ # below when we run .subquery() on this query (Keystone)
1574
+ # 2. applications which are doing subqueryload with complex
1575
+ # from_self() queries, as query.subquery() / .statement
1576
+ # has to do the full compile context for multiply-nested
1577
+ # from_self() (Neutron) - see test_subqload_from_self
1578
+ # for demo.
1579
+ q2 = query.Query.__new__(query.Query)
1580
+ q2.__dict__.update(q.__dict__)
1581
+ q = q2
1582
+
1583
+ # set the query's "FROM" list explicitly to what the
1584
+ # FROM list would be in any case, as we will be limiting
1585
+ # the columns in the SELECT list which may no longer include
1586
+ # all entities mentioned in things like WHERE, JOIN, etc.
1587
+ if not q._from_obj:
1588
+ q._enable_assertions = False
1589
+ q.select_from.non_generative(
1590
+ q,
1591
+ *{
1592
+ ent["entity"]
1593
+ for ent in _column_descriptions(
1594
+ orig_query, compile_state=orig_compile_state
1595
+ )
1596
+ if ent["entity"] is not None
1597
+ },
1598
+ )
1599
+
1600
+ # select from the identity columns of the outer (specifically, these
1601
+ # are the 'local_cols' of the property). This will remove other
1602
+ # columns from the query that might suggest the right entity which is
1603
+ # why we do set select_from above. The attributes we have are
1604
+ # coerced and adapted using the original query's adapter, which is
1605
+ # needed only for the case of adapting a subclass column to
1606
+ # that of a polymorphic selectable, e.g. we have
1607
+ # Engineer.primary_language and the entity is Person. All other
1608
+ # adaptations, e.g. from_self, select_entity_from(), will occur
1609
+ # within the new query when it compiles, as the compile_state we are
1610
+ # using here is only a partial one. If the subqueryload is from a
1611
+ # with_polymorphic() or other aliased() object, left_attr will already
1612
+ # be the correct attributes so no adaptation is needed.
1613
+ target_cols = orig_compile_state._adapt_col_list(
1614
+ [
1615
+ sql.coercions.expect(sql.roles.ColumnsClauseRole, o)
1616
+ for o in leftmost_attr
1617
+ ],
1618
+ orig_compile_state._get_current_adapter(),
1619
+ )
1620
+ q._raw_columns = target_cols
1621
+
1622
+ distinct_target_key = leftmost_relationship.distinct_target_key
1623
+
1624
+ if distinct_target_key is True:
1625
+ q._distinct = True
1626
+ elif distinct_target_key is None:
1627
+ # if target_cols refer to a non-primary key or only
1628
+ # part of a composite primary key, set the q as distinct
1629
+ for t in {c.table for c in target_cols}:
1630
+ if not set(target_cols).issuperset(t.primary_key):
1631
+ q._distinct = True
1632
+ break
1633
+
1634
+ # don't need ORDER BY if no limit/offset
1635
+ if not q._has_row_limiting_clause:
1636
+ q._order_by_clauses = ()
1637
+
1638
+ if q._distinct is True and q._order_by_clauses:
1639
+ # the logic to automatically add the order by columns to the query
1640
+ # when distinct is True is deprecated in the query
1641
+ to_add = sql_util.expand_column_list_from_order_by(
1642
+ target_cols, q._order_by_clauses
1643
+ )
1644
+ if to_add:
1645
+ q._set_entities(target_cols + to_add)
1646
+
1647
+ # the original query now becomes a subquery
1648
+ # which we'll join onto.
1649
+ # LEGACY: as "q" is a Query, the before_compile() event is invoked
1650
+ # here.
1651
+ embed_q = q.set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL).subquery()
1652
+ left_alias = orm_util.AliasedClass(
1653
+ leftmost_mapper, embed_q, use_mapper_path=True
1654
+ )
1655
+ return left_alias
1656
+
1657
+ def _prep_for_joins(self, left_alias, subq_path):
1658
+ # figure out what's being joined. a.k.a. the fun part
1659
+ to_join = []
1660
+ pairs = list(subq_path.pairs())
1661
+
1662
+ for i, (mapper, prop) in enumerate(pairs):
1663
+ if i > 0:
1664
+ # look at the previous mapper in the chain -
1665
+ # if it is as or more specific than this prop's
1666
+ # mapper, use that instead.
1667
+ # note we have an assumption here that
1668
+ # the non-first element is always going to be a mapper,
1669
+ # not an AliasedClass
1670
+
1671
+ prev_mapper = pairs[i - 1][1].mapper
1672
+ to_append = prev_mapper if prev_mapper.isa(mapper) else mapper
1673
+ else:
1674
+ to_append = mapper
1675
+
1676
+ to_join.append((to_append, prop.key))
1677
+
1678
+ # determine the immediate parent class we are joining from,
1679
+ # which needs to be aliased.
1680
+
1681
+ if len(to_join) < 2:
1682
+ # in the case of a one level eager load, this is the
1683
+ # leftmost "left_alias".
1684
+ parent_alias = left_alias
1685
+ else:
1686
+ info = inspect(to_join[-1][0])
1687
+ if info.is_aliased_class:
1688
+ parent_alias = info.entity
1689
+ else:
1690
+ # alias a plain mapper as we may be
1691
+ # joining multiple times
1692
+ parent_alias = orm_util.AliasedClass(
1693
+ info.entity, use_mapper_path=True
1694
+ )
1695
+
1696
+ local_cols = self.parent_property.local_columns
1697
+
1698
+ local_attr = [
1699
+ getattr(parent_alias, self.parent._columntoproperty[c].key)
1700
+ for c in local_cols
1701
+ ]
1702
+ return to_join, local_attr, parent_alias
1703
+
1704
+ def _apply_joins(
1705
+ self, q, to_join, left_alias, parent_alias, effective_entity
1706
+ ):
1707
+ ltj = len(to_join)
1708
+ if ltj == 1:
1709
+ to_join = [
1710
+ getattr(left_alias, to_join[0][1]).of_type(effective_entity)
1711
+ ]
1712
+ elif ltj == 2:
1713
+ to_join = [
1714
+ getattr(left_alias, to_join[0][1]).of_type(parent_alias),
1715
+ getattr(parent_alias, to_join[-1][1]).of_type(
1716
+ effective_entity
1717
+ ),
1718
+ ]
1719
+ elif ltj > 2:
1720
+ middle = [
1721
+ (
1722
+ (
1723
+ orm_util.AliasedClass(item[0])
1724
+ if not inspect(item[0]).is_aliased_class
1725
+ else item[0].entity
1726
+ ),
1727
+ item[1],
1728
+ )
1729
+ for item in to_join[1:-1]
1730
+ ]
1731
+ inner = []
1732
+
1733
+ while middle:
1734
+ item = middle.pop(0)
1735
+ attr = getattr(item[0], item[1])
1736
+ if middle:
1737
+ attr = attr.of_type(middle[0][0])
1738
+ else:
1739
+ attr = attr.of_type(parent_alias)
1740
+
1741
+ inner.append(attr)
1742
+
1743
+ to_join = (
1744
+ [getattr(left_alias, to_join[0][1]).of_type(inner[0].parent)]
1745
+ + inner
1746
+ + [
1747
+ getattr(parent_alias, to_join[-1][1]).of_type(
1748
+ effective_entity
1749
+ )
1750
+ ]
1751
+ )
1752
+
1753
+ for attr in to_join:
1754
+ q = q.join(attr)
1755
+
1756
+ return q
1757
+
1758
+ def _setup_options(
1759
+ self,
1760
+ context,
1761
+ q,
1762
+ subq_path,
1763
+ rewritten_path,
1764
+ orig_query,
1765
+ effective_entity,
1766
+ loadopt,
1767
+ ):
1768
+ # note that because the subqueryload object
1769
+ # does not reuse the cached query, instead always making
1770
+ # use of the current invoked query, while we have two queries
1771
+ # here (orig and context.query), they are both non-cached
1772
+ # queries and we can transfer the options as is without
1773
+ # adjusting for new criteria. Some work on #6881 / #6889
1774
+ # brought this into question.
1775
+ new_options = orig_query._with_options
1776
+
1777
+ if loadopt and loadopt._extra_criteria:
1778
+ new_options += (
1779
+ orm_util.LoaderCriteriaOption(
1780
+ effective_entity,
1781
+ loadopt._generate_extra_criteria(context),
1782
+ ),
1783
+ )
1784
+
1785
+ # propagate loader options etc. to the new query.
1786
+ # these will fire relative to subq_path.
1787
+ q = q._with_current_path(rewritten_path)
1788
+ q = q.options(*new_options)
1789
+
1790
+ return q
1791
+
1792
+ def _setup_outermost_orderby(self, q):
1793
+ if self.parent_property.order_by:
1794
+
1795
+ def _setup_outermost_orderby(compile_context):
1796
+ compile_context.eager_order_by += tuple(
1797
+ util.to_list(self.parent_property.order_by)
1798
+ )
1799
+
1800
+ q = q._add_compile_state_func(
1801
+ _setup_outermost_orderby, self.parent_property
1802
+ )
1803
+
1804
+ return q
1805
+
1806
+ class _SubqCollections:
1807
+ """Given a :class:`_query.Query` used to emit the "subquery load",
1808
+ provide a load interface that executes the query at the
1809
+ first moment a value is needed.
1810
+
1811
+ """
1812
+
1813
+ __slots__ = (
1814
+ "session",
1815
+ "execution_options",
1816
+ "load_options",
1817
+ "params",
1818
+ "subq",
1819
+ "_data",
1820
+ )
1821
+
1822
+ def __init__(self, context, subq):
1823
+ # avoid creating a cycle by storing context
1824
+ # even though that's preferable
1825
+ self.session = context.session
1826
+ self.execution_options = context.execution_options
1827
+ self.load_options = context.load_options
1828
+ self.params = context.params or {}
1829
+ self.subq = subq
1830
+ self._data = None
1831
+
1832
+ def get(self, key, default):
1833
+ if self._data is None:
1834
+ self._load()
1835
+ return self._data.get(key, default)
1836
+
1837
+ def _load(self):
1838
+ self._data = collections.defaultdict(list)
1839
+
1840
+ q = self.subq
1841
+ assert q.session is None
1842
+
1843
+ q = q.with_session(self.session)
1844
+
1845
+ if self.load_options._populate_existing:
1846
+ q = q.populate_existing()
1847
+ # to work with baked query, the parameters may have been
1848
+ # updated since this query was created, so take these into account
1849
+
1850
+ data = self._data
1851
+ for row in q.params(self.params):
1852
+ # group plain tuples rather than Row slices, which would
1853
+ # incur Row construction and Row.__eq__ per row
1854
+ tup = row._to_tuple_instance()
1855
+ data[tup[1:]].append(tup[0])
1856
+
1857
+ def loader(self, state, dict_, row):
1858
+ if self._data is None:
1859
+ self._load()
1860
+
1861
+ def _setup_query_from_rowproc(
1862
+ self,
1863
+ context,
1864
+ query_entity,
1865
+ path,
1866
+ entity,
1867
+ loadopt,
1868
+ adapter,
1869
+ ):
1870
+ compile_state = context.compile_state
1871
+ if (
1872
+ not compile_state.compile_options._enable_eagerloads
1873
+ or compile_state.compile_options._for_refresh_state
1874
+ ):
1875
+ return
1876
+
1877
+ orig_query_entity_index = compile_state._entities.index(query_entity)
1878
+ context.loaders_require_buffering = True
1879
+
1880
+ path = path[self.parent_property]
1881
+
1882
+ # build up a path indicating the path from the leftmost
1883
+ # entity to the thing we're subquery loading.
1884
+ with_poly_entity = path.get(
1885
+ compile_state.attributes, "path_with_polymorphic", None
1886
+ )
1887
+ if with_poly_entity is not None:
1888
+ effective_entity = with_poly_entity
1889
+ else:
1890
+ effective_entity = self.entity
1891
+
1892
+ subq_path, rewritten_path = context.query._execution_options.get(
1893
+ ("subquery_paths", None),
1894
+ (orm_util.PathRegistry.root, orm_util.PathRegistry.root),
1895
+ )
1896
+ is_root = subq_path is orm_util.PathRegistry.root
1897
+ subq_path = subq_path + path
1898
+ rewritten_path = rewritten_path + path
1899
+
1900
+ # use the current query being invoked, not the compile state
1901
+ # one. this is so that we get the current parameters. however,
1902
+ # it means we can't use the existing compile state, we have to make
1903
+ # a new one. other approaches include possibly using the
1904
+ # compiled query but swapping the params, seems only marginally
1905
+ # less time spent but more complicated
1906
+ orig_query = context.query._execution_options.get(
1907
+ ("orig_query", _SubqueryLoader), context.query
1908
+ )
1909
+
1910
+ # make a new compile_state for the query that's probably cached, but
1911
+ # we're sort of undoing a bit of that caching :(
1912
+ compile_state_cls = _ORMCompileState._get_plugin_class_for_plugin(
1913
+ orig_query, "orm"
1914
+ )
1915
+
1916
+ if orig_query._is_lambda_element:
1917
+ if context.load_options._lazy_loaded_from is None:
1918
+ util.warn(
1919
+ 'subqueryloader for "%s" must invoke lambda callable '
1920
+ "at %r in "
1921
+ "order to produce a new query, decreasing the efficiency "
1922
+ "of caching for this statement. Consider using "
1923
+ "selectinload() for more effective full-lambda caching"
1924
+ % (self, orig_query)
1925
+ )
1926
+ orig_query = orig_query._resolved
1927
+
1928
+ # this is the more "quick" version, however it's not clear how
1929
+ # much of this we need. in particular I can't get a test to
1930
+ # fail if the "set_base_alias" is missing and not sure why that is.
1931
+ orig_compile_state = compile_state_cls._create_entities_collection(
1932
+ orig_query, legacy=False
1933
+ )
1934
+
1935
+ (
1936
+ leftmost_mapper,
1937
+ leftmost_attr,
1938
+ leftmost_relationship,
1939
+ rewritten_path,
1940
+ ) = self._get_leftmost(
1941
+ orig_query_entity_index,
1942
+ rewritten_path,
1943
+ orig_compile_state,
1944
+ is_root,
1945
+ )
1946
+
1947
+ # generate a new Query from the original, then
1948
+ # produce a subquery from it.
1949
+ left_alias = self._generate_from_original_query(
1950
+ orig_compile_state,
1951
+ orig_query,
1952
+ leftmost_mapper,
1953
+ leftmost_attr,
1954
+ leftmost_relationship,
1955
+ entity,
1956
+ )
1957
+
1958
+ # generate another Query that will join the
1959
+ # left alias to the target relationships.
1960
+ # basically doing a longhand
1961
+ # "from_self()". (from_self() itself not quite industrial
1962
+ # strength enough for all contingencies...but very close)
1963
+
1964
+ q = query.Query(effective_entity)
1965
+
1966
+ q._execution_options = context.query._execution_options.merge_with(
1967
+ context.execution_options,
1968
+ {
1969
+ ("orig_query", _SubqueryLoader): orig_query,
1970
+ ("subquery_paths", None): (subq_path, rewritten_path),
1971
+ },
1972
+ )
1973
+
1974
+ q = q._set_enable_single_crit(False)
1975
+ to_join, local_attr, parent_alias = self._prep_for_joins(
1976
+ left_alias, subq_path
1977
+ )
1978
+
1979
+ q = q.add_columns(*local_attr)
1980
+ q = self._apply_joins(
1981
+ q, to_join, left_alias, parent_alias, effective_entity
1982
+ )
1983
+
1984
+ q = self._setup_options(
1985
+ context,
1986
+ q,
1987
+ subq_path,
1988
+ rewritten_path,
1989
+ orig_query,
1990
+ effective_entity,
1991
+ loadopt,
1992
+ )
1993
+ q = self._setup_outermost_orderby(q)
1994
+
1995
+ return q
1996
+
1997
+ def create_row_processor(
1998
+ self,
1999
+ context,
2000
+ query_entity,
2001
+ path,
2002
+ loadopt,
2003
+ mapper,
2004
+ result,
2005
+ adapter,
2006
+ populators,
2007
+ ):
2008
+ if (
2009
+ loadopt
2010
+ and context.compile_state.statement is not None
2011
+ and context.compile_state.statement.is_dml
2012
+ ):
2013
+ util.warn_deprecated(
2014
+ "The subqueryload loader option is not compatible with DML "
2015
+ "statements such as INSERT, UPDATE. Only SELECT may be used."
2016
+ "This warning will become an exception in a future release.",
2017
+ "2.0",
2018
+ )
2019
+
2020
+ if context.refresh_state:
2021
+ return self._immediateload_create_row_processor(
2022
+ context,
2023
+ query_entity,
2024
+ path,
2025
+ loadopt,
2026
+ mapper,
2027
+ result,
2028
+ adapter,
2029
+ populators,
2030
+ )
2031
+
2032
+ _, run_loader, _, _ = self._setup_for_recursion(
2033
+ context, path, loadopt, self.join_depth
2034
+ )
2035
+ if not run_loader:
2036
+ return
2037
+
2038
+ if not isinstance(context.compile_state, _ORMSelectCompileState):
2039
+ # issue 7505 - subqueryload() in 1.3 and previous would silently
2040
+ # degrade for from_statement() without warning. this behavior
2041
+ # is restored here
2042
+ return
2043
+
2044
+ if not self.parent.class_manager[self.key].impl.supports_population:
2045
+ raise sa_exc.InvalidRequestError(
2046
+ "'%s' does not support object "
2047
+ "population - eager loading cannot be applied." % self
2048
+ )
2049
+
2050
+ # a little dance here as the "path" is still something that only
2051
+ # semi-tracks the exact series of things we are loading, still not
2052
+ # telling us about with_polymorphic() and stuff like that when it's at
2053
+ # the root.. the initial MapperEntity is more accurate for this case.
2054
+ if len(path) == 1:
2055
+ if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
2056
+ return
2057
+ elif not orm_util._entity_isa(
2058
+ path[-1], self.parent
2059
+ ) and not self.parent.isa(path[-1].mapper):
2060
+ # second check accommodates a polymorphic entity where
2061
+ # the path has been normalized to the base mapper but
2062
+ # self.parent is a subclass mapper. Fixes #13209.
2063
+ return
2064
+
2065
+ subq = self._setup_query_from_rowproc(
2066
+ context,
2067
+ query_entity,
2068
+ path,
2069
+ path[-1],
2070
+ loadopt,
2071
+ adapter,
2072
+ )
2073
+
2074
+ if subq is None:
2075
+ return
2076
+
2077
+ assert subq.session is None
2078
+
2079
+ path = path[self.parent_property]
2080
+
2081
+ local_cols = self.parent_property.local_columns
2082
+
2083
+ # cache the loaded collections in the context
2084
+ # so that inheriting mappers don't re-load when they
2085
+ # call upon create_row_processor again
2086
+ collections = path.get(context.attributes, "collections")
2087
+ if collections is None:
2088
+ collections = self._SubqCollections(context, subq)
2089
+ path.set(context.attributes, "collections", collections)
2090
+
2091
+ if adapter:
2092
+ local_cols = [adapter.columns[c] for c in local_cols]
2093
+
2094
+ if self.uselist:
2095
+ self._create_collection_loader(
2096
+ context, result, collections, local_cols, populators
2097
+ )
2098
+ else:
2099
+ self._create_scalar_loader(
2100
+ context, result, collections, local_cols, populators
2101
+ )
2102
+
2103
+ def _create_collection_loader(
2104
+ self, context, result, collections, local_cols, populators
2105
+ ):
2106
+ tuple_getter = result._tuple_getter(local_cols)
2107
+
2108
+ def load_collection_from_subq(state, dict_, row):
2109
+ collection = collections.get(tuple_getter(row), ())
2110
+ state.get_impl(self.key).set_committed_value(
2111
+ state, dict_, collection
2112
+ )
2113
+
2114
+ def load_collection_from_subq_existing_row(state, dict_, row):
2115
+ if self.key not in dict_:
2116
+ load_collection_from_subq(state, dict_, row)
2117
+
2118
+ populators["new"].append((self.key, load_collection_from_subq))
2119
+ populators["existing"].append(
2120
+ (self.key, load_collection_from_subq_existing_row)
2121
+ )
2122
+
2123
+ if context.invoke_all_eagers:
2124
+ populators["eager"].append((self.key, collections.loader))
2125
+
2126
+ def _create_scalar_loader(
2127
+ self, context, result, collections, local_cols, populators
2128
+ ):
2129
+ tuple_getter = result._tuple_getter(local_cols)
2130
+
2131
+ def load_scalar_from_subq(state, dict_, row):
2132
+ collection = collections.get(tuple_getter(row), (None,))
2133
+ if len(collection) > 1:
2134
+ util.warn(
2135
+ "Multiple rows returned with "
2136
+ "uselist=False for eagerly-loaded attribute '%s' " % self
2137
+ )
2138
+
2139
+ scalar = collection[0]
2140
+ state.get_impl(self.key).set_committed_value(state, dict_, scalar)
2141
+
2142
+ def load_scalar_from_subq_existing_row(state, dict_, row):
2143
+ if self.key not in dict_:
2144
+ load_scalar_from_subq(state, dict_, row)
2145
+
2146
+ populators["new"].append((self.key, load_scalar_from_subq))
2147
+ populators["existing"].append(
2148
+ (self.key, load_scalar_from_subq_existing_row)
2149
+ )
2150
+ if context.invoke_all_eagers:
2151
+ populators["eager"].append((self.key, collections.loader))
2152
+
2153
+
2154
+ @log.class_logger
2155
+ @relationships.RelationshipProperty.strategy_for(lazy="joined")
2156
+ @relationships.RelationshipProperty.strategy_for(lazy=False)
2157
+ class _JoinedLoader(_AbstractRelationshipLoader):
2158
+ """Provide loading behavior for a :class:`.Relationship`
2159
+ using joined eager loading.
2160
+
2161
+ """
2162
+
2163
+ __slots__ = "join_depth"
2164
+
2165
+ def __init__(self, parent, strategy_key):
2166
+ super().__init__(parent, strategy_key)
2167
+ self.join_depth = self.parent_property.join_depth
2168
+
2169
+ def init_class_attribute(self, mapper):
2170
+ self.parent_property._get_strategy(
2171
+ (("lazy", "select"),)
2172
+ ).init_class_attribute(mapper)
2173
+
2174
+ def setup_query(
2175
+ self,
2176
+ compile_state,
2177
+ query_entity,
2178
+ path,
2179
+ loadopt,
2180
+ adapter,
2181
+ column_collection=None,
2182
+ parentmapper=None,
2183
+ chained_from_outerjoin=False,
2184
+ **kwargs,
2185
+ ):
2186
+ """Add a left outer join to the statement that's being constructed."""
2187
+
2188
+ if not compile_state.compile_options._enable_eagerloads:
2189
+ return
2190
+ elif (
2191
+ loadopt
2192
+ and compile_state.statement is not None
2193
+ and compile_state.statement.is_dml
2194
+ ):
2195
+ util.warn_deprecated(
2196
+ "The joinedload loader option is not compatible with DML "
2197
+ "statements such as INSERT, UPDATE. Only SELECT may be used."
2198
+ "This warning will become an exception in a future release.",
2199
+ "2.0",
2200
+ )
2201
+ elif self.uselist:
2202
+ compile_state.multi_row_eager_loaders = True
2203
+
2204
+ path = path[self.parent_property]
2205
+
2206
+ user_defined_adapter = (
2207
+ self._init_user_defined_eager_proc(
2208
+ loadopt, compile_state, compile_state.attributes
2209
+ )
2210
+ if loadopt
2211
+ else False
2212
+ )
2213
+
2214
+ if user_defined_adapter is not False:
2215
+ # setup an adapter but dont create any JOIN, assume it's already
2216
+ # in the query
2217
+ (
2218
+ clauses,
2219
+ adapter,
2220
+ add_to_collection,
2221
+ ) = self._setup_query_on_user_defined_adapter(
2222
+ compile_state,
2223
+ query_entity,
2224
+ path,
2225
+ adapter,
2226
+ user_defined_adapter,
2227
+ )
2228
+
2229
+ # don't do "wrap" for multi-row, we want to wrap
2230
+ # limited/distinct SELECT,
2231
+ # because we want to put the JOIN on the outside.
2232
+
2233
+ else:
2234
+ # if not via query option, check for
2235
+ # a cycle
2236
+ if not path.contains(compile_state.attributes, "loader"):
2237
+ if self.join_depth:
2238
+ if path.length / 2 > self.join_depth:
2239
+ return
2240
+ elif path.contains_mapper(self.mapper):
2241
+ return
2242
+
2243
+ # add the JOIN and create an adapter
2244
+ (
2245
+ clauses,
2246
+ adapter,
2247
+ add_to_collection,
2248
+ chained_from_outerjoin,
2249
+ ) = self._generate_row_adapter(
2250
+ compile_state,
2251
+ query_entity,
2252
+ path,
2253
+ loadopt,
2254
+ adapter,
2255
+ column_collection,
2256
+ parentmapper,
2257
+ chained_from_outerjoin,
2258
+ )
2259
+
2260
+ # for multi-row, we want to wrap limited/distinct SELECT,
2261
+ # because we want to put the JOIN on the outside.
2262
+ compile_state.eager_adding_joins = True
2263
+
2264
+ with_poly_entity = path.get(
2265
+ compile_state.attributes, "path_with_polymorphic", None
2266
+ )
2267
+ if with_poly_entity is not None:
2268
+ with_polymorphic = inspect(
2269
+ with_poly_entity
2270
+ ).with_polymorphic_mappers
2271
+ else:
2272
+ with_polymorphic = None
2273
+
2274
+ path = path[self.entity]
2275
+
2276
+ loading._setup_entity_query(
2277
+ compile_state,
2278
+ self.mapper,
2279
+ query_entity,
2280
+ path,
2281
+ clauses,
2282
+ add_to_collection,
2283
+ with_polymorphic=with_polymorphic,
2284
+ parentmapper=self.mapper,
2285
+ chained_from_outerjoin=chained_from_outerjoin,
2286
+ )
2287
+
2288
+ has_nones = util.NONE_SET.intersection(compile_state.secondary_columns)
2289
+
2290
+ if has_nones:
2291
+ if with_poly_entity is not None:
2292
+ raise sa_exc.InvalidRequestError(
2293
+ "Detected unaliased columns when generating joined "
2294
+ "load. Make sure to use aliased=True or flat=True "
2295
+ "when using joined loading with with_polymorphic()."
2296
+ )
2297
+ else:
2298
+ compile_state.secondary_columns = [
2299
+ c for c in compile_state.secondary_columns if c is not None
2300
+ ]
2301
+
2302
+ def _init_user_defined_eager_proc(
2303
+ self, loadopt, compile_state, target_attributes
2304
+ ):
2305
+ # check if the opt applies at all
2306
+ if "eager_from_alias" not in loadopt.local_opts:
2307
+ # nope
2308
+ return False
2309
+
2310
+ path = loadopt.path.parent
2311
+
2312
+ # the option applies. check if the "user_defined_eager_row_processor"
2313
+ # has been built up.
2314
+ adapter = path.get(
2315
+ compile_state.attributes, "user_defined_eager_row_processor", False
2316
+ )
2317
+ if adapter is not False:
2318
+ # just return it
2319
+ return adapter
2320
+
2321
+ # otherwise figure it out.
2322
+ alias = loadopt.local_opts["eager_from_alias"]
2323
+ root_mapper, prop = path[-2:]
2324
+
2325
+ if alias is not None:
2326
+ if isinstance(alias, str):
2327
+ alias = prop.target.alias(alias)
2328
+ adapter = orm_util.ORMAdapter(
2329
+ orm_util._TraceAdaptRole.JOINEDLOAD_USER_DEFINED_ALIAS,
2330
+ prop.mapper,
2331
+ selectable=alias,
2332
+ equivalents=prop.mapper._equivalent_columns,
2333
+ limit_on_entity=False,
2334
+ )
2335
+ else:
2336
+ if path.contains(
2337
+ compile_state.attributes, "path_with_polymorphic"
2338
+ ):
2339
+ with_poly_entity = path.get(
2340
+ compile_state.attributes, "path_with_polymorphic"
2341
+ )
2342
+ adapter = orm_util.ORMAdapter(
2343
+ orm_util._TraceAdaptRole.JOINEDLOAD_PATH_WITH_POLYMORPHIC,
2344
+ with_poly_entity,
2345
+ equivalents=prop.mapper._equivalent_columns,
2346
+ )
2347
+ else:
2348
+ adapter = compile_state._polymorphic_adapters.get(
2349
+ prop.mapper, None
2350
+ )
2351
+ path.set(
2352
+ target_attributes,
2353
+ "user_defined_eager_row_processor",
2354
+ adapter,
2355
+ )
2356
+
2357
+ return adapter
2358
+
2359
+ def _setup_query_on_user_defined_adapter(
2360
+ self, context, entity, path, adapter, user_defined_adapter
2361
+ ):
2362
+ # apply some more wrapping to the "user defined adapter"
2363
+ # if we are setting up the query for SQL render.
2364
+ adapter = entity._get_entity_clauses(context)
2365
+
2366
+ if adapter and user_defined_adapter:
2367
+ user_defined_adapter = user_defined_adapter.wrap(adapter)
2368
+ path.set(
2369
+ context.attributes,
2370
+ "user_defined_eager_row_processor",
2371
+ user_defined_adapter,
2372
+ )
2373
+ elif adapter:
2374
+ user_defined_adapter = adapter
2375
+ path.set(
2376
+ context.attributes,
2377
+ "user_defined_eager_row_processor",
2378
+ user_defined_adapter,
2379
+ )
2380
+
2381
+ add_to_collection = context.primary_columns
2382
+ return user_defined_adapter, adapter, add_to_collection
2383
+
2384
+ def _generate_row_adapter(
2385
+ self,
2386
+ compile_state,
2387
+ entity,
2388
+ path,
2389
+ loadopt,
2390
+ adapter,
2391
+ column_collection,
2392
+ parentmapper,
2393
+ chained_from_outerjoin,
2394
+ ):
2395
+ with_poly_entity = path.get(
2396
+ compile_state.attributes, "path_with_polymorphic", None
2397
+ )
2398
+ if with_poly_entity:
2399
+ to_adapt = with_poly_entity
2400
+ else:
2401
+ insp = inspect(self.entity)
2402
+ if insp.is_aliased_class:
2403
+ alt_selectable = insp.selectable
2404
+ else:
2405
+ alt_selectable = None
2406
+
2407
+ to_adapt = orm_util.AliasedClass(
2408
+ self.mapper,
2409
+ alias=(
2410
+ alt_selectable._anonymous_fromclause(flat=True)
2411
+ if alt_selectable is not None
2412
+ else None
2413
+ ),
2414
+ flat=True,
2415
+ use_mapper_path=True,
2416
+ )
2417
+
2418
+ to_adapt_insp = inspect(to_adapt)
2419
+
2420
+ clauses = to_adapt_insp._memo(
2421
+ ("joinedloader_ormadapter", self),
2422
+ orm_util.ORMAdapter,
2423
+ orm_util._TraceAdaptRole.JOINEDLOAD_MEMOIZED_ADAPTER,
2424
+ to_adapt_insp,
2425
+ equivalents=self.mapper._equivalent_columns,
2426
+ adapt_required=True,
2427
+ allow_label_resolve=False,
2428
+ anonymize_labels=True,
2429
+ )
2430
+
2431
+ assert clauses.is_aliased_class
2432
+
2433
+ innerjoin = (
2434
+ loadopt.local_opts.get("innerjoin", self.parent_property.innerjoin)
2435
+ if loadopt is not None
2436
+ else self.parent_property.innerjoin
2437
+ )
2438
+
2439
+ if not innerjoin:
2440
+ # if this is an outer join, all non-nested eager joins from
2441
+ # this path must also be outer joins
2442
+ chained_from_outerjoin = True
2443
+
2444
+ compile_state.create_eager_joins.append(
2445
+ (
2446
+ self._create_eager_join,
2447
+ entity,
2448
+ path,
2449
+ adapter,
2450
+ parentmapper,
2451
+ clauses,
2452
+ innerjoin,
2453
+ chained_from_outerjoin,
2454
+ loadopt._extra_criteria if loadopt else (),
2455
+ )
2456
+ )
2457
+
2458
+ add_to_collection = compile_state.secondary_columns
2459
+ path.set(compile_state.attributes, "eager_row_processor", clauses)
2460
+
2461
+ return clauses, adapter, add_to_collection, chained_from_outerjoin
2462
+
2463
+ def _create_eager_join(
2464
+ self,
2465
+ compile_state,
2466
+ query_entity,
2467
+ path,
2468
+ adapter,
2469
+ parentmapper,
2470
+ clauses,
2471
+ innerjoin,
2472
+ chained_from_outerjoin,
2473
+ extra_criteria,
2474
+ ):
2475
+ if parentmapper is None:
2476
+ localparent = query_entity.mapper
2477
+ else:
2478
+ localparent = parentmapper
2479
+
2480
+ # whether or not the Query will wrap the selectable in a subquery,
2481
+ # and then attach eager load joins to that (i.e., in the case of
2482
+ # LIMIT/OFFSET etc.)
2483
+ should_nest_selectable = compile_state._should_nest_selectable
2484
+
2485
+ query_entity_key = None
2486
+
2487
+ if (
2488
+ query_entity not in compile_state.eager_joins
2489
+ and not should_nest_selectable
2490
+ and compile_state.from_clauses
2491
+ ):
2492
+ indexes = sql_util.find_left_clause_that_matches_given(
2493
+ compile_state.from_clauses, query_entity.selectable
2494
+ )
2495
+
2496
+ if len(indexes) > 1:
2497
+ # for the eager load case, I can't reproduce this right
2498
+ # now. For query.join() I can.
2499
+ raise sa_exc.InvalidRequestError(
2500
+ "Can't identify which query entity in which to joined "
2501
+ "eager load from. Please use an exact match when "
2502
+ "specifying the join path."
2503
+ )
2504
+
2505
+ if indexes:
2506
+ clause = compile_state.from_clauses[indexes[0]]
2507
+ # join to an existing FROM clause on the query.
2508
+ # key it to its list index in the eager_joins dict.
2509
+ # Query._compile_context will adapt as needed and
2510
+ # append to the FROM clause of the select().
2511
+ query_entity_key, default_towrap = indexes[0], clause
2512
+
2513
+ if query_entity_key is None:
2514
+ query_entity_key, default_towrap = (
2515
+ query_entity,
2516
+ query_entity.selectable,
2517
+ )
2518
+
2519
+ towrap = compile_state.eager_joins.setdefault(
2520
+ query_entity_key, default_towrap
2521
+ )
2522
+
2523
+ if adapter:
2524
+ if getattr(adapter, "is_aliased_class", False):
2525
+ # joining from an adapted entity. The adapted entity
2526
+ # might be a "with_polymorphic", so resolve that to our
2527
+ # specific mapper's entity before looking for our attribute
2528
+ # name on it.
2529
+ efm = adapter.aliased_insp._entity_for_mapper(
2530
+ localparent
2531
+ if localparent.isa(self.parent)
2532
+ else self.parent
2533
+ )
2534
+
2535
+ # look for our attribute on the adapted entity, else fall back
2536
+ # to our straight property
2537
+ onclause = getattr(efm.entity, self.key, self.parent_property)
2538
+ else:
2539
+ onclause = getattr(
2540
+ orm_util.AliasedClass(
2541
+ self.parent, adapter.selectable, use_mapper_path=True
2542
+ ),
2543
+ self.key,
2544
+ self.parent_property,
2545
+ )
2546
+
2547
+ else:
2548
+ onclause = self.parent_property
2549
+
2550
+ assert clauses.is_aliased_class
2551
+
2552
+ attach_on_outside = (
2553
+ not chained_from_outerjoin
2554
+ or not innerjoin
2555
+ or innerjoin == "unnested"
2556
+ or query_entity.entity_zero.represents_outer_join
2557
+ )
2558
+
2559
+ extra_join_criteria = extra_criteria
2560
+ additional_entity_criteria = compile_state.global_attributes.get(
2561
+ ("additional_entity_criteria", self.mapper), ()
2562
+ )
2563
+ if additional_entity_criteria:
2564
+ extra_join_criteria += tuple(
2565
+ ae._resolve_where_criteria(self.mapper)
2566
+ for ae in additional_entity_criteria
2567
+ if ae.propagate_to_loaders
2568
+ )
2569
+
2570
+ if attach_on_outside:
2571
+ # this is the "classic" eager join case.
2572
+ eagerjoin = orm_util._ORMJoin(
2573
+ towrap,
2574
+ clauses.aliased_insp,
2575
+ onclause,
2576
+ isouter=not innerjoin
2577
+ or query_entity.entity_zero.represents_outer_join
2578
+ or (chained_from_outerjoin and isinstance(towrap, sql.Join)),
2579
+ _left_memo=self.parent,
2580
+ _right_memo=path[self.mapper],
2581
+ _extra_criteria=extra_join_criteria,
2582
+ )
2583
+ else:
2584
+ # all other cases are innerjoin=='nested' approach
2585
+ eagerjoin = self._splice_nested_inner_join(
2586
+ path, path[-2], towrap, clauses, onclause, extra_join_criteria
2587
+ )
2588
+
2589
+ compile_state.eager_joins[query_entity_key] = eagerjoin
2590
+
2591
+ # send a hint to the Query as to where it may "splice" this join
2592
+ eagerjoin.stop_on = query_entity.selectable
2593
+
2594
+ if not parentmapper:
2595
+ # for parentclause that is the non-eager end of the join,
2596
+ # ensure all the parent cols in the primaryjoin are actually
2597
+ # in the
2598
+ # columns clause (i.e. are not deferred), so that aliasing applied
2599
+ # by the Query propagates those columns outward.
2600
+ # This has the effect
2601
+ # of "undefering" those columns.
2602
+ for col in sql_util._find_columns(
2603
+ self.parent_property.primaryjoin
2604
+ ):
2605
+ if localparent.persist_selectable.c.contains_column(col):
2606
+ if adapter:
2607
+ col = adapter.columns[col]
2608
+ compile_state._append_dedupe_col_collection(
2609
+ col, compile_state.primary_columns
2610
+ )
2611
+
2612
+ if self.parent_property.order_by:
2613
+ compile_state.eager_order_by += tuple(
2614
+ (eagerjoin._target_adapter.copy_and_process)(
2615
+ util.to_list(self.parent_property.order_by)
2616
+ )
2617
+ )
2618
+
2619
+ def _splice_nested_inner_join(
2620
+ self,
2621
+ path,
2622
+ entity_we_want_to_splice_onto,
2623
+ join_obj,
2624
+ clauses,
2625
+ onclause,
2626
+ extra_criteria,
2627
+ entity_inside_join_structure: Union[
2628
+ Mapper, None, Literal[False]
2629
+ ] = False,
2630
+ detected_existing_path: Optional[path_registry.PathRegistry] = None,
2631
+ ):
2632
+ # recursive fn to splice a nested join into an existing one.
2633
+ # entity_inside_join_structure=False means this is the outermost call,
2634
+ # and it should return a value. entity_inside_join_structure=<mapper>
2635
+ # indicates we've descended into a join and are looking at a FROM
2636
+ # clause representing this mapper; if this is not
2637
+ # entity_we_want_to_splice_onto then return None to end the recursive
2638
+ # branch
2639
+
2640
+ assert entity_we_want_to_splice_onto is path[-2]
2641
+
2642
+ if entity_inside_join_structure is False:
2643
+ assert isinstance(join_obj, orm_util._ORMJoin)
2644
+
2645
+ if isinstance(join_obj, sql.selectable.FromGrouping):
2646
+ # FromGrouping - continue descending into the structure
2647
+ return self._splice_nested_inner_join(
2648
+ path,
2649
+ entity_we_want_to_splice_onto,
2650
+ join_obj.element,
2651
+ clauses,
2652
+ onclause,
2653
+ extra_criteria,
2654
+ entity_inside_join_structure,
2655
+ )
2656
+ elif isinstance(join_obj, orm_util._ORMJoin):
2657
+ # _ORMJoin - continue descending into the structure
2658
+
2659
+ join_right_path = join_obj._right_memo
2660
+
2661
+ # see if right side of join is viable
2662
+ target_join = self._splice_nested_inner_join(
2663
+ path,
2664
+ entity_we_want_to_splice_onto,
2665
+ join_obj.right,
2666
+ clauses,
2667
+ onclause,
2668
+ extra_criteria,
2669
+ entity_inside_join_structure=(
2670
+ join_right_path[-1].mapper
2671
+ if join_right_path is not None
2672
+ else None
2673
+ ),
2674
+ )
2675
+
2676
+ if target_join is not None:
2677
+ # for a right splice, attempt to flatten out
2678
+ # a JOIN b JOIN c JOIN .. to avoid needless
2679
+ # parenthesis nesting
2680
+ if not join_obj.isouter and not target_join.isouter:
2681
+ eagerjoin = join_obj._splice_into_center(target_join)
2682
+ else:
2683
+ eagerjoin = orm_util._ORMJoin(
2684
+ join_obj.left,
2685
+ target_join,
2686
+ join_obj.onclause,
2687
+ isouter=join_obj.isouter,
2688
+ _left_memo=join_obj._left_memo,
2689
+ )
2690
+
2691
+ eagerjoin._target_adapter = target_join._target_adapter
2692
+ return eagerjoin
2693
+
2694
+ else:
2695
+ # see if left side of join is viable
2696
+ target_join = self._splice_nested_inner_join(
2697
+ path,
2698
+ entity_we_want_to_splice_onto,
2699
+ join_obj.left,
2700
+ clauses,
2701
+ onclause,
2702
+ extra_criteria,
2703
+ entity_inside_join_structure=join_obj._left_memo,
2704
+ detected_existing_path=join_right_path,
2705
+ )
2706
+
2707
+ if target_join is not None:
2708
+ eagerjoin = orm_util._ORMJoin(
2709
+ target_join,
2710
+ join_obj.right,
2711
+ join_obj.onclause,
2712
+ isouter=join_obj.isouter,
2713
+ _right_memo=join_obj._right_memo,
2714
+ )
2715
+ eagerjoin._target_adapter = target_join._target_adapter
2716
+ return eagerjoin
2717
+
2718
+ # neither side viable, return None, or fail if this was the top
2719
+ # most call
2720
+ if entity_inside_join_structure is False:
2721
+ assert (
2722
+ False
2723
+ ), "assertion failed attempting to produce joined eager loads"
2724
+ return None
2725
+
2726
+ # reached an endpoint (e.g. a table that's mapped, or an alias of that
2727
+ # table). determine if we can use this endpoint to splice onto
2728
+
2729
+ # is this the entity we want to splice onto in the first place?
2730
+ if not entity_we_want_to_splice_onto.isa(entity_inside_join_structure):
2731
+ return None
2732
+
2733
+ # path check. if we know the path how this join endpoint got here,
2734
+ # lets look at our path we are satisfying and see if we're in the
2735
+ # wrong place. This is specifically for when our entity may
2736
+ # appear more than once in the path, issue #11449
2737
+ # updated in issue #11965.
2738
+ if detected_existing_path and len(detected_existing_path) > 2:
2739
+ # this assertion is currently based on how this call is made,
2740
+ # where given a join_obj, the call will have these parameters as
2741
+ # entity_inside_join_structure=join_obj._left_memo
2742
+ # and entity_inside_join_structure=join_obj._right_memo.mapper
2743
+ assert detected_existing_path[-3] is entity_inside_join_structure
2744
+
2745
+ # from that, see if the path we are targeting matches the
2746
+ # "existing" path of this join all the way up to the midpoint
2747
+ # of this join object (e.g. the relationship).
2748
+ # if not, then this is not our target
2749
+ #
2750
+ # a test condition where this test is false looks like:
2751
+ #
2752
+ # desired splice: Node->kind->Kind
2753
+ # path of desired splice: NodeGroup->nodes->Node->kind
2754
+ # path we've located: NodeGroup->nodes->Node->common_node->Node
2755
+ #
2756
+ # above, because we want to splice kind->Kind onto
2757
+ # NodeGroup->nodes->Node, this is not our path because it actually
2758
+ # goes more steps than we want into self-referential
2759
+ # ->common_node->Node
2760
+ #
2761
+ # a test condition where this test is true looks like:
2762
+ #
2763
+ # desired splice: B->c2s->C2
2764
+ # path of desired splice: A->bs->B->c2s
2765
+ # path we've located: A->bs->B->c1s->C1
2766
+ #
2767
+ # above, we want to splice c2s->C2 onto B, and the located path
2768
+ # shows that the join ends with B->c1s->C1. so we will
2769
+ # add another join onto that, which would create a "branch" that
2770
+ # we might represent in a pseudopath as:
2771
+ #
2772
+ # B->c1s->C1
2773
+ # ->c2s->C2
2774
+ #
2775
+ # i.e. A JOIN B ON <bs> JOIN C1 ON <c1s>
2776
+ # JOIN C2 ON <c2s>
2777
+ #
2778
+
2779
+ if detected_existing_path[0:-2] != path.path[0:-1]:
2780
+ return None
2781
+
2782
+ return orm_util._ORMJoin(
2783
+ join_obj,
2784
+ clauses.aliased_insp,
2785
+ onclause,
2786
+ isouter=False,
2787
+ _left_memo=entity_inside_join_structure,
2788
+ _right_memo=path[path[-1].mapper],
2789
+ _extra_criteria=extra_criteria,
2790
+ )
2791
+
2792
+ def _create_eager_adapter(self, context, result, adapter, path, loadopt):
2793
+ compile_state = context.compile_state
2794
+
2795
+ user_defined_adapter = (
2796
+ self._init_user_defined_eager_proc(
2797
+ loadopt, compile_state, context.attributes
2798
+ )
2799
+ if loadopt
2800
+ else False
2801
+ )
2802
+
2803
+ if user_defined_adapter is not False:
2804
+ decorator = user_defined_adapter
2805
+ # user defined eagerloads are part of the "primary"
2806
+ # portion of the load.
2807
+ # the adapters applied to the Query should be honored.
2808
+ if compile_state.compound_eager_adapter and decorator:
2809
+ decorator = decorator.wrap(
2810
+ compile_state.compound_eager_adapter
2811
+ )
2812
+ elif compile_state.compound_eager_adapter:
2813
+ decorator = compile_state.compound_eager_adapter
2814
+ else:
2815
+ decorator = path.get(
2816
+ compile_state.attributes, "eager_row_processor"
2817
+ )
2818
+ if decorator is None:
2819
+ return False
2820
+
2821
+ if self.mapper._result_has_identity_key(result, decorator):
2822
+ return decorator
2823
+ else:
2824
+ # no identity key - don't return a row
2825
+ # processor, will cause a degrade to lazy
2826
+ return False
2827
+
2828
+ def create_row_processor(
2829
+ self,
2830
+ context,
2831
+ query_entity,
2832
+ path,
2833
+ loadopt,
2834
+ mapper,
2835
+ result,
2836
+ adapter,
2837
+ populators,
2838
+ ):
2839
+
2840
+ if not context.compile_state.compile_options._enable_eagerloads:
2841
+ return
2842
+
2843
+ if not self.parent.class_manager[self.key].impl.supports_population:
2844
+ raise sa_exc.InvalidRequestError(
2845
+ "'%s' does not support object "
2846
+ "population - eager loading cannot be applied." % self
2847
+ )
2848
+
2849
+ if self.uselist:
2850
+ context.loaders_require_uniquing = True
2851
+
2852
+ our_path = path[self.parent_property]
2853
+
2854
+ eager_adapter = self._create_eager_adapter(
2855
+ context, result, adapter, our_path, loadopt
2856
+ )
2857
+
2858
+ if eager_adapter is not False:
2859
+ key = self.key
2860
+
2861
+ _instance = loading._instance_processor(
2862
+ query_entity,
2863
+ self.mapper,
2864
+ context,
2865
+ result,
2866
+ our_path[self.entity],
2867
+ eager_adapter,
2868
+ )
2869
+
2870
+ if not self.uselist:
2871
+ self._create_scalar_loader(context, key, _instance, populators)
2872
+ else:
2873
+ self._create_collection_loader(
2874
+ context, key, _instance, populators
2875
+ )
2876
+ else:
2877
+ self.parent_property._get_strategy(
2878
+ (("lazy", "select"),)
2879
+ ).create_row_processor(
2880
+ context,
2881
+ query_entity,
2882
+ path,
2883
+ loadopt,
2884
+ mapper,
2885
+ result,
2886
+ adapter,
2887
+ populators,
2888
+ )
2889
+
2890
+ def _create_collection_loader(self, context, key, _instance, populators):
2891
+ def load_collection_from_joined_new_row(state, dict_, row):
2892
+ # note this must unconditionally clear out any existing collection.
2893
+ # an existing collection would be present only in the case of
2894
+ # populate_existing().
2895
+ collection = attributes.init_state_collection(state, dict_, key)
2896
+ result_list = util.UniqueAppender(
2897
+ collection, "append_without_event"
2898
+ )
2899
+ context.attributes[(state, key)] = result_list
2900
+ inst = _instance(row)
2901
+ if inst is not None:
2902
+ result_list.append(inst)
2903
+
2904
+ def load_collection_from_joined_existing_row(state, dict_, row):
2905
+ if (state, key) in context.attributes:
2906
+ result_list = context.attributes[(state, key)]
2907
+ else:
2908
+ # appender_key can be absent from context.attributes
2909
+ # with isnew=False when self-referential eager loading
2910
+ # is used; the same instance may be present in two
2911
+ # distinct sets of result columns
2912
+ collection = attributes.init_state_collection(
2913
+ state, dict_, key
2914
+ )
2915
+ result_list = util.UniqueAppender(
2916
+ collection, "append_without_event"
2917
+ )
2918
+ context.attributes[(state, key)] = result_list
2919
+ inst = _instance(row)
2920
+ if inst is not None:
2921
+ result_list.append(inst)
2922
+
2923
+ def load_collection_from_joined_exec(state, dict_, row):
2924
+ _instance(row)
2925
+
2926
+ populators["new"].append(
2927
+ (self.key, load_collection_from_joined_new_row)
2928
+ )
2929
+ populators["existing"].append(
2930
+ (self.key, load_collection_from_joined_existing_row)
2931
+ )
2932
+ if context.invoke_all_eagers:
2933
+ populators["eager"].append(
2934
+ (self.key, load_collection_from_joined_exec)
2935
+ )
2936
+
2937
+ def _create_scalar_loader(self, context, key, _instance, populators):
2938
+ def load_scalar_from_joined_new_row(state, dict_, row):
2939
+ # set a scalar object instance directly on the parent
2940
+ # object, bypassing InstrumentedAttribute event handlers.
2941
+ dict_[key] = _instance(row)
2942
+
2943
+ def load_scalar_from_joined_existing_row(state, dict_, row):
2944
+ # call _instance on the row, even though the object has
2945
+ # been created, so that we further descend into properties
2946
+ existing = _instance(row)
2947
+
2948
+ # conflicting value already loaded, this shouldn't happen
2949
+ if key in dict_:
2950
+ if existing is not dict_[key]:
2951
+ util.warn(
2952
+ "Multiple rows returned with "
2953
+ "uselist=False for eagerly-loaded attribute '%s' "
2954
+ % self
2955
+ )
2956
+ else:
2957
+ # this case is when one row has multiple loads of the
2958
+ # same entity (e.g. via aliasing), one has an attribute
2959
+ # that the other doesn't.
2960
+ dict_[key] = existing
2961
+
2962
+ def load_scalar_from_joined_exec(state, dict_, row):
2963
+ _instance(row)
2964
+
2965
+ populators["new"].append((self.key, load_scalar_from_joined_new_row))
2966
+ populators["existing"].append(
2967
+ (self.key, load_scalar_from_joined_existing_row)
2968
+ )
2969
+ if context.invoke_all_eagers:
2970
+ populators["eager"].append(
2971
+ (self.key, load_scalar_from_joined_exec)
2972
+ )
2973
+
2974
+
2975
+ @log.class_logger
2976
+ @relationships.RelationshipProperty.strategy_for(lazy="selectin")
2977
+ class _SelectInLoader(_PostLoader, util.MemoizedSlots):
2978
+ __slots__ = (
2979
+ "join_depth",
2980
+ "omit_join",
2981
+ "_parent_alias",
2982
+ "_query_info",
2983
+ "_fallback_query_info",
2984
+ )
2985
+
2986
+ query_info = collections.namedtuple(
2987
+ "queryinfo",
2988
+ [
2989
+ "load_only_child",
2990
+ "load_with_join",
2991
+ "in_expr",
2992
+ "pk_cols",
2993
+ "zero_idx",
2994
+ "n_pk",
2995
+ "child_lookup_cols",
2996
+ ],
2997
+ )
2998
+
2999
+ _chunksize = 500
3000
+
3001
+ @classmethod
3002
+ def _set_chunksize(cls, loadopt) -> int:
3003
+ if loadopt is None or hasattr(loadopt, "local_opts") is None:
3004
+ return cls._chunksize
3005
+
3006
+ user_input = loadopt.local_opts.get("chunksize", None)
3007
+ if user_input is None:
3008
+ return cls._chunksize
3009
+ elif not isinstance(user_input, int) or user_input < 1:
3010
+ raise sa_exc.ArgumentError(
3011
+ f"'chunksize={user_input}' is not an appropriate input, "
3012
+ f"please use a positive non-zero integer."
3013
+ )
3014
+ return user_input
3015
+
3016
+ def __init__(self, parent, strategy_key):
3017
+ super().__init__(parent, strategy_key)
3018
+ self.join_depth = self.parent_property.join_depth
3019
+ is_m2o = self.parent_property.direction is interfaces.MANYTOONE
3020
+ is_m2m = self.parent_property.direction is interfaces.MANYTOMANY
3021
+
3022
+ if self.parent_property.omit_join is not None:
3023
+ self.omit_join = self.parent_property.omit_join
3024
+ else:
3025
+ lazyloader = self.parent_property._get_strategy(
3026
+ (("lazy", "select"),)
3027
+ )
3028
+ if is_m2o:
3029
+ self.omit_join = lazyloader.use_get
3030
+ elif is_m2m and not self.parent_property._is_self_referential:
3031
+ join_cond = self.parent_property._join_condition
3032
+ self.omit_join = join_cond.secondary_covers_parent_primary_key
3033
+ else:
3034
+ self.omit_join = self.parent._get_clause[0].compare(
3035
+ lazyloader._rev_lazywhere,
3036
+ use_proxies=True,
3037
+ compare_keys=False,
3038
+ equivalents=self.parent._equivalent_columns,
3039
+ )
3040
+
3041
+ if self.omit_join:
3042
+ if is_m2o:
3043
+ self._query_info = self._init_for_omit_join_m2o()
3044
+ self._fallback_query_info = self._init_for_join()
3045
+ else:
3046
+ self._query_info = self._init_for_omit_join()
3047
+ else:
3048
+ self._query_info = self._init_for_join()
3049
+
3050
+ def _init_for_omit_join(self):
3051
+ pk_to_fk = dict(
3052
+ self.parent_property._join_condition.local_remote_pairs
3053
+ )
3054
+ pk_to_fk.update(
3055
+ (equiv, pk_to_fk[k])
3056
+ for k in list(pk_to_fk)
3057
+ for equiv in self.parent._equivalent_columns.get(k, ())
3058
+ )
3059
+
3060
+ pk_cols = fk_cols = [
3061
+ pk_to_fk[col] for col in self.parent.primary_key if col in pk_to_fk
3062
+ ]
3063
+ if len(fk_cols) > 1:
3064
+ in_expr = sql.tuple_(*fk_cols)
3065
+ zero_idx = False
3066
+ else:
3067
+ in_expr = fk_cols[0]
3068
+ zero_idx = True
3069
+
3070
+ return self.query_info(
3071
+ False, False, in_expr, pk_cols, zero_idx, len(pk_cols), None
3072
+ )
3073
+
3074
+ def _init_for_omit_join_m2o(self):
3075
+ pk_cols = self.mapper.primary_key
3076
+ if len(pk_cols) > 1:
3077
+ in_expr = sql.tuple_(*pk_cols)
3078
+ zero_idx = False
3079
+ else:
3080
+ in_expr = pk_cols[0]
3081
+ zero_idx = True
3082
+
3083
+ lazyloader = self.parent_property._get_strategy((("lazy", "select"),))
3084
+ lookup_cols = [lazyloader._equated_columns[pk] for pk in pk_cols]
3085
+
3086
+ return self.query_info(
3087
+ True, False, in_expr, pk_cols, zero_idx, len(pk_cols), lookup_cols
3088
+ )
3089
+
3090
+ def _init_for_join(self):
3091
+ self._parent_alias = AliasedClass(self.parent.class_)
3092
+ pa_insp = inspect(self._parent_alias)
3093
+ pk_cols = [
3094
+ pa_insp._adapt_element(col) for col in self.parent.primary_key
3095
+ ]
3096
+ if len(pk_cols) > 1:
3097
+ in_expr = sql.tuple_(*pk_cols)
3098
+ zero_idx = False
3099
+ else:
3100
+ in_expr = pk_cols[0]
3101
+ zero_idx = True
3102
+ return self.query_info(
3103
+ False, True, in_expr, pk_cols, zero_idx, len(pk_cols), None
3104
+ )
3105
+
3106
+ def init_class_attribute(self, mapper):
3107
+ self.parent_property._get_strategy(
3108
+ (("lazy", "select"),)
3109
+ ).init_class_attribute(mapper)
3110
+
3111
+ def create_row_processor(
3112
+ self,
3113
+ context,
3114
+ query_entity,
3115
+ path,
3116
+ loadopt,
3117
+ mapper,
3118
+ result,
3119
+ adapter,
3120
+ populators,
3121
+ ):
3122
+ if context.refresh_state:
3123
+ return self._immediateload_create_row_processor(
3124
+ context,
3125
+ query_entity,
3126
+ path,
3127
+ loadopt,
3128
+ mapper,
3129
+ result,
3130
+ adapter,
3131
+ populators,
3132
+ )
3133
+
3134
+ (
3135
+ effective_path,
3136
+ run_loader,
3137
+ execution_options,
3138
+ recursion_depth,
3139
+ ) = self._setup_for_recursion(
3140
+ context, path, loadopt, join_depth=self.join_depth
3141
+ )
3142
+
3143
+ if not run_loader:
3144
+ return
3145
+
3146
+ if not context.compile_state.compile_options._enable_eagerloads:
3147
+ return
3148
+
3149
+ if not self.parent.class_manager[self.key].impl.supports_population:
3150
+ raise sa_exc.InvalidRequestError(
3151
+ "'%s' does not support object "
3152
+ "population - eager loading cannot be applied." % self
3153
+ )
3154
+
3155
+ # a little dance here as the "path" is still something that only
3156
+ # semi-tracks the exact series of things we are loading, still not
3157
+ # telling us about with_polymorphic() and stuff like that when it's at
3158
+ # the root.. the initial MapperEntity is more accurate for this case.
3159
+ if len(path) == 1:
3160
+ if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
3161
+ return
3162
+ elif not orm_util._entity_isa(
3163
+ path[-1], self.parent
3164
+ ) and not self.parent.isa(path[-1].mapper):
3165
+ # second check accommodates a polymorphic entity where
3166
+ # the path has been normalized to the base mapper but
3167
+ # self.parent is a subclass mapper, e.g.
3168
+ # joinedload(A.b.of_type(poly)).selectinload(poly.Sub.rel)
3169
+ # Fixes #13209.
3170
+ return
3171
+
3172
+ selectin_path = effective_path
3173
+
3174
+ path_w_prop = path[self.parent_property]
3175
+
3176
+ # build up a path indicating the path from the leftmost
3177
+ # entity to the thing we're subquery loading.
3178
+ with_poly_entity = path_w_prop.get(
3179
+ context.attributes, "path_with_polymorphic", None
3180
+ )
3181
+ if with_poly_entity is not None:
3182
+ effective_entity = inspect(with_poly_entity)
3183
+ else:
3184
+ effective_entity = self.entity
3185
+
3186
+ loading._PostLoad.callable_for_path(
3187
+ context,
3188
+ selectin_path,
3189
+ self.parent,
3190
+ self.parent_property,
3191
+ self._load_for_path,
3192
+ effective_entity,
3193
+ loadopt,
3194
+ recursion_depth,
3195
+ execution_options,
3196
+ )
3197
+
3198
+ def _load_for_path(
3199
+ self,
3200
+ context,
3201
+ path,
3202
+ states,
3203
+ load_only,
3204
+ effective_entity,
3205
+ loadopt,
3206
+ recursion_depth,
3207
+ execution_options,
3208
+ ):
3209
+ if load_only and self.key not in load_only:
3210
+ return
3211
+
3212
+ query_info = self._query_info
3213
+
3214
+ if query_info.load_only_child:
3215
+ our_states = collections.defaultdict(list)
3216
+ none_states = []
3217
+
3218
+ mapper = self.parent
3219
+
3220
+ # attribute keys for the lookup columns; when these are
3221
+ # present in a state's dict, reading them directly is
3222
+ # equivalent to the PASSIVE_NO_FETCH attribute lookup below.
3223
+ # whether or not a key is present can vary per state, e.g.
3224
+ # individual instances may have the attribute expired or
3225
+ # deferred, so this is determined state-by-state
3226
+ get_related_ident = mapper._state_ident_getter(
3227
+ query_info.child_lookup_cols,
3228
+ passive=attributes.PASSIVE_NO_FETCH,
3229
+ )
3230
+
3231
+ for state, overwrite in states:
3232
+ state_dict = state.dict
3233
+ related_ident = get_related_ident(state, state_dict)
3234
+ # if the loaded parent objects do not have the foreign key
3235
+ # to the related item loaded, then degrade into the joined
3236
+ # version of selectinload
3237
+ if LoaderCallableStatus.PASSIVE_NO_RESULT in related_ident:
3238
+ query_info = self._fallback_query_info
3239
+ break
3240
+
3241
+ # organize states into lists keyed to particular foreign
3242
+ # key values.
3243
+ if None not in related_ident:
3244
+ our_states[related_ident].append(
3245
+ (state, state_dict, overwrite)
3246
+ )
3247
+ else:
3248
+ # For FK values that have None, add them to a
3249
+ # separate collection that will be populated separately
3250
+ none_states.append((state, state_dict, overwrite))
3251
+
3252
+ # note the above conditional may have changed query_info
3253
+ if not query_info.load_only_child:
3254
+ our_states = [
3255
+ (state.key[1], state, state.dict, overwrite)
3256
+ for state, overwrite in states
3257
+ ]
3258
+
3259
+ pk_cols = query_info.pk_cols
3260
+ in_expr = query_info.in_expr
3261
+
3262
+ if not query_info.load_with_join:
3263
+ # in "omit join" mode, the primary key column and the
3264
+ # "in" expression are in terms of the related entity. So
3265
+ # if the related entity is polymorphic or otherwise aliased,
3266
+ # we need to adapt our "pk_cols" and "in_expr" to that
3267
+ # entity. in non-"omit join" mode, these are against the
3268
+ # parent entity and do not need adaption.
3269
+ if effective_entity.is_aliased_class:
3270
+ pk_cols = [
3271
+ effective_entity._adapt_element(col) for col in pk_cols
3272
+ ]
3273
+ in_expr = effective_entity._adapt_element(in_expr)
3274
+
3275
+ entity_sql = effective_entity.__clause_element__()
3276
+ q = Select._create_raw_select(
3277
+ _raw_columns=[*pk_cols, entity_sql],
3278
+ _compile_options=_ORMCompileState.default_compile_options,
3279
+ _propagate_attrs={
3280
+ "compile_state_plugin": "orm",
3281
+ "plugin_subject": effective_entity,
3282
+ },
3283
+ )
3284
+
3285
+ if (
3286
+ self.parent_property.secondary is not None
3287
+ and self.omit_join is True
3288
+ ):
3289
+ # The secondaryjoin condition is used to connect the
3290
+ # secondary table to the related entity,
3291
+ # and is required for composite foreign keys where SQLAlchemy
3292
+ # cannot determine the join condition.
3293
+ q = q.select_from(self.parent_property.secondary).join(
3294
+ entity_sql, self.parent_property._join_condition.secondaryjoin
3295
+ )
3296
+ elif not query_info.load_with_join:
3297
+ # the pk columns in the "omit_join" case are raw, non-annotated
3298
+ # columns, so to ensure the Query knows its primary entity, we
3299
+ # add it explicitly. Using annotated columns here would hit a
3300
+ # performance issue detailed in issue #4347.
3301
+ q = q.select_from(effective_entity)
3302
+ else:
3303
+ # in the non-omit_join case, the pk columns are against the
3304
+ # annotated/mapped column of the parent entity, but the #4347
3305
+ # issue does not occur in this case.
3306
+ q = q.select_from(self._parent_alias).join(
3307
+ getattr(self._parent_alias, self.parent_property.key).of_type(
3308
+ effective_entity
3309
+ )
3310
+ )
3311
+
3312
+ q = q.filter(in_expr.in_(sql.bindparam("primary_keys")))
3313
+
3314
+ # a test which exercises what these comments talk about is
3315
+ # test_selectin_relations.py -> test_twolevel_selectin_w_polymorphic
3316
+ #
3317
+ # effective_entity above is given to us in terms of the cached
3318
+ # statement, namely this one:
3319
+ orig_query = context.compile_state.select_statement
3320
+
3321
+ # the actual statement that was requested is this one:
3322
+ # context_query = context.user_passed_query
3323
+ #
3324
+ # that's not the cached one, however. So while it is of the identical
3325
+ # structure, if it has entities like AliasedInsp, which we get from
3326
+ # aliased() or with_polymorphic(), the AliasedInsp will likely be a
3327
+ # different object identity each time, and will not match up
3328
+ # hashing-wise to the corresponding AliasedInsp that's in the
3329
+ # cached query, meaning it won't match on paths and loader lookups
3330
+ # and loaders like this one will be skipped if it is used in options.
3331
+ #
3332
+ # as it turns out, standard loader options like selectinload(),
3333
+ # lazyload() that have a path need
3334
+ # to come from the cached query so that the AliasedInsp etc. objects
3335
+ # that are in the query line up with the object that's in the path
3336
+ # of the strategy object. however other options like
3337
+ # with_loader_criteria() that doesn't have a path (has a fixed entity)
3338
+ # and needs to have access to the latest closure state in order to
3339
+ # be correct, we need to use the uncached one.
3340
+ #
3341
+ # as of #8399 we let the loader option itself figure out what it
3342
+ # wants to do given cached and uncached version of itself.
3343
+
3344
+ effective_path = path[self.parent_property]
3345
+
3346
+ if orig_query is context.user_passed_query:
3347
+ new_options = orig_query._with_options
3348
+ else:
3349
+ cached_options = orig_query._with_options
3350
+ uncached_options = context.user_passed_query._with_options
3351
+
3352
+ # propagate compile state options from the original query,
3353
+ # updating their "extra_criteria" as necessary.
3354
+ # note this will create a different cache key than
3355
+ # "orig" options if extra_criteria is present, because the copy
3356
+ # of extra_criteria will have different boundparam than that of
3357
+ # the QueryableAttribute in the path
3358
+ new_options = [
3359
+ orig_opt._adapt_cached_option_to_uncached_option(
3360
+ context, uncached_opt
3361
+ )
3362
+ for orig_opt, uncached_opt in zip(
3363
+ cached_options, uncached_options
3364
+ )
3365
+ ]
3366
+
3367
+ if loadopt and loadopt._extra_criteria:
3368
+ new_options += (
3369
+ orm_util.LoaderCriteriaOption(
3370
+ effective_entity,
3371
+ loadopt._generate_extra_criteria(context),
3372
+ ),
3373
+ )
3374
+
3375
+ if recursion_depth is not None:
3376
+ effective_path = effective_path._truncate_recursive()
3377
+
3378
+ q = q.options(*new_options)
3379
+
3380
+ q = q._update_compile_options({"_current_path": effective_path})
3381
+ if context.populate_existing:
3382
+ q = q.execution_options(populate_existing=True)
3383
+
3384
+ if self.parent_property.order_by:
3385
+ if not query_info.load_with_join:
3386
+ eager_order_by = self.parent_property.order_by
3387
+ if effective_entity.is_aliased_class:
3388
+ eager_order_by = [
3389
+ effective_entity._adapt_element(elem)
3390
+ for elem in eager_order_by
3391
+ ]
3392
+ q = q.order_by(*eager_order_by)
3393
+ else:
3394
+
3395
+ def _setup_outermost_orderby(compile_context):
3396
+ compile_context.eager_order_by += tuple(
3397
+ util.to_list(self.parent_property.order_by)
3398
+ )
3399
+
3400
+ q = q._add_compile_state_func(
3401
+ _setup_outermost_orderby, self.parent_property
3402
+ )
3403
+
3404
+ chunksize = self._set_chunksize(loadopt)
3405
+
3406
+ if query_info.load_only_child:
3407
+ self._load_via_child(
3408
+ our_states,
3409
+ none_states,
3410
+ query_info,
3411
+ q,
3412
+ context,
3413
+ execution_options,
3414
+ chunksize,
3415
+ )
3416
+ else:
3417
+ self._load_via_parent(
3418
+ our_states,
3419
+ query_info,
3420
+ q,
3421
+ context,
3422
+ execution_options,
3423
+ chunksize,
3424
+ )
3425
+
3426
+ def _load_via_child(
3427
+ self,
3428
+ our_states,
3429
+ none_states,
3430
+ query_info,
3431
+ q,
3432
+ context,
3433
+ execution_options,
3434
+ chunksize,
3435
+ ):
3436
+ uselist = self.uselist
3437
+ n_pk = query_info.n_pk
3438
+ zero_idx = query_info.zero_idx
3439
+
3440
+ # historically this used sorted instead of list to add determinism in
3441
+ # tests. Since dicts are now ordered it's likely no longer needed
3442
+ our_keys = list(our_states)
3443
+ while our_keys:
3444
+ chunk = our_keys[0:chunksize]
3445
+ our_keys = our_keys[chunksize:]
3446
+ primary_keys = [key[0] if zero_idx else key for key in chunk]
3447
+ result = context.session.execute(
3448
+ q,
3449
+ params={"primary_keys": primary_keys},
3450
+ execution_options=execution_options,
3451
+ )
3452
+ if result.context is not None and result.context.requires_uniquing:
3453
+ rows = result.unique()
3454
+ else:
3455
+ rows = result._raw_all_tuples()
3456
+ data = {row[:n_pk]: row[n_pk] for row in rows}
3457
+
3458
+ for key in chunk:
3459
+ # for a real foreign key and no concurrent changes to the
3460
+ # DB while running this method, "key" is always present in
3461
+ # data. However, for primaryjoins without real foreign keys
3462
+ # a non-None primaryjoin condition may still refer to no
3463
+ # related object.
3464
+ related_obj = data.get(key, None)
3465
+ for state, dict_, overwrite in our_states[key]:
3466
+ if not overwrite and self.key in dict_:
3467
+ continue
3468
+
3469
+ state.get_impl(self.key).set_committed_value(
3470
+ state,
3471
+ dict_,
3472
+ related_obj if not uselist else [related_obj],
3473
+ )
3474
+ # populate none states with empty value / collection
3475
+ for state, dict_, overwrite in none_states:
3476
+ if not overwrite and self.key in dict_:
3477
+ continue
3478
+
3479
+ # note it's OK if this is a uselist=True attribute, the empty
3480
+ # collection will be populated
3481
+ state.get_impl(self.key).set_committed_value(state, dict_, None)
3482
+
3483
+ def _load_via_parent(
3484
+ self, our_states, query_info, q, context, execution_options, chunksize
3485
+ ):
3486
+ uselist = self.uselist
3487
+ n_pk = query_info.n_pk
3488
+ zero_idx = query_info.zero_idx
3489
+ _empty_result = () if uselist else None
3490
+
3491
+ while our_states:
3492
+ chunk = our_states[0:chunksize]
3493
+ our_states = our_states[chunksize:]
3494
+
3495
+ primary_keys = [
3496
+ item[0][0] if zero_idx else item[0] for item in chunk
3497
+ ]
3498
+
3499
+ result = context.session.execute(
3500
+ q,
3501
+ params={"primary_keys": primary_keys},
3502
+ execution_options=execution_options,
3503
+ )
3504
+ if result.context is not None and result.context.requires_uniquing:
3505
+ rows = result.unique()
3506
+ else:
3507
+ rows = result._raw_all_tuples()
3508
+ data = collections.defaultdict(list)
3509
+ for row in rows:
3510
+ data[row[:n_pk]].append(row[n_pk])
3511
+
3512
+ for key, state, state_dict, overwrite in chunk:
3513
+ if not overwrite and self.key in state_dict:
3514
+ continue
3515
+
3516
+ collection = data.get(key, _empty_result)
3517
+
3518
+ if not uselist and collection:
3519
+ if len(collection) > 1:
3520
+ util.warn(
3521
+ "Multiple rows returned with "
3522
+ "uselist=False for eagerly-loaded "
3523
+ "attribute '%s' " % self
3524
+ )
3525
+ state.get_impl(self.key).set_committed_value(
3526
+ state, state_dict, collection[0]
3527
+ )
3528
+ else:
3529
+ # note that empty tuple set on uselist=False sets the
3530
+ # value to None
3531
+ state.get_impl(self.key).set_committed_value(
3532
+ state, state_dict, collection
3533
+ )
3534
+
3535
+
3536
+ def _single_parent_validator(desc, prop):
3537
+ def _do_check(state, value, oldvalue, initiator):
3538
+ if value is not None and initiator.key == prop.key:
3539
+ hasparent = initiator.hasparent(attributes.instance_state(value))
3540
+ if hasparent and oldvalue is not value:
3541
+ raise sa_exc.InvalidRequestError(
3542
+ "Instance %s is already associated with an instance "
3543
+ "of %s via its %s attribute, and is only allowed a "
3544
+ "single parent."
3545
+ % (orm_util.instance_str(value), state.class_, prop),
3546
+ code="bbf1",
3547
+ )
3548
+ return value
3549
+
3550
+ def append(state, value, initiator):
3551
+ return _do_check(state, value, None, initiator)
3552
+
3553
+ def set_(state, value, oldvalue, initiator):
3554
+ return _do_check(state, value, oldvalue, initiator)
3555
+
3556
+ event.listen(
3557
+ desc, "append", append, raw=True, retval=True, active_history=True
3558
+ )
3559
+ event.listen(desc, "set", set_, raw=True, retval=True, active_history=True)