strategies.py 117 KB

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