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