strategies.py 111 KB

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