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