Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/strategies.py: 17%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1163 statements  

1# orm/strategies.py 

2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors 

3# <see AUTHORS file> 

4# 

5# This module is part of SQLAlchemy and is released under 

6# the MIT License: https://www.opensource.org/licenses/mit-license.php 

7# mypy: ignore-errors 

8 

9 

10"""sqlalchemy.orm.interfaces.LoaderStrategy 

11implementations, and related MapperOptions.""" 

12 

13from __future__ import annotations 

14 

15import collections 

16import itertools 

17from typing import Any 

18from typing import Dict 

19from typing import Literal 

20from typing import Optional 

21from typing import Tuple 

22from typing import TYPE_CHECKING 

23from typing import Union 

24 

25from . import attributes 

26from . import exc as orm_exc 

27from . import interfaces 

28from . import loading 

29from . import path_registry 

30from . import properties 

31from . import query 

32from . import relationships 

33from . import unitofwork 

34from . import util as orm_util 

35from .base import _DEFER_FOR_STATE 

36from .base import _RAISE_FOR_STATE 

37from .base import _SET_DEFERRED_EXPIRED 

38from .base import ATTR_WAS_SET 

39from .base import LoaderCallableStatus 

40from .base import PASSIVE_OFF 

41from .base import PassiveFlag 

42from .context import _column_descriptions 

43from .context import _ORMCompileState 

44from .context import _ORMSelectCompileState 

45from .context import QueryContext 

46from .interfaces import LoaderStrategy 

47from .interfaces import StrategizedProperty 

48from .session import _state_session 

49from .state import InstanceState 

50from .strategy_options import Load 

51from .util import _none_only_set 

52from .util import AliasedClass 

53from .. import event 

54from .. import exc as sa_exc 

55from .. import inspect 

56from .. import log 

57from .. import sql 

58from .. import util 

59from ..sql import util as sql_util 

60from ..sql import visitors 

61from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL 

62from ..sql.selectable import Select 

63 

64if TYPE_CHECKING: 

65 from .mapper import Mapper 

66 from .relationships import RelationshipProperty 

67 from ..sql.elements import ColumnElement 

68 

69 

70def _register_attribute( 

71 prop, 

72 mapper, 

73 useobject, 

74 compare_function=None, 

75 typecallable=None, 

76 callable_=None, 

77 proxy_property=None, 

78 active_history=False, 

79 impl_class=None, 

80 default_scalar_value=None, 

81 **kw, 

82): 

83 listen_hooks = [] 

84 

85 uselist = useobject and prop.uselist 

86 

87 if useobject and prop.single_parent: 

88 listen_hooks.append(_single_parent_validator) 

89 

90 if prop.key in prop.parent.validators: 

91 fn, opts = prop.parent.validators[prop.key] 

92 listen_hooks.append( 

93 lambda desc, prop: orm_util._validator_events( 

94 desc, prop.key, fn, **opts 

95 ) 

96 ) 

97 

98 if useobject: 

99 listen_hooks.append(unitofwork._track_cascade_events) 

100 

101 # need to assemble backref listeners 

102 # after the singleparentvalidator, mapper validator 

103 if useobject: 

104 backref = prop.back_populates 

105 if backref and prop._effective_sync_backref: 

106 listen_hooks.append( 

107 lambda desc, prop: attributes._backref_listeners( 

108 desc, backref, uselist 

109 ) 

110 ) 

111 

112 # a single MapperProperty is shared down a class inheritance 

113 # hierarchy, so we set up attribute instrumentation and backref event 

114 # for each mapper down the hierarchy. 

115 

116 # typically, "mapper" is the same as prop.parent, due to the way 

117 # the configure_mappers() process runs, however this is not strongly 

118 # enforced, and in the case of a second configure_mappers() run the 

119 # mapper here might not be prop.parent; also, a subclass mapper may 

120 # be called here before a superclass mapper. That is, can't depend 

121 # on mappers not already being set up so we have to check each one. 

122 

123 for m in mapper.self_and_descendants: 

124 if prop is m._props.get( 

125 prop.key 

126 ) and not m.class_manager._attr_has_impl(prop.key): 

127 desc = attributes._register_attribute_impl( 

128 m.class_, 

129 prop.key, 

130 parent_token=prop, 

131 uselist=uselist, 

132 compare_function=compare_function, 

133 useobject=useobject, 

134 trackparent=useobject 

135 and ( 

136 prop.single_parent 

137 or prop.direction is interfaces.ONETOMANY 

138 ), 

139 typecallable=typecallable, 

140 callable_=callable_, 

141 active_history=active_history, 

142 default_scalar_value=default_scalar_value, 

143 impl_class=impl_class, 

144 send_modified_events=not useobject or not prop.viewonly, 

145 doc=prop.doc, 

146 **kw, 

147 ) 

148 

149 for hook in listen_hooks: 

150 hook(desc, prop) 

151 

152 

153@properties.ColumnProperty.strategy_for(instrument=False, deferred=False) 

154class _UninstrumentedColumnLoader(LoaderStrategy): 

155 """Represent a non-instrumented MapperProperty. 

156 

157 The polymorphic_on argument of mapper() often results in this, 

158 if the argument is against the with_polymorphic selectable. 

159 

160 """ 

161 

162 __slots__ = ("columns",) 

163 

164 def __init__(self, parent, strategy_key): 

165 super().__init__(parent, strategy_key) 

166 self.columns = self.parent_property.columns 

167 

168 def setup_query( 

169 self, 

170 compile_state, 

171 query_entity, 

172 path, 

173 loadopt, 

174 adapter, 

175 column_collection=None, 

176 **kwargs, 

177 ): 

178 for c in self.columns: 

179 if adapter: 

180 c = adapter.columns[c] 

181 compile_state._append_dedupe_col_collection(c, column_collection) 

182 

183 def create_row_processor( 

184 self, 

185 context, 

186 query_entity, 

187 path, 

188 loadopt, 

189 mapper, 

190 result, 

191 adapter, 

192 populators, 

193 ): 

194 pass 

195 

196 

197@log.class_logger 

198@properties.ColumnProperty.strategy_for(instrument=True, deferred=False) 

199class _ColumnLoader(LoaderStrategy): 

200 """Provide loading behavior for a :class:`.ColumnProperty`.""" 

201 

202 __slots__ = "columns", "is_composite" 

203 

204 def __init__(self, parent, strategy_key): 

205 super().__init__(parent, strategy_key) 

206 self.columns = self.parent_property.columns 

207 self.is_composite = hasattr(self.parent_property, "composite_class") 

208 

209 def setup_query( 

210 self, 

211 compile_state, 

212 query_entity, 

213 path, 

214 loadopt, 

215 adapter, 

216 column_collection, 

217 memoized_populators, 

218 check_for_adapt=False, 

219 **kwargs, 

220 ): 

221 for c in self.columns: 

222 if adapter: 

223 if check_for_adapt: 

224 c = adapter.adapt_check_present(c) 

225 if c is None: 

226 return 

227 else: 

228 c = adapter.columns[c] 

229 

230 compile_state._append_dedupe_col_collection(c, column_collection) 

231 

232 fetch = self.columns[0] 

233 if adapter: 

234 fetch = adapter.columns[fetch] 

235 if fetch is None: 

236 # None happens here only for dml bulk_persistence cases 

237 # when context.DMLReturningColFilter is used 

238 return 

239 

240 memoized_populators[self.parent_property] = fetch 

241 

242 def init_class_attribute(self, mapper): 

243 self.is_class_level = True 

244 coltype = self.columns[0].type 

245 # TODO: check all columns ? check for foreign key as well? 

246 active_history = ( 

247 self.parent_property.active_history 

248 or self.columns[0].primary_key 

249 or ( 

250 mapper.version_id_col is not None 

251 and mapper._columntoproperty.get(mapper.version_id_col, None) 

252 is self.parent_property 

253 ) 

254 ) 

255 

256 _register_attribute( 

257 self.parent_property, 

258 mapper, 

259 useobject=False, 

260 compare_function=coltype.compare_values, 

261 active_history=active_history, 

262 default_scalar_value=self.parent_property._default_scalar_value, 

263 ) 

264 

265 def create_row_processor( 

266 self, 

267 context, 

268 query_entity, 

269 path, 

270 loadopt, 

271 mapper, 

272 result, 

273 adapter, 

274 populators, 

275 ): 

276 # look through list of columns represented here 

277 # to see which, if any, is present in the row. 

278 

279 for col in self.columns: 

280 if adapter: 

281 col = adapter.columns[col] 

282 getter = result._getter(col, False) 

283 if getter: 

284 populators["quick"].append((self.key, getter)) 

285 break 

286 else: 

287 populators["expire"].append((self.key, True)) 

288 

289 

290@log.class_logger 

291@properties.ColumnProperty.strategy_for(query_expression=True) 

292class _ExpressionColumnLoader(_ColumnLoader): 

293 def __init__(self, parent, strategy_key): 

294 super().__init__(parent, strategy_key) 

295 

296 # compare to the "default" expression that is mapped in 

297 # the column. If it's sql.null, we don't need to render 

298 # unless an expr is passed in the options. 

299 null = sql.null().label(None) 

300 self._have_default_expression = any( 

301 not c.compare(null) for c in self.parent_property.columns 

302 ) 

303 

304 def setup_query( 

305 self, 

306 compile_state, 

307 query_entity, 

308 path, 

309 loadopt, 

310 adapter, 

311 column_collection, 

312 memoized_populators, 

313 **kwargs, 

314 ): 

315 columns = None 

316 if loadopt and loadopt._extra_criteria: 

317 columns = loadopt._extra_criteria 

318 

319 elif self._have_default_expression: 

320 columns = self.parent_property.columns 

321 

322 if columns is None: 

323 return 

324 

325 for c in columns: 

326 if adapter: 

327 c = adapter.columns[c] 

328 compile_state._append_dedupe_col_collection(c, column_collection) 

329 

330 fetch = columns[0] 

331 if adapter: 

332 fetch = adapter.columns[fetch] 

333 if fetch is None: 

334 # None is not expected to be the result of any 

335 # adapter implementation here, however there may be theoretical 

336 # usages of returning() with context.DMLReturningColFilter 

337 return 

338 

339 memoized_populators[self.parent_property] = fetch 

340 

341 def create_row_processor( 

342 self, 

343 context, 

344 query_entity, 

345 path, 

346 loadopt, 

347 mapper, 

348 result, 

349 adapter, 

350 populators, 

351 ): 

352 # look through list of columns represented here 

353 # to see which, if any, is present in the row. 

354 if loadopt and loadopt._extra_criteria: 

355 columns = loadopt._extra_criteria 

356 

357 for col in columns: 

358 if adapter: 

359 col = adapter.columns[col] 

360 getter = result._getter(col, False) 

361 if getter: 

362 populators["quick"].append((self.key, getter)) 

363 break 

364 else: 

365 populators["expire"].append((self.key, True)) 

366 

367 def init_class_attribute(self, mapper): 

368 self.is_class_level = True 

369 

370 _register_attribute( 

371 self.parent_property, 

372 mapper, 

373 useobject=False, 

374 compare_function=self.columns[0].type.compare_values, 

375 accepts_scalar_loader=False, 

376 default_scalar_value=self.parent_property._default_scalar_value, 

377 ) 

378 

379 

380@log.class_logger 

381@properties.ColumnProperty.strategy_for(deferred=True, instrument=True) 

382@properties.ColumnProperty.strategy_for( 

383 deferred=True, instrument=True, raiseload=True 

384) 

385@properties.ColumnProperty.strategy_for(do_nothing=True) 

386class _DeferredColumnLoader(LoaderStrategy): 

387 """Provide loading behavior for a deferred :class:`.ColumnProperty`.""" 

388 

389 __slots__ = "columns", "group", "raiseload" 

390 

391 def __init__(self, parent, strategy_key): 

392 super().__init__(parent, strategy_key) 

393 if hasattr(self.parent_property, "composite_class"): 

394 raise NotImplementedError( 

395 "Deferred loading for composite types not implemented yet" 

396 ) 

397 self.raiseload = self.strategy_opts.get("raiseload", False) 

398 self.columns = self.parent_property.columns 

399 self.group = self.parent_property.group 

400 

401 def create_row_processor( 

402 self, 

403 context, 

404 query_entity, 

405 path, 

406 loadopt, 

407 mapper, 

408 result, 

409 adapter, 

410 populators, 

411 ): 

412 # for a DeferredColumnLoader, this method is only used during a 

413 # "row processor only" query; see test_deferred.py -> 

414 # tests with "rowproc_only" in their name. As of the 1.0 series, 

415 # loading._instance_processor doesn't use a "row processing" function 

416 # to populate columns, instead it uses data in the "populators" 

417 # dictionary. Normally, the DeferredColumnLoader.setup_query() 

418 # sets up that data in the "memoized_populators" dictionary 

419 # and "create_row_processor()" here is never invoked. 

420 

421 if ( 

422 context.refresh_state 

423 and context.query._compile_options._only_load_props 

424 and self.key in context.query._compile_options._only_load_props 

425 ): 

426 self.parent_property._get_strategy( 

427 (("deferred", False), ("instrument", True)) 

428 ).create_row_processor( 

429 context, 

430 query_entity, 

431 path, 

432 loadopt, 

433 mapper, 

434 result, 

435 adapter, 

436 populators, 

437 ) 

438 

439 elif not self.is_class_level: 

440 if self.raiseload: 

441 set_deferred_for_local_state = ( 

442 self.parent_property._raise_column_loader 

443 ) 

444 else: 

445 set_deferred_for_local_state = ( 

446 self.parent_property._deferred_column_loader 

447 ) 

448 populators["new"].append((self.key, set_deferred_for_local_state)) 

449 else: 

450 populators["expire"].append((self.key, False)) 

451 

452 def init_class_attribute(self, mapper): 

453 self.is_class_level = True 

454 

455 _register_attribute( 

456 self.parent_property, 

457 mapper, 

458 useobject=False, 

459 compare_function=self.columns[0].type.compare_values, 

460 callable_=self._load_for_state, 

461 load_on_unexpire=False, 

462 default_scalar_value=self.parent_property._default_scalar_value, 

463 ) 

464 

465 def setup_query( 

466 self, 

467 compile_state, 

468 query_entity, 

469 path, 

470 loadopt, 

471 adapter, 

472 column_collection, 

473 memoized_populators, 

474 only_load_props=None, 

475 **kw, 

476 ): 

477 if ( 

478 ( 

479 compile_state.compile_options._render_for_subquery 

480 and self.parent_property._renders_in_subqueries 

481 ) 

482 or ( 

483 loadopt 

484 and set(self.columns).intersection( 

485 self.parent._should_undefer_in_wildcard 

486 ) 

487 ) 

488 or ( 

489 loadopt 

490 and self.group 

491 and loadopt.local_opts.get( 

492 "undefer_group_%s" % self.group, False 

493 ) 

494 ) 

495 or (only_load_props and self.key in only_load_props) 

496 ): 

497 self.parent_property._get_strategy( 

498 (("deferred", False), ("instrument", True)) 

499 ).setup_query( 

500 compile_state, 

501 query_entity, 

502 path, 

503 loadopt, 

504 adapter, 

505 column_collection, 

506 memoized_populators, 

507 **kw, 

508 ) 

509 elif self.is_class_level: 

510 memoized_populators[self.parent_property] = _SET_DEFERRED_EXPIRED 

511 elif not self.raiseload: 

512 memoized_populators[self.parent_property] = _DEFER_FOR_STATE 

513 else: 

514 memoized_populators[self.parent_property] = _RAISE_FOR_STATE 

515 

516 def _load_for_state(self, state, passive): 

517 if not state.key: 

518 return LoaderCallableStatus.ATTR_EMPTY 

519 

520 if not passive & PassiveFlag.SQL_OK: 

521 return LoaderCallableStatus.PASSIVE_NO_RESULT 

522 

523 localparent = state.manager.mapper 

524 

525 if self.group: 

526 toload = [ 

527 p.key 

528 for p in localparent.iterate_properties 

529 if isinstance(p, StrategizedProperty) 

530 and isinstance(p.strategy, _DeferredColumnLoader) 

531 and p.group == self.group 

532 ] 

533 else: 

534 toload = [self.key] 

535 

536 # narrow the keys down to just those which have no history 

537 group = [k for k in toload if k in state.unmodified] 

538 

539 session = _state_session(state) 

540 if session is None: 

541 raise orm_exc.DetachedInstanceError( 

542 "Parent instance %s is not bound to a Session; " 

543 "deferred load operation of attribute '%s' cannot proceed" 

544 % (orm_util.state_str(state), self.key) 

545 ) 

546 

547 if self.raiseload: 

548 self._invoke_raise_load(state, passive, "raise") 

549 

550 loading._load_scalar_attributes( 

551 state.mapper, state, set(group), PASSIVE_OFF 

552 ) 

553 

554 return LoaderCallableStatus.ATTR_WAS_SET 

555 

556 def _invoke_raise_load(self, state, passive, lazy): 

557 raise sa_exc.InvalidRequestError( 

558 "'%s' is not available due to raiseload=True" % (self,) 

559 ) 

560 

561 

562class _LoadDeferredColumns: 

563 """serializable loader object used by DeferredColumnLoader""" 

564 

565 def __init__(self, key: str, raiseload: bool = False): 

566 self.key = key 

567 self.raiseload = raiseload 

568 

569 def __call__(self, state, passive=attributes.PASSIVE_OFF): 

570 key = self.key 

571 

572 localparent = state.manager.mapper 

573 prop = localparent._props[key] 

574 if self.raiseload: 

575 strategy_key = ( 

576 ("deferred", True), 

577 ("instrument", True), 

578 ("raiseload", True), 

579 ) 

580 else: 

581 strategy_key = (("deferred", True), ("instrument", True)) 

582 strategy = prop._get_strategy(strategy_key) 

583 return strategy._load_for_state(state, passive) 

584 

585 

586class _AbstractRelationshipLoader(LoaderStrategy): 

587 """LoaderStratgies which deal with related objects.""" 

588 

589 __slots__ = "mapper", "target", "uselist", "entity" 

590 

591 def __init__(self, parent, strategy_key): 

592 super().__init__(parent, strategy_key) 

593 self.mapper = self.parent_property.mapper 

594 self.entity = self.parent_property.entity 

595 self.target = self.parent_property.target 

596 self.uselist = self.parent_property.uselist 

597 

598 def _immediateload_create_row_processor( 

599 self, 

600 context, 

601 query_entity, 

602 path, 

603 loadopt, 

604 mapper, 

605 result, 

606 adapter, 

607 populators, 

608 ): 

609 return self.parent_property._get_strategy( 

610 (("lazy", "immediate"),) 

611 ).create_row_processor( 

612 context, 

613 query_entity, 

614 path, 

615 loadopt, 

616 mapper, 

617 result, 

618 adapter, 

619 populators, 

620 ) 

621 

622 

623@log.class_logger 

624@relationships.RelationshipProperty.strategy_for(do_nothing=True) 

625class _DoNothingLoader(LoaderStrategy): 

626 """Relationship loader that makes no change to the object's state. 

627 

628 Compared to NoLoader, this loader does not initialize the 

629 collection/attribute to empty/none; the usual default LazyLoader will 

630 take effect. 

631 

632 """ 

633 

634 

635@log.class_logger 

636@relationships.RelationshipProperty.strategy_for(lazy="noload") 

637@relationships.RelationshipProperty.strategy_for(lazy=None) 

638class _NoLoader(_AbstractRelationshipLoader): 

639 """Provide loading behavior for a :class:`.Relationship` 

640 with "lazy=None". 

641 

642 """ 

643 

644 __slots__ = () 

645 

646 @util.deprecated( 

647 "2.1", 

648 "The ``noload`` loader strategy is deprecated and will be removed " 

649 "in a future release. This option " 

650 "produces incorrect results by returning ``None`` for related " 

651 "items.", 

652 ) 

653 def init_class_attribute(self, mapper): 

654 self.is_class_level = True 

655 

656 _register_attribute( 

657 self.parent_property, 

658 mapper, 

659 useobject=True, 

660 typecallable=self.parent_property.collection_class, 

661 ) 

662 

663 def create_row_processor( 

664 self, 

665 context, 

666 query_entity, 

667 path, 

668 loadopt, 

669 mapper, 

670 result, 

671 adapter, 

672 populators, 

673 ): 

674 def invoke_no_load(state, dict_, row): 

675 if self.uselist: 

676 attributes.init_state_collection(state, dict_, self.key) 

677 else: 

678 dict_[self.key] = None 

679 

680 populators["new"].append((self.key, invoke_no_load)) 

681 

682 

683@log.class_logger 

684@relationships.RelationshipProperty.strategy_for(lazy=True) 

685@relationships.RelationshipProperty.strategy_for(lazy="select") 

686@relationships.RelationshipProperty.strategy_for(lazy="raise") 

687@relationships.RelationshipProperty.strategy_for(lazy="raise_on_sql") 

688@relationships.RelationshipProperty.strategy_for(lazy="baked_select") 

689class _LazyLoader( 

690 _AbstractRelationshipLoader, util.MemoizedSlots, log.Identified 

691): 

692 """Provide loading behavior for a :class:`.Relationship` 

693 with "lazy=True", that is loads when first accessed. 

694 

695 """ 

696 

697 __slots__ = ( 

698 "_lazywhere", 

699 "_rev_lazywhere", 

700 "_lazyload_reverse_option", 

701 "_order_by", 

702 "use_get", 

703 "is_aliased_class", 

704 "_bind_to_col", 

705 "_equated_columns", 

706 "_rev_bind_to_col", 

707 "_rev_equated_columns", 

708 "_simple_lazy_clause", 

709 "_raise_always", 

710 "_raise_on_sql", 

711 ) 

712 

713 _lazywhere: ColumnElement[bool] 

714 _bind_to_col: Dict[str, ColumnElement[Any]] 

715 _rev_lazywhere: ColumnElement[bool] 

716 _rev_bind_to_col: Dict[str, ColumnElement[Any]] 

717 

718 parent_property: RelationshipProperty[Any] 

719 

720 def __init__( 

721 self, parent: RelationshipProperty[Any], strategy_key: Tuple[Any, ...] 

722 ): 

723 super().__init__(parent, strategy_key) 

724 self._raise_always = self.strategy_opts["lazy"] == "raise" 

725 self._raise_on_sql = self.strategy_opts["lazy"] == "raise_on_sql" 

726 

727 self.is_aliased_class = inspect(self.entity).is_aliased_class 

728 

729 join_condition = self.parent_property._join_condition 

730 ( 

731 self._lazywhere, 

732 self._bind_to_col, 

733 self._equated_columns, 

734 ) = join_condition.create_lazy_clause() 

735 

736 ( 

737 self._rev_lazywhere, 

738 self._rev_bind_to_col, 

739 self._rev_equated_columns, 

740 ) = join_condition.create_lazy_clause(reverse_direction=True) 

741 

742 if self.parent_property.order_by: 

743 self._order_by = util.to_list(self.parent_property.order_by) 

744 else: 

745 self._order_by = None 

746 

747 self.logger.info("%s lazy loading clause %s", self, self._lazywhere) 

748 

749 # determine if our "lazywhere" clause is the same as the mapper's 

750 # get() clause. then we can just use mapper.get() 

751 # 

752 # TODO: the "not self.uselist" can be taken out entirely; a m2o 

753 # load that populates for a list (very unusual, but is possible with 

754 # the API) can still set for "None" and the attribute system will 

755 # populate as an empty list. 

756 self.use_get = ( 

757 not self.is_aliased_class 

758 and not self.uselist 

759 and self.entity._get_clause[0].compare( 

760 self._lazywhere, 

761 use_proxies=True, 

762 compare_keys=False, 

763 equivalents=self.mapper._equivalent_columns, 

764 ) 

765 ) 

766 

767 if self.use_get: 

768 for col in list(self._equated_columns): 

769 if col in self.mapper._equivalent_columns: 

770 for c in self.mapper._equivalent_columns[col]: 

771 self._equated_columns[c] = self._equated_columns[col] 

772 

773 self.logger.info( 

774 "%s will use Session.get() to optimize instance loads", self 

775 ) 

776 

777 def init_class_attribute(self, mapper): 

778 self.is_class_level = True 

779 

780 _legacy_inactive_history_style = ( 

781 self.parent_property._legacy_inactive_history_style 

782 ) 

783 

784 if self.parent_property.active_history: 

785 active_history = True 

786 _deferred_history = False 

787 

788 elif ( 

789 self.parent_property.direction is not interfaces.MANYTOONE 

790 or not self.use_get 

791 ): 

792 if _legacy_inactive_history_style: 

793 active_history = True 

794 _deferred_history = False 

795 else: 

796 active_history = False 

797 _deferred_history = True 

798 else: 

799 active_history = _deferred_history = False 

800 

801 _register_attribute( 

802 self.parent_property, 

803 mapper, 

804 useobject=True, 

805 callable_=self._load_for_state, 

806 typecallable=self.parent_property.collection_class, 

807 active_history=active_history, 

808 _deferred_history=_deferred_history, 

809 ) 

810 

811 def _memoized_attr__simple_lazy_clause(self): 

812 lazywhere = self._lazywhere 

813 

814 criterion, bind_to_col = (lazywhere, self._bind_to_col) 

815 

816 params = [] 

817 

818 def visit_bindparam(bindparam): 

819 bindparam.unique = False 

820 

821 visitors.traverse(criterion, {}, {"bindparam": visit_bindparam}) 

822 

823 def visit_bindparam(bindparam): 

824 if bindparam._identifying_key in bind_to_col: 

825 params.append( 

826 ( 

827 bindparam.key, 

828 bind_to_col[bindparam._identifying_key], 

829 None, 

830 ) 

831 ) 

832 elif bindparam.callable is None: 

833 params.append((bindparam.key, None, bindparam.value)) 

834 

835 criterion = visitors.cloned_traverse( 

836 criterion, {}, {"bindparam": visit_bindparam} 

837 ) 

838 

839 return criterion, params 

840 

841 def _generate_lazy_clause(self, state, passive): 

842 criterion, param_keys = self._simple_lazy_clause 

843 

844 if state is None: 

845 return sql_util.adapt_criterion_to_null( 

846 criterion, [key for key, ident, value in param_keys] 

847 ) 

848 

849 mapper = self.parent_property.parent 

850 

851 o = state.obj() # strong ref 

852 dict_ = attributes.instance_dict(o) 

853 

854 if passive & PassiveFlag.INIT_OK: 

855 passive ^= PassiveFlag.INIT_OK 

856 

857 params = {} 

858 for key, ident, value in param_keys: 

859 if ident is not None: 

860 if passive and passive & PassiveFlag.LOAD_AGAINST_COMMITTED: 

861 value = mapper._get_committed_state_attr_by_column( 

862 state, dict_, ident, passive 

863 ) 

864 else: 

865 value = mapper._get_state_attr_by_column( 

866 state, dict_, ident, passive 

867 ) 

868 

869 params[key] = value 

870 

871 return criterion, params 

872 

873 def _invoke_raise_load(self, state, passive, lazy): 

874 raise sa_exc.InvalidRequestError( 

875 "'%s' is not available due to lazy='%s'" % (self, lazy) 

876 ) 

877 

878 def _load_for_state( 

879 self, 

880 state, 

881 passive, 

882 loadopt=None, 

883 extra_criteria=(), 

884 extra_options=(), 

885 alternate_effective_path=None, 

886 execution_options=util.EMPTY_DICT, 

887 ): 

888 if not state.key and ( 

889 ( 

890 not self.parent_property.load_on_pending 

891 and not state._load_pending 

892 ) 

893 or not state.session_id 

894 ): 

895 return LoaderCallableStatus.ATTR_EMPTY 

896 

897 pending = not state.key 

898 primary_key_identity = None 

899 

900 use_get = self.use_get and (not loadopt or not loadopt._extra_criteria) 

901 

902 if (not passive & PassiveFlag.SQL_OK and not use_get) or ( 

903 not passive & attributes.NON_PERSISTENT_OK and pending 

904 ): 

905 return LoaderCallableStatus.PASSIVE_NO_RESULT 

906 

907 if ( 

908 # we were given lazy="raise" 

909 self._raise_always 

910 # the no_raise history-related flag was not passed 

911 and not passive & PassiveFlag.NO_RAISE 

912 and ( 

913 # if we are use_get and related_object_ok is disabled, 

914 # which means we are at most looking in the identity map 

915 # for history purposes or otherwise returning 

916 # PASSIVE_NO_RESULT, don't raise. This is also a 

917 # history-related flag 

918 not use_get 

919 or passive & PassiveFlag.RELATED_OBJECT_OK 

920 ) 

921 ): 

922 self._invoke_raise_load(state, passive, "raise") 

923 

924 session = _state_session(state) 

925 if not session: 

926 if passive & PassiveFlag.NO_RAISE: 

927 return LoaderCallableStatus.PASSIVE_NO_RESULT 

928 

929 raise orm_exc.DetachedInstanceError( 

930 "Parent instance %s is not bound to a Session; " 

931 "lazy load operation of attribute '%s' cannot proceed" 

932 % (orm_util.state_str(state), self.key) 

933 ) 

934 

935 # if we have a simple primary key load, check the 

936 # identity map without generating a Query at all 

937 if use_get: 

938 primary_key_identity = self._get_ident_for_use_get( 

939 session, state, passive 

940 ) 

941 if LoaderCallableStatus.PASSIVE_NO_RESULT in primary_key_identity: 

942 return LoaderCallableStatus.PASSIVE_NO_RESULT 

943 elif LoaderCallableStatus.NEVER_SET in primary_key_identity: 

944 return LoaderCallableStatus.NEVER_SET 

945 

946 # test for None alone in primary_key_identity based on 

947 # allow_partial_pks preference. PASSIVE_NO_RESULT and NEVER_SET 

948 # have already been tested above 

949 if not self.mapper.allow_partial_pks: 

950 if _none_only_set.intersection(primary_key_identity): 

951 return None 

952 else: 

953 if _none_only_set.issuperset(primary_key_identity): 

954 return None 

955 

956 if ( 

957 self.key in state.dict 

958 and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD 

959 ): 

960 return LoaderCallableStatus.ATTR_WAS_SET 

961 

962 # look for this identity in the identity map. Delegate to the 

963 # Query class in use, as it may have special rules for how it 

964 # does this, including how it decides what the correct 

965 # identity_token would be for this identity. 

966 

967 instance = session._identity_lookup( 

968 self.entity, 

969 primary_key_identity, 

970 passive=passive, 

971 lazy_loaded_from=state, 

972 ) 

973 

974 if instance is not None: 

975 if instance is LoaderCallableStatus.PASSIVE_CLASS_MISMATCH: 

976 return None 

977 else: 

978 return instance 

979 elif ( 

980 not passive & PassiveFlag.SQL_OK 

981 or not passive & PassiveFlag.RELATED_OBJECT_OK 

982 ): 

983 return LoaderCallableStatus.PASSIVE_NO_RESULT 

984 

985 return self._emit_lazyload( 

986 session, 

987 state, 

988 primary_key_identity, 

989 passive, 

990 loadopt, 

991 extra_criteria, 

992 extra_options, 

993 alternate_effective_path, 

994 execution_options, 

995 ) 

996 

997 def _get_ident_for_use_get(self, session, state, passive): 

998 instance_mapper = state.manager.mapper 

999 

1000 if passive & PassiveFlag.LOAD_AGAINST_COMMITTED: 

1001 get_attr = instance_mapper._get_committed_state_attr_by_column 

1002 else: 

1003 get_attr = instance_mapper._get_state_attr_by_column 

1004 

1005 dict_ = state.dict 

1006 

1007 return [ 

1008 get_attr(state, dict_, self._equated_columns[pk], passive=passive) 

1009 for pk in self.mapper.primary_key 

1010 ] 

1011 

1012 @util.preload_module("sqlalchemy.orm.strategy_options") 

1013 def _emit_lazyload( 

1014 self, 

1015 session, 

1016 state, 

1017 primary_key_identity, 

1018 passive, 

1019 loadopt, 

1020 extra_criteria, 

1021 extra_options, 

1022 alternate_effective_path, 

1023 execution_options, 

1024 ): 

1025 strategy_options = util.preloaded.orm_strategy_options 

1026 

1027 clauseelement = self.entity.__clause_element__() 

1028 stmt = Select._create_raw_select( 

1029 _raw_columns=[clauseelement], 

1030 _propagate_attrs=clauseelement._propagate_attrs, 

1031 _label_style=LABEL_STYLE_TABLENAME_PLUS_COL, 

1032 _compile_options=_ORMCompileState.default_compile_options, 

1033 ) 

1034 load_options = QueryContext.default_load_options 

1035 

1036 load_options += { 

1037 "_invoke_all_eagers": False, 

1038 "_lazy_loaded_from": state, 

1039 } 

1040 

1041 if self.parent_property.secondary is not None: 

1042 stmt = stmt.select_from( 

1043 self.mapper, self.parent_property.secondary 

1044 ) 

1045 

1046 pending = not state.key 

1047 

1048 # don't autoflush on pending 

1049 if pending or passive & attributes.NO_AUTOFLUSH: 

1050 stmt._execution_options = util.immutabledict({"autoflush": False}) 

1051 

1052 use_get = self.use_get 

1053 

1054 if state.load_options or (loadopt and loadopt._extra_criteria): 

1055 if alternate_effective_path is None: 

1056 effective_path = state.load_path[self.parent_property] 

1057 else: 

1058 effective_path = alternate_effective_path[self.parent_property] 

1059 

1060 opts = state.load_options 

1061 

1062 if loadopt and loadopt._extra_criteria: 

1063 use_get = False 

1064 opts += ( 

1065 orm_util.LoaderCriteriaOption(self.entity, extra_criteria), 

1066 ) 

1067 

1068 stmt._with_options = opts 

1069 elif alternate_effective_path is None: 

1070 # this path is used if there are not already any options 

1071 # in the query, but an event may want to add them 

1072 effective_path = state.mapper._path_registry[self.parent_property] 

1073 else: 

1074 # added by immediateloader 

1075 effective_path = alternate_effective_path[self.parent_property] 

1076 

1077 if extra_options: 

1078 stmt._with_options += extra_options 

1079 

1080 stmt._compile_options += {"_current_path": effective_path} 

1081 

1082 if use_get: 

1083 if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE: 

1084 self._invoke_raise_load(state, passive, "raise_on_sql") 

1085 

1086 return loading._load_on_pk_identity( 

1087 session, 

1088 stmt, 

1089 primary_key_identity, 

1090 load_options=load_options, 

1091 execution_options=execution_options, 

1092 ) 

1093 

1094 if self._order_by: 

1095 stmt._order_by_clauses = self._order_by 

1096 

1097 def _lazyload_reverse(compile_context): 

1098 for rev in self.parent_property._reverse_property: 

1099 # reverse props that are MANYTOONE are loading *this* 

1100 # object from get(), so don't need to eager out to those. 

1101 if ( 

1102 rev.direction is interfaces.MANYTOONE 

1103 and rev._use_get 

1104 and not isinstance(rev.strategy, _LazyLoader) 

1105 ): 

1106 strategy_options.Load._construct_for_existing_path( 

1107 compile_context.compile_options._current_path[ 

1108 rev.parent 

1109 ] 

1110 ).lazyload(rev).process_compile_state(compile_context) 

1111 

1112 stmt = stmt._add_compile_state_func( 

1113 _lazyload_reverse, self.parent_property 

1114 ) 

1115 

1116 lazy_clause, params = self._generate_lazy_clause(state, passive) 

1117 

1118 if execution_options: 

1119 execution_options = util.EMPTY_DICT.merge_with( 

1120 execution_options, 

1121 { 

1122 "_sa_orm_load_options": load_options, 

1123 }, 

1124 ) 

1125 else: 

1126 execution_options = { 

1127 "_sa_orm_load_options": load_options, 

1128 } 

1129 

1130 if ( 

1131 self.key in state.dict 

1132 and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD 

1133 ): 

1134 return LoaderCallableStatus.ATTR_WAS_SET 

1135 

1136 if pending: 

1137 if util.has_intersection(orm_util._none_set, params.values()): 

1138 return None 

1139 

1140 elif util.has_intersection(orm_util._never_set, params.values()): 

1141 return None 

1142 

1143 if self._raise_on_sql and not passive & PassiveFlag.NO_RAISE: 

1144 self._invoke_raise_load(state, passive, "raise_on_sql") 

1145 

1146 stmt._where_criteria = (lazy_clause,) 

1147 

1148 result = session.execute( 

1149 stmt, params, execution_options=execution_options 

1150 ) 

1151 

1152 result = result.unique().scalars().all() 

1153 

1154 if self.uselist: 

1155 return result 

1156 else: 

1157 l = len(result) 

1158 if l: 

1159 if l > 1: 

1160 util.warn( 

1161 "Multiple rows returned with " 

1162 "uselist=False for lazily-loaded attribute '%s' " 

1163 % self.parent_property 

1164 ) 

1165 

1166 return result[0] 

1167 else: 

1168 return None 

1169 

1170 def create_row_processor( 

1171 self, 

1172 context, 

1173 query_entity, 

1174 path, 

1175 loadopt, 

1176 mapper, 

1177 result, 

1178 adapter, 

1179 populators, 

1180 ): 

1181 key = self.key 

1182 

1183 if ( 

1184 context.load_options._is_user_refresh 

1185 and context.query._compile_options._only_load_props 

1186 and self.key in context.query._compile_options._only_load_props 

1187 ): 

1188 return self._immediateload_create_row_processor( 

1189 context, 

1190 query_entity, 

1191 path, 

1192 loadopt, 

1193 mapper, 

1194 result, 

1195 adapter, 

1196 populators, 

1197 ) 

1198 

1199 if not self.is_class_level or (loadopt and loadopt._extra_criteria): 

1200 # we are not the primary manager for this attribute 

1201 # on this class - set up a 

1202 # per-instance lazyloader, which will override the 

1203 # class-level behavior. 

1204 # this currently only happens when using a 

1205 # "lazyload" option on a "no load" 

1206 # attribute - "eager" attributes always have a 

1207 # class-level lazyloader installed. 

1208 set_lazy_callable = ( 

1209 InstanceState._instance_level_callable_processor 

1210 )( 

1211 mapper.class_manager, 

1212 _LoadLazyAttribute( 

1213 key, 

1214 self, 

1215 loadopt, 

1216 ( 

1217 loadopt._generate_extra_criteria(context) 

1218 if loadopt._extra_criteria 

1219 else None 

1220 ), 

1221 ), 

1222 key, 

1223 ) 

1224 

1225 populators["new"].append((self.key, set_lazy_callable)) 

1226 elif context.populate_existing or mapper.always_refresh: 

1227 

1228 def reset_for_lazy_callable(state, dict_, row): 

1229 # we are the primary manager for this attribute on 

1230 # this class - reset its 

1231 # per-instance attribute state, so that the class-level 

1232 # lazy loader is 

1233 # executed when next referenced on this instance. 

1234 # this is needed in 

1235 # populate_existing() types of scenarios to reset 

1236 # any existing state. 

1237 state._reset(dict_, key) 

1238 

1239 populators["new"].append((self.key, reset_for_lazy_callable)) 

1240 

1241 

1242class _LoadLazyAttribute: 

1243 """semi-serializable loader object used by LazyLoader 

1244 

1245 Historically, this object would be carried along with instances that 

1246 needed to run lazyloaders, so it had to be serializable to support 

1247 cached instances. 

1248 

1249 this is no longer a general requirement, and the case where this object 

1250 is used is exactly the case where we can't really serialize easily, 

1251 which is when extra criteria in the loader option is present. 

1252 

1253 We can't reliably serialize that as it refers to mapped entities and 

1254 AliasedClass objects that are local to the current process, which would 

1255 need to be matched up on deserialize e.g. the sqlalchemy.ext.serializer 

1256 approach. 

1257 

1258 """ 

1259 

1260 def __init__(self, key, initiating_strategy, loadopt, extra_criteria): 

1261 self.key = key 

1262 self.strategy_key = initiating_strategy.strategy_key 

1263 self.loadopt = loadopt 

1264 self.extra_criteria = extra_criteria 

1265 

1266 def __getstate__(self): 

1267 if self.extra_criteria is not None: 

1268 util.warn( 

1269 "Can't reliably serialize a lazyload() option that " 

1270 "contains additional criteria; please use eager loading " 

1271 "for this case" 

1272 ) 

1273 return { 

1274 "key": self.key, 

1275 "strategy_key": self.strategy_key, 

1276 "loadopt": self.loadopt, 

1277 "extra_criteria": (), 

1278 } 

1279 

1280 def __call__(self, state, passive=attributes.PASSIVE_OFF): 

1281 key = self.key 

1282 instance_mapper = state.manager.mapper 

1283 prop = instance_mapper._props[key] 

1284 strategy = prop._strategies[self.strategy_key] 

1285 

1286 return strategy._load_for_state( 

1287 state, 

1288 passive, 

1289 loadopt=self.loadopt, 

1290 extra_criteria=self.extra_criteria, 

1291 ) 

1292 

1293 

1294class _PostLoader(_AbstractRelationshipLoader): 

1295 """A relationship loader that emits a second SELECT statement.""" 

1296 

1297 __slots__ = () 

1298 

1299 def _setup_for_recursion(self, context, path, loadopt, join_depth=None): 

1300 effective_path = ( 

1301 context.compile_state.current_path or orm_util.PathRegistry.root 

1302 ) + path 

1303 

1304 top_level_context = context._get_top_level_context() 

1305 execution_options = util.immutabledict( 

1306 {"sa_top_level_orm_context": top_level_context} 

1307 ) 

1308 

1309 if loadopt: 

1310 recursion_depth = loadopt.local_opts.get("recursion_depth", None) 

1311 unlimited_recursion = recursion_depth == -1 

1312 else: 

1313 recursion_depth = None 

1314 unlimited_recursion = False 

1315 

1316 if recursion_depth is not None: 

1317 if not self.parent_property._is_self_referential: 

1318 raise sa_exc.InvalidRequestError( 

1319 f"recursion_depth option on relationship " 

1320 f"{self.parent_property} not valid for " 

1321 "non-self-referential relationship" 

1322 ) 

1323 recursion_depth = context.execution_options.get( 

1324 f"_recursion_depth_{id(self)}", recursion_depth 

1325 ) 

1326 

1327 if not unlimited_recursion and recursion_depth < 0: 

1328 return ( 

1329 effective_path, 

1330 False, 

1331 execution_options, 

1332 recursion_depth, 

1333 ) 

1334 

1335 if not unlimited_recursion: 

1336 execution_options = execution_options.union( 

1337 { 

1338 f"_recursion_depth_{id(self)}": recursion_depth - 1, 

1339 } 

1340 ) 

1341 

1342 if loading._PostLoad.path_exists( 

1343 context, effective_path, self.parent_property 

1344 ): 

1345 return effective_path, False, execution_options, recursion_depth 

1346 

1347 path_w_prop = path[self.parent_property] 

1348 effective_path_w_prop = effective_path[self.parent_property] 

1349 

1350 if not path_w_prop.contains(context.attributes, "loader"): 

1351 if join_depth: 

1352 if effective_path_w_prop.length / 2 > join_depth: 

1353 return ( 

1354 effective_path, 

1355 False, 

1356 execution_options, 

1357 recursion_depth, 

1358 ) 

1359 elif effective_path_w_prop.contains_mapper(self.mapper): 

1360 return ( 

1361 effective_path, 

1362 False, 

1363 execution_options, 

1364 recursion_depth, 

1365 ) 

1366 

1367 return effective_path, True, execution_options, recursion_depth 

1368 

1369 

1370@relationships.RelationshipProperty.strategy_for(lazy="immediate") 

1371class _ImmediateLoader(_PostLoader): 

1372 __slots__ = ("join_depth",) 

1373 

1374 def __init__(self, parent, strategy_key): 

1375 super().__init__(parent, strategy_key) 

1376 self.join_depth = self.parent_property.join_depth 

1377 

1378 def init_class_attribute(self, mapper): 

1379 self.parent_property._get_strategy( 

1380 (("lazy", "select"),) 

1381 ).init_class_attribute(mapper) 

1382 

1383 def create_row_processor( 

1384 self, 

1385 context, 

1386 query_entity, 

1387 path, 

1388 loadopt, 

1389 mapper, 

1390 result, 

1391 adapter, 

1392 populators, 

1393 ): 

1394 if not context.compile_state.compile_options._enable_eagerloads: 

1395 return 

1396 

1397 ( 

1398 effective_path, 

1399 run_loader, 

1400 execution_options, 

1401 recursion_depth, 

1402 ) = self._setup_for_recursion(context, path, loadopt, self.join_depth) 

1403 

1404 if not run_loader: 

1405 # this will not emit SQL and will only emit for a many-to-one 

1406 # "use get" load. the "_RELATED" part means it may return 

1407 # instance even if its expired, since this is a mutually-recursive 

1408 # load operation. 

1409 flags = attributes.PASSIVE_NO_FETCH_RELATED | PassiveFlag.NO_RAISE 

1410 else: 

1411 flags = attributes.PASSIVE_OFF | PassiveFlag.NO_RAISE 

1412 

1413 loading._PostLoad.callable_for_path( 

1414 context, 

1415 effective_path, 

1416 self.parent, 

1417 self.parent_property, 

1418 self._load_for_path, 

1419 loadopt, 

1420 flags, 

1421 recursion_depth, 

1422 execution_options, 

1423 ) 

1424 

1425 def _load_for_path( 

1426 self, 

1427 context, 

1428 path, 

1429 states, 

1430 load_only, 

1431 loadopt, 

1432 flags, 

1433 recursion_depth, 

1434 execution_options, 

1435 ): 

1436 if recursion_depth: 

1437 new_opt = Load(loadopt.path.entity) 

1438 new_opt.context = ( 

1439 loadopt, 

1440 loadopt._recurse(), 

1441 ) 

1442 alternate_effective_path = path._truncate_recursive() 

1443 extra_options = (new_opt,) 

1444 else: 

1445 alternate_effective_path = path 

1446 extra_options = () 

1447 

1448 key = self.key 

1449 lazyloader = self.parent_property._get_strategy((("lazy", "select"),)) 

1450 for state, overwrite in states: 

1451 dict_ = state.dict 

1452 

1453 if overwrite or key not in dict_: 

1454 value = lazyloader._load_for_state( 

1455 state, 

1456 flags, 

1457 extra_options=extra_options, 

1458 alternate_effective_path=alternate_effective_path, 

1459 execution_options=execution_options, 

1460 ) 

1461 if value not in ( 

1462 ATTR_WAS_SET, 

1463 LoaderCallableStatus.PASSIVE_NO_RESULT, 

1464 ): 

1465 state.get_impl(key).set_committed_value( 

1466 state, dict_, value 

1467 ) 

1468 

1469 

1470@log.class_logger 

1471@relationships.RelationshipProperty.strategy_for(lazy="subquery") 

1472class _SubqueryLoader(_PostLoader): 

1473 __slots__ = ("join_depth",) 

1474 

1475 def __init__(self, parent, strategy_key): 

1476 super().__init__(parent, strategy_key) 

1477 self.join_depth = self.parent_property.join_depth 

1478 

1479 def init_class_attribute(self, mapper): 

1480 self.parent_property._get_strategy( 

1481 (("lazy", "select"),) 

1482 ).init_class_attribute(mapper) 

1483 

1484 def _get_leftmost( 

1485 self, 

1486 orig_query_entity_index, 

1487 subq_path, 

1488 current_compile_state, 

1489 is_root, 

1490 ): 

1491 given_subq_path = subq_path 

1492 subq_path = subq_path.path 

1493 subq_mapper = orm_util._class_to_mapper(subq_path[0]) 

1494 

1495 # determine attributes of the leftmost mapper 

1496 if ( 

1497 self.parent.isa(subq_mapper) 

1498 and self.parent_property is subq_path[1] 

1499 ): 

1500 leftmost_mapper, leftmost_prop = self.parent, self.parent_property 

1501 else: 

1502 leftmost_mapper, leftmost_prop = subq_mapper, subq_path[1] 

1503 

1504 if is_root: 

1505 # the subq_path is also coming from cached state, so when we start 

1506 # building up this path, it has to also be converted to be in terms 

1507 # of the current state. this is for the specific case of the entity 

1508 # is an AliasedClass against a subquery that's not otherwise going 

1509 # to adapt 

1510 new_subq_path = current_compile_state._entities[ 

1511 orig_query_entity_index 

1512 ].entity_zero._path_registry[leftmost_prop] 

1513 additional = len(subq_path) - len(new_subq_path) 

1514 if additional: 

1515 new_subq_path += path_registry.PathRegistry.coerce( 

1516 subq_path[-additional:] 

1517 ) 

1518 else: 

1519 new_subq_path = given_subq_path 

1520 

1521 leftmost_cols = leftmost_prop.local_columns 

1522 

1523 leftmost_attr = [ 

1524 getattr( 

1525 new_subq_path.path[0].entity, 

1526 leftmost_mapper._columntoproperty[c].key, 

1527 ) 

1528 for c in leftmost_cols 

1529 ] 

1530 

1531 return leftmost_mapper, leftmost_attr, leftmost_prop, new_subq_path 

1532 

1533 def _generate_from_original_query( 

1534 self, 

1535 orig_compile_state, 

1536 orig_query, 

1537 leftmost_mapper, 

1538 leftmost_attr, 

1539 leftmost_relationship, 

1540 orig_entity, 

1541 ): 

1542 # reformat the original query 

1543 # to look only for significant columns 

1544 q = orig_query._clone().correlate(None) 

1545 

1546 # LEGACY: make a Query back from the select() !! 

1547 # This suits at least two legacy cases: 

1548 # 1. applications which expect before_compile() to be called 

1549 # below when we run .subquery() on this query (Keystone) 

1550 # 2. applications which are doing subqueryload with complex 

1551 # from_self() queries, as query.subquery() / .statement 

1552 # has to do the full compile context for multiply-nested 

1553 # from_self() (Neutron) - see test_subqload_from_self 

1554 # for demo. 

1555 q2 = query.Query.__new__(query.Query) 

1556 q2.__dict__.update(q.__dict__) 

1557 q = q2 

1558 

1559 # set the query's "FROM" list explicitly to what the 

1560 # FROM list would be in any case, as we will be limiting 

1561 # the columns in the SELECT list which may no longer include 

1562 # all entities mentioned in things like WHERE, JOIN, etc. 

1563 if not q._from_obj: 

1564 q._enable_assertions = False 

1565 q.select_from.non_generative( 

1566 q, 

1567 *{ 

1568 ent["entity"] 

1569 for ent in _column_descriptions( 

1570 orig_query, compile_state=orig_compile_state 

1571 ) 

1572 if ent["entity"] is not None 

1573 }, 

1574 ) 

1575 

1576 # select from the identity columns of the outer (specifically, these 

1577 # are the 'local_cols' of the property). This will remove other 

1578 # columns from the query that might suggest the right entity which is 

1579 # why we do set select_from above. The attributes we have are 

1580 # coerced and adapted using the original query's adapter, which is 

1581 # needed only for the case of adapting a subclass column to 

1582 # that of a polymorphic selectable, e.g. we have 

1583 # Engineer.primary_language and the entity is Person. All other 

1584 # adaptations, e.g. from_self, select_entity_from(), will occur 

1585 # within the new query when it compiles, as the compile_state we are 

1586 # using here is only a partial one. If the subqueryload is from a 

1587 # with_polymorphic() or other aliased() object, left_attr will already 

1588 # be the correct attributes so no adaptation is needed. 

1589 target_cols = orig_compile_state._adapt_col_list( 

1590 [ 

1591 sql.coercions.expect(sql.roles.ColumnsClauseRole, o) 

1592 for o in leftmost_attr 

1593 ], 

1594 orig_compile_state._get_current_adapter(), 

1595 ) 

1596 q._raw_columns = target_cols 

1597 

1598 distinct_target_key = leftmost_relationship.distinct_target_key 

1599 

1600 if distinct_target_key is True: 

1601 q._distinct = True 

1602 elif distinct_target_key is None: 

1603 # if target_cols refer to a non-primary key or only 

1604 # part of a composite primary key, set the q as distinct 

1605 for t in {c.table for c in target_cols}: 

1606 if not set(target_cols).issuperset(t.primary_key): 

1607 q._distinct = True 

1608 break 

1609 

1610 # don't need ORDER BY if no limit/offset 

1611 if not q._has_row_limiting_clause: 

1612 q._order_by_clauses = () 

1613 

1614 if q._distinct is True and q._order_by_clauses: 

1615 # the logic to automatically add the order by columns to the query 

1616 # when distinct is True is deprecated in the query 

1617 to_add = sql_util.expand_column_list_from_order_by( 

1618 target_cols, q._order_by_clauses 

1619 ) 

1620 if to_add: 

1621 q._set_entities(target_cols + to_add) 

1622 

1623 # the original query now becomes a subquery 

1624 # which we'll join onto. 

1625 # LEGACY: as "q" is a Query, the before_compile() event is invoked 

1626 # here. 

1627 embed_q = q.set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL).subquery() 

1628 left_alias = orm_util.AliasedClass( 

1629 leftmost_mapper, embed_q, use_mapper_path=True 

1630 ) 

1631 return left_alias 

1632 

1633 def _prep_for_joins(self, left_alias, subq_path): 

1634 # figure out what's being joined. a.k.a. the fun part 

1635 to_join = [] 

1636 pairs = list(subq_path.pairs()) 

1637 

1638 for i, (mapper, prop) in enumerate(pairs): 

1639 if i > 0: 

1640 # look at the previous mapper in the chain - 

1641 # if it is as or more specific than this prop's 

1642 # mapper, use that instead. 

1643 # note we have an assumption here that 

1644 # the non-first element is always going to be a mapper, 

1645 # not an AliasedClass 

1646 

1647 prev_mapper = pairs[i - 1][1].mapper 

1648 to_append = prev_mapper if prev_mapper.isa(mapper) else mapper 

1649 else: 

1650 to_append = mapper 

1651 

1652 to_join.append((to_append, prop.key)) 

1653 

1654 # determine the immediate parent class we are joining from, 

1655 # which needs to be aliased. 

1656 

1657 if len(to_join) < 2: 

1658 # in the case of a one level eager load, this is the 

1659 # leftmost "left_alias". 

1660 parent_alias = left_alias 

1661 else: 

1662 info = inspect(to_join[-1][0]) 

1663 if info.is_aliased_class: 

1664 parent_alias = info.entity 

1665 else: 

1666 # alias a plain mapper as we may be 

1667 # joining multiple times 

1668 parent_alias = orm_util.AliasedClass( 

1669 info.entity, use_mapper_path=True 

1670 ) 

1671 

1672 local_cols = self.parent_property.local_columns 

1673 

1674 local_attr = [ 

1675 getattr(parent_alias, self.parent._columntoproperty[c].key) 

1676 for c in local_cols 

1677 ] 

1678 return to_join, local_attr, parent_alias 

1679 

1680 def _apply_joins( 

1681 self, q, to_join, left_alias, parent_alias, effective_entity 

1682 ): 

1683 ltj = len(to_join) 

1684 if ltj == 1: 

1685 to_join = [ 

1686 getattr(left_alias, to_join[0][1]).of_type(effective_entity) 

1687 ] 

1688 elif ltj == 2: 

1689 to_join = [ 

1690 getattr(left_alias, to_join[0][1]).of_type(parent_alias), 

1691 getattr(parent_alias, to_join[-1][1]).of_type( 

1692 effective_entity 

1693 ), 

1694 ] 

1695 elif ltj > 2: 

1696 middle = [ 

1697 ( 

1698 ( 

1699 orm_util.AliasedClass(item[0]) 

1700 if not inspect(item[0]).is_aliased_class 

1701 else item[0].entity 

1702 ), 

1703 item[1], 

1704 ) 

1705 for item in to_join[1:-1] 

1706 ] 

1707 inner = [] 

1708 

1709 while middle: 

1710 item = middle.pop(0) 

1711 attr = getattr(item[0], item[1]) 

1712 if middle: 

1713 attr = attr.of_type(middle[0][0]) 

1714 else: 

1715 attr = attr.of_type(parent_alias) 

1716 

1717 inner.append(attr) 

1718 

1719 to_join = ( 

1720 [getattr(left_alias, to_join[0][1]).of_type(inner[0].parent)] 

1721 + inner 

1722 + [ 

1723 getattr(parent_alias, to_join[-1][1]).of_type( 

1724 effective_entity 

1725 ) 

1726 ] 

1727 ) 

1728 

1729 for attr in to_join: 

1730 q = q.join(attr) 

1731 

1732 return q 

1733 

1734 def _setup_options( 

1735 self, 

1736 context, 

1737 q, 

1738 subq_path, 

1739 rewritten_path, 

1740 orig_query, 

1741 effective_entity, 

1742 loadopt, 

1743 ): 

1744 # note that because the subqueryload object 

1745 # does not re-use the cached query, instead always making 

1746 # use of the current invoked query, while we have two queries 

1747 # here (orig and context.query), they are both non-cached 

1748 # queries and we can transfer the options as is without 

1749 # adjusting for new criteria. Some work on #6881 / #6889 

1750 # brought this into question. 

1751 new_options = orig_query._with_options 

1752 

1753 if loadopt and loadopt._extra_criteria: 

1754 new_options += ( 

1755 orm_util.LoaderCriteriaOption( 

1756 self.entity, 

1757 loadopt._generate_extra_criteria(context), 

1758 ), 

1759 ) 

1760 

1761 # propagate loader options etc. to the new query. 

1762 # these will fire relative to subq_path. 

1763 q = q._with_current_path(rewritten_path) 

1764 q = q.options(*new_options) 

1765 

1766 return q 

1767 

1768 def _setup_outermost_orderby(self, q): 

1769 if self.parent_property.order_by: 

1770 

1771 def _setup_outermost_orderby(compile_context): 

1772 compile_context.eager_order_by += tuple( 

1773 util.to_list(self.parent_property.order_by) 

1774 ) 

1775 

1776 q = q._add_compile_state_func( 

1777 _setup_outermost_orderby, self.parent_property 

1778 ) 

1779 

1780 return q 

1781 

1782 class _SubqCollections: 

1783 """Given a :class:`_query.Query` used to emit the "subquery load", 

1784 provide a load interface that executes the query at the 

1785 first moment a value is needed. 

1786 

1787 """ 

1788 

1789 __slots__ = ( 

1790 "session", 

1791 "execution_options", 

1792 "load_options", 

1793 "params", 

1794 "subq", 

1795 "_data", 

1796 ) 

1797 

1798 def __init__(self, context, subq): 

1799 # avoid creating a cycle by storing context 

1800 # even though that's preferable 

1801 self.session = context.session 

1802 self.execution_options = context.execution_options 

1803 self.load_options = context.load_options 

1804 self.params = context.params or {} 

1805 self.subq = subq 

1806 self._data = None 

1807 

1808 def get(self, key, default): 

1809 if self._data is None: 

1810 self._load() 

1811 return self._data.get(key, default) 

1812 

1813 def _load(self): 

1814 self._data = collections.defaultdict(list) 

1815 

1816 q = self.subq 

1817 assert q.session is None 

1818 

1819 q = q.with_session(self.session) 

1820 

1821 if self.load_options._populate_existing: 

1822 q = q.populate_existing() 

1823 # to work with baked query, the parameters may have been 

1824 # updated since this query was created, so take these into account 

1825 

1826 rows = list(q.params(self.params)) 

1827 for k, v in itertools.groupby(rows, lambda x: x[1:]): 

1828 self._data[k].extend(vv[0] for vv in v) 

1829 

1830 def loader(self, state, dict_, row): 

1831 if self._data is None: 

1832 self._load() 

1833 

1834 def _setup_query_from_rowproc( 

1835 self, 

1836 context, 

1837 query_entity, 

1838 path, 

1839 entity, 

1840 loadopt, 

1841 adapter, 

1842 ): 

1843 compile_state = context.compile_state 

1844 if ( 

1845 not compile_state.compile_options._enable_eagerloads 

1846 or compile_state.compile_options._for_refresh_state 

1847 ): 

1848 return 

1849 

1850 orig_query_entity_index = compile_state._entities.index(query_entity) 

1851 context.loaders_require_buffering = True 

1852 

1853 path = path[self.parent_property] 

1854 

1855 # build up a path indicating the path from the leftmost 

1856 # entity to the thing we're subquery loading. 

1857 with_poly_entity = path.get( 

1858 compile_state.attributes, "path_with_polymorphic", None 

1859 ) 

1860 if with_poly_entity is not None: 

1861 effective_entity = with_poly_entity 

1862 else: 

1863 effective_entity = self.entity 

1864 

1865 subq_path, rewritten_path = context.query._execution_options.get( 

1866 ("subquery_paths", None), 

1867 (orm_util.PathRegistry.root, orm_util.PathRegistry.root), 

1868 ) 

1869 is_root = subq_path is orm_util.PathRegistry.root 

1870 subq_path = subq_path + path 

1871 rewritten_path = rewritten_path + path 

1872 

1873 # use the current query being invoked, not the compile state 

1874 # one. this is so that we get the current parameters. however, 

1875 # it means we can't use the existing compile state, we have to make 

1876 # a new one. other approaches include possibly using the 

1877 # compiled query but swapping the params, seems only marginally 

1878 # less time spent but more complicated 

1879 orig_query = context.query._execution_options.get( 

1880 ("orig_query", _SubqueryLoader), context.query 

1881 ) 

1882 

1883 # make a new compile_state for the query that's probably cached, but 

1884 # we're sort of undoing a bit of that caching :( 

1885 compile_state_cls = _ORMCompileState._get_plugin_class_for_plugin( 

1886 orig_query, "orm" 

1887 ) 

1888 

1889 if orig_query._is_lambda_element: 

1890 if context.load_options._lazy_loaded_from is None: 

1891 util.warn( 

1892 'subqueryloader for "%s" must invoke lambda callable ' 

1893 "at %r in " 

1894 "order to produce a new query, decreasing the efficiency " 

1895 "of caching for this statement. Consider using " 

1896 "selectinload() for more effective full-lambda caching" 

1897 % (self, orig_query) 

1898 ) 

1899 orig_query = orig_query._resolved 

1900 

1901 # this is the more "quick" version, however it's not clear how 

1902 # much of this we need. in particular I can't get a test to 

1903 # fail if the "set_base_alias" is missing and not sure why that is. 

1904 orig_compile_state = compile_state_cls._create_entities_collection( 

1905 orig_query, legacy=False 

1906 ) 

1907 

1908 ( 

1909 leftmost_mapper, 

1910 leftmost_attr, 

1911 leftmost_relationship, 

1912 rewritten_path, 

1913 ) = self._get_leftmost( 

1914 orig_query_entity_index, 

1915 rewritten_path, 

1916 orig_compile_state, 

1917 is_root, 

1918 ) 

1919 

1920 # generate a new Query from the original, then 

1921 # produce a subquery from it. 

1922 left_alias = self._generate_from_original_query( 

1923 orig_compile_state, 

1924 orig_query, 

1925 leftmost_mapper, 

1926 leftmost_attr, 

1927 leftmost_relationship, 

1928 entity, 

1929 ) 

1930 

1931 # generate another Query that will join the 

1932 # left alias to the target relationships. 

1933 # basically doing a longhand 

1934 # "from_self()". (from_self() itself not quite industrial 

1935 # strength enough for all contingencies...but very close) 

1936 

1937 q = query.Query(effective_entity) 

1938 

1939 q._execution_options = context.query._execution_options.merge_with( 

1940 context.execution_options, 

1941 { 

1942 ("orig_query", _SubqueryLoader): orig_query, 

1943 ("subquery_paths", None): (subq_path, rewritten_path), 

1944 }, 

1945 ) 

1946 

1947 q = q._set_enable_single_crit(False) 

1948 to_join, local_attr, parent_alias = self._prep_for_joins( 

1949 left_alias, subq_path 

1950 ) 

1951 

1952 q = q.add_columns(*local_attr) 

1953 q = self._apply_joins( 

1954 q, to_join, left_alias, parent_alias, effective_entity 

1955 ) 

1956 

1957 q = self._setup_options( 

1958 context, 

1959 q, 

1960 subq_path, 

1961 rewritten_path, 

1962 orig_query, 

1963 effective_entity, 

1964 loadopt, 

1965 ) 

1966 q = self._setup_outermost_orderby(q) 

1967 

1968 return q 

1969 

1970 def create_row_processor( 

1971 self, 

1972 context, 

1973 query_entity, 

1974 path, 

1975 loadopt, 

1976 mapper, 

1977 result, 

1978 adapter, 

1979 populators, 

1980 ): 

1981 if ( 

1982 loadopt 

1983 and context.compile_state.statement is not None 

1984 and context.compile_state.statement.is_dml 

1985 ): 

1986 util.warn_deprecated( 

1987 "The subqueryload loader option is not compatible with DML " 

1988 "statements such as INSERT, UPDATE. Only SELECT may be used." 

1989 "This warning will become an exception in a future release.", 

1990 "2.0", 

1991 ) 

1992 

1993 if context.refresh_state: 

1994 return self._immediateload_create_row_processor( 

1995 context, 

1996 query_entity, 

1997 path, 

1998 loadopt, 

1999 mapper, 

2000 result, 

2001 adapter, 

2002 populators, 

2003 ) 

2004 

2005 _, run_loader, _, _ = self._setup_for_recursion( 

2006 context, path, loadopt, self.join_depth 

2007 ) 

2008 if not run_loader: 

2009 return 

2010 

2011 if not isinstance(context.compile_state, _ORMSelectCompileState): 

2012 # issue 7505 - subqueryload() in 1.3 and previous would silently 

2013 # degrade for from_statement() without warning. this behavior 

2014 # is restored here 

2015 return 

2016 

2017 if not self.parent.class_manager[self.key].impl.supports_population: 

2018 raise sa_exc.InvalidRequestError( 

2019 "'%s' does not support object " 

2020 "population - eager loading cannot be applied." % self 

2021 ) 

2022 

2023 # a little dance here as the "path" is still something that only 

2024 # semi-tracks the exact series of things we are loading, still not 

2025 # telling us about with_polymorphic() and stuff like that when it's at 

2026 # the root.. the initial MapperEntity is more accurate for this case. 

2027 if len(path) == 1: 

2028 if not orm_util._entity_isa(query_entity.entity_zero, self.parent): 

2029 return 

2030 elif not orm_util._entity_isa(path[-1], self.parent): 

2031 return 

2032 

2033 subq = self._setup_query_from_rowproc( 

2034 context, 

2035 query_entity, 

2036 path, 

2037 path[-1], 

2038 loadopt, 

2039 adapter, 

2040 ) 

2041 

2042 if subq is None: 

2043 return 

2044 

2045 assert subq.session is None 

2046 

2047 path = path[self.parent_property] 

2048 

2049 local_cols = self.parent_property.local_columns 

2050 

2051 # cache the loaded collections in the context 

2052 # so that inheriting mappers don't re-load when they 

2053 # call upon create_row_processor again 

2054 collections = path.get(context.attributes, "collections") 

2055 if collections is None: 

2056 collections = self._SubqCollections(context, subq) 

2057 path.set(context.attributes, "collections", collections) 

2058 

2059 if adapter: 

2060 local_cols = [adapter.columns[c] for c in local_cols] 

2061 

2062 if self.uselist: 

2063 self._create_collection_loader( 

2064 context, result, collections, local_cols, populators 

2065 ) 

2066 else: 

2067 self._create_scalar_loader( 

2068 context, result, collections, local_cols, populators 

2069 ) 

2070 

2071 def _create_collection_loader( 

2072 self, context, result, collections, local_cols, populators 

2073 ): 

2074 tuple_getter = result._tuple_getter(local_cols) 

2075 

2076 def load_collection_from_subq(state, dict_, row): 

2077 collection = collections.get(tuple_getter(row), ()) 

2078 state.get_impl(self.key).set_committed_value( 

2079 state, dict_, collection 

2080 ) 

2081 

2082 def load_collection_from_subq_existing_row(state, dict_, row): 

2083 if self.key not in dict_: 

2084 load_collection_from_subq(state, dict_, row) 

2085 

2086 populators["new"].append((self.key, load_collection_from_subq)) 

2087 populators["existing"].append( 

2088 (self.key, load_collection_from_subq_existing_row) 

2089 ) 

2090 

2091 if context.invoke_all_eagers: 

2092 populators["eager"].append((self.key, collections.loader)) 

2093 

2094 def _create_scalar_loader( 

2095 self, context, result, collections, local_cols, populators 

2096 ): 

2097 tuple_getter = result._tuple_getter(local_cols) 

2098 

2099 def load_scalar_from_subq(state, dict_, row): 

2100 collection = collections.get(tuple_getter(row), (None,)) 

2101 if len(collection) > 1: 

2102 util.warn( 

2103 "Multiple rows returned with " 

2104 "uselist=False for eagerly-loaded attribute '%s' " % self 

2105 ) 

2106 

2107 scalar = collection[0] 

2108 state.get_impl(self.key).set_committed_value(state, dict_, scalar) 

2109 

2110 def load_scalar_from_subq_existing_row(state, dict_, row): 

2111 if self.key not in dict_: 

2112 load_scalar_from_subq(state, dict_, row) 

2113 

2114 populators["new"].append((self.key, load_scalar_from_subq)) 

2115 populators["existing"].append( 

2116 (self.key, load_scalar_from_subq_existing_row) 

2117 ) 

2118 if context.invoke_all_eagers: 

2119 populators["eager"].append((self.key, collections.loader)) 

2120 

2121 

2122@log.class_logger 

2123@relationships.RelationshipProperty.strategy_for(lazy="joined") 

2124@relationships.RelationshipProperty.strategy_for(lazy=False) 

2125class _JoinedLoader(_AbstractRelationshipLoader): 

2126 """Provide loading behavior for a :class:`.Relationship` 

2127 using joined eager loading. 

2128 

2129 """ 

2130 

2131 __slots__ = "join_depth" 

2132 

2133 def __init__(self, parent, strategy_key): 

2134 super().__init__(parent, strategy_key) 

2135 self.join_depth = self.parent_property.join_depth 

2136 

2137 def init_class_attribute(self, mapper): 

2138 self.parent_property._get_strategy( 

2139 (("lazy", "select"),) 

2140 ).init_class_attribute(mapper) 

2141 

2142 def setup_query( 

2143 self, 

2144 compile_state, 

2145 query_entity, 

2146 path, 

2147 loadopt, 

2148 adapter, 

2149 column_collection=None, 

2150 parentmapper=None, 

2151 chained_from_outerjoin=False, 

2152 **kwargs, 

2153 ): 

2154 """Add a left outer join to the statement that's being constructed.""" 

2155 

2156 if not compile_state.compile_options._enable_eagerloads: 

2157 return 

2158 elif ( 

2159 loadopt 

2160 and compile_state.statement is not None 

2161 and compile_state.statement.is_dml 

2162 ): 

2163 util.warn_deprecated( 

2164 "The joinedload loader option is not compatible with DML " 

2165 "statements such as INSERT, UPDATE. Only SELECT may be used." 

2166 "This warning will become an exception in a future release.", 

2167 "2.0", 

2168 ) 

2169 elif self.uselist: 

2170 compile_state.multi_row_eager_loaders = True 

2171 

2172 path = path[self.parent_property] 

2173 

2174 user_defined_adapter = ( 

2175 self._init_user_defined_eager_proc( 

2176 loadopt, compile_state, compile_state.attributes 

2177 ) 

2178 if loadopt 

2179 else False 

2180 ) 

2181 

2182 if user_defined_adapter is not False: 

2183 # setup an adapter but dont create any JOIN, assume it's already 

2184 # in the query 

2185 ( 

2186 clauses, 

2187 adapter, 

2188 add_to_collection, 

2189 ) = self._setup_query_on_user_defined_adapter( 

2190 compile_state, 

2191 query_entity, 

2192 path, 

2193 adapter, 

2194 user_defined_adapter, 

2195 ) 

2196 

2197 # don't do "wrap" for multi-row, we want to wrap 

2198 # limited/distinct SELECT, 

2199 # because we want to put the JOIN on the outside. 

2200 

2201 else: 

2202 # if not via query option, check for 

2203 # a cycle 

2204 if not path.contains(compile_state.attributes, "loader"): 

2205 if self.join_depth: 

2206 if path.length / 2 > self.join_depth: 

2207 return 

2208 elif path.contains_mapper(self.mapper): 

2209 return 

2210 

2211 # add the JOIN and create an adapter 

2212 ( 

2213 clauses, 

2214 adapter, 

2215 add_to_collection, 

2216 chained_from_outerjoin, 

2217 ) = self._generate_row_adapter( 

2218 compile_state, 

2219 query_entity, 

2220 path, 

2221 loadopt, 

2222 adapter, 

2223 column_collection, 

2224 parentmapper, 

2225 chained_from_outerjoin, 

2226 ) 

2227 

2228 # for multi-row, we want to wrap limited/distinct SELECT, 

2229 # because we want to put the JOIN on the outside. 

2230 compile_state.eager_adding_joins = True 

2231 

2232 with_poly_entity = path.get( 

2233 compile_state.attributes, "path_with_polymorphic", None 

2234 ) 

2235 if with_poly_entity is not None: 

2236 with_polymorphic = inspect( 

2237 with_poly_entity 

2238 ).with_polymorphic_mappers 

2239 else: 

2240 with_polymorphic = None 

2241 

2242 path = path[self.entity] 

2243 

2244 loading._setup_entity_query( 

2245 compile_state, 

2246 self.mapper, 

2247 query_entity, 

2248 path, 

2249 clauses, 

2250 add_to_collection, 

2251 with_polymorphic=with_polymorphic, 

2252 parentmapper=self.mapper, 

2253 chained_from_outerjoin=chained_from_outerjoin, 

2254 ) 

2255 

2256 has_nones = util.NONE_SET.intersection(compile_state.secondary_columns) 

2257 

2258 if has_nones: 

2259 if with_poly_entity is not None: 

2260 raise sa_exc.InvalidRequestError( 

2261 "Detected unaliased columns when generating joined " 

2262 "load. Make sure to use aliased=True or flat=True " 

2263 "when using joined loading with with_polymorphic()." 

2264 ) 

2265 else: 

2266 compile_state.secondary_columns = [ 

2267 c for c in compile_state.secondary_columns if c is not None 

2268 ] 

2269 

2270 def _init_user_defined_eager_proc( 

2271 self, loadopt, compile_state, target_attributes 

2272 ): 

2273 # check if the opt applies at all 

2274 if "eager_from_alias" not in loadopt.local_opts: 

2275 # nope 

2276 return False 

2277 

2278 path = loadopt.path.parent 

2279 

2280 # the option applies. check if the "user_defined_eager_row_processor" 

2281 # has been built up. 

2282 adapter = path.get( 

2283 compile_state.attributes, "user_defined_eager_row_processor", False 

2284 ) 

2285 if adapter is not False: 

2286 # just return it 

2287 return adapter 

2288 

2289 # otherwise figure it out. 

2290 alias = loadopt.local_opts["eager_from_alias"] 

2291 root_mapper, prop = path[-2:] 

2292 

2293 if alias is not None: 

2294 if isinstance(alias, str): 

2295 alias = prop.target.alias(alias) 

2296 adapter = orm_util.ORMAdapter( 

2297 orm_util._TraceAdaptRole.JOINEDLOAD_USER_DEFINED_ALIAS, 

2298 prop.mapper, 

2299 selectable=alias, 

2300 equivalents=prop.mapper._equivalent_columns, 

2301 limit_on_entity=False, 

2302 ) 

2303 else: 

2304 if path.contains( 

2305 compile_state.attributes, "path_with_polymorphic" 

2306 ): 

2307 with_poly_entity = path.get( 

2308 compile_state.attributes, "path_with_polymorphic" 

2309 ) 

2310 adapter = orm_util.ORMAdapter( 

2311 orm_util._TraceAdaptRole.JOINEDLOAD_PATH_WITH_POLYMORPHIC, 

2312 with_poly_entity, 

2313 equivalents=prop.mapper._equivalent_columns, 

2314 ) 

2315 else: 

2316 adapter = compile_state._polymorphic_adapters.get( 

2317 prop.mapper, None 

2318 ) 

2319 path.set( 

2320 target_attributes, 

2321 "user_defined_eager_row_processor", 

2322 adapter, 

2323 ) 

2324 

2325 return adapter 

2326 

2327 def _setup_query_on_user_defined_adapter( 

2328 self, context, entity, path, adapter, user_defined_adapter 

2329 ): 

2330 # apply some more wrapping to the "user defined adapter" 

2331 # if we are setting up the query for SQL render. 

2332 adapter = entity._get_entity_clauses(context) 

2333 

2334 if adapter and user_defined_adapter: 

2335 user_defined_adapter = user_defined_adapter.wrap(adapter) 

2336 path.set( 

2337 context.attributes, 

2338 "user_defined_eager_row_processor", 

2339 user_defined_adapter, 

2340 ) 

2341 elif adapter: 

2342 user_defined_adapter = adapter 

2343 path.set( 

2344 context.attributes, 

2345 "user_defined_eager_row_processor", 

2346 user_defined_adapter, 

2347 ) 

2348 

2349 add_to_collection = context.primary_columns 

2350 return user_defined_adapter, adapter, add_to_collection 

2351 

2352 def _generate_row_adapter( 

2353 self, 

2354 compile_state, 

2355 entity, 

2356 path, 

2357 loadopt, 

2358 adapter, 

2359 column_collection, 

2360 parentmapper, 

2361 chained_from_outerjoin, 

2362 ): 

2363 with_poly_entity = path.get( 

2364 compile_state.attributes, "path_with_polymorphic", None 

2365 ) 

2366 if with_poly_entity: 

2367 to_adapt = with_poly_entity 

2368 else: 

2369 insp = inspect(self.entity) 

2370 if insp.is_aliased_class: 

2371 alt_selectable = insp.selectable 

2372 else: 

2373 alt_selectable = None 

2374 

2375 to_adapt = orm_util.AliasedClass( 

2376 self.mapper, 

2377 alias=( 

2378 alt_selectable._anonymous_fromclause(flat=True) 

2379 if alt_selectable is not None 

2380 else None 

2381 ), 

2382 flat=True, 

2383 use_mapper_path=True, 

2384 ) 

2385 

2386 to_adapt_insp = inspect(to_adapt) 

2387 

2388 clauses = to_adapt_insp._memo( 

2389 ("joinedloader_ormadapter", self), 

2390 orm_util.ORMAdapter, 

2391 orm_util._TraceAdaptRole.JOINEDLOAD_MEMOIZED_ADAPTER, 

2392 to_adapt_insp, 

2393 equivalents=self.mapper._equivalent_columns, 

2394 adapt_required=True, 

2395 allow_label_resolve=False, 

2396 anonymize_labels=True, 

2397 ) 

2398 

2399 assert clauses.is_aliased_class 

2400 

2401 innerjoin = ( 

2402 loadopt.local_opts.get("innerjoin", self.parent_property.innerjoin) 

2403 if loadopt is not None 

2404 else self.parent_property.innerjoin 

2405 ) 

2406 

2407 if not innerjoin: 

2408 # if this is an outer join, all non-nested eager joins from 

2409 # this path must also be outer joins 

2410 chained_from_outerjoin = True 

2411 

2412 compile_state.create_eager_joins.append( 

2413 ( 

2414 self._create_eager_join, 

2415 entity, 

2416 path, 

2417 adapter, 

2418 parentmapper, 

2419 clauses, 

2420 innerjoin, 

2421 chained_from_outerjoin, 

2422 loadopt._extra_criteria if loadopt else (), 

2423 ) 

2424 ) 

2425 

2426 add_to_collection = compile_state.secondary_columns 

2427 path.set(compile_state.attributes, "eager_row_processor", clauses) 

2428 

2429 return clauses, adapter, add_to_collection, chained_from_outerjoin 

2430 

2431 def _create_eager_join( 

2432 self, 

2433 compile_state, 

2434 query_entity, 

2435 path, 

2436 adapter, 

2437 parentmapper, 

2438 clauses, 

2439 innerjoin, 

2440 chained_from_outerjoin, 

2441 extra_criteria, 

2442 ): 

2443 if parentmapper is None: 

2444 localparent = query_entity.mapper 

2445 else: 

2446 localparent = parentmapper 

2447 

2448 # whether or not the Query will wrap the selectable in a subquery, 

2449 # and then attach eager load joins to that (i.e., in the case of 

2450 # LIMIT/OFFSET etc.) 

2451 should_nest_selectable = compile_state._should_nest_selectable 

2452 

2453 query_entity_key = None 

2454 

2455 if ( 

2456 query_entity not in compile_state.eager_joins 

2457 and not should_nest_selectable 

2458 and compile_state.from_clauses 

2459 ): 

2460 indexes = sql_util.find_left_clause_that_matches_given( 

2461 compile_state.from_clauses, query_entity.selectable 

2462 ) 

2463 

2464 if len(indexes) > 1: 

2465 # for the eager load case, I can't reproduce this right 

2466 # now. For query.join() I can. 

2467 raise sa_exc.InvalidRequestError( 

2468 "Can't identify which query entity in which to joined " 

2469 "eager load from. Please use an exact match when " 

2470 "specifying the join path." 

2471 ) 

2472 

2473 if indexes: 

2474 clause = compile_state.from_clauses[indexes[0]] 

2475 # join to an existing FROM clause on the query. 

2476 # key it to its list index in the eager_joins dict. 

2477 # Query._compile_context will adapt as needed and 

2478 # append to the FROM clause of the select(). 

2479 query_entity_key, default_towrap = indexes[0], clause 

2480 

2481 if query_entity_key is None: 

2482 query_entity_key, default_towrap = ( 

2483 query_entity, 

2484 query_entity.selectable, 

2485 ) 

2486 

2487 towrap = compile_state.eager_joins.setdefault( 

2488 query_entity_key, default_towrap 

2489 ) 

2490 

2491 if adapter: 

2492 if getattr(adapter, "is_aliased_class", False): 

2493 # joining from an adapted entity. The adapted entity 

2494 # might be a "with_polymorphic", so resolve that to our 

2495 # specific mapper's entity before looking for our attribute 

2496 # name on it. 

2497 efm = adapter.aliased_insp._entity_for_mapper( 

2498 localparent 

2499 if localparent.isa(self.parent) 

2500 else self.parent 

2501 ) 

2502 

2503 # look for our attribute on the adapted entity, else fall back 

2504 # to our straight property 

2505 onclause = getattr(efm.entity, self.key, self.parent_property) 

2506 else: 

2507 onclause = getattr( 

2508 orm_util.AliasedClass( 

2509 self.parent, adapter.selectable, use_mapper_path=True 

2510 ), 

2511 self.key, 

2512 self.parent_property, 

2513 ) 

2514 

2515 else: 

2516 onclause = self.parent_property 

2517 

2518 assert clauses.is_aliased_class 

2519 

2520 attach_on_outside = ( 

2521 not chained_from_outerjoin 

2522 or not innerjoin 

2523 or innerjoin == "unnested" 

2524 or query_entity.entity_zero.represents_outer_join 

2525 ) 

2526 

2527 extra_join_criteria = extra_criteria 

2528 additional_entity_criteria = compile_state.global_attributes.get( 

2529 ("additional_entity_criteria", self.mapper), () 

2530 ) 

2531 if additional_entity_criteria: 

2532 extra_join_criteria += tuple( 

2533 ae._resolve_where_criteria(self.mapper) 

2534 for ae in additional_entity_criteria 

2535 if ae.propagate_to_loaders 

2536 ) 

2537 

2538 if attach_on_outside: 

2539 # this is the "classic" eager join case. 

2540 eagerjoin = orm_util._ORMJoin( 

2541 towrap, 

2542 clauses.aliased_insp, 

2543 onclause, 

2544 isouter=not innerjoin 

2545 or query_entity.entity_zero.represents_outer_join 

2546 or (chained_from_outerjoin and isinstance(towrap, sql.Join)), 

2547 _left_memo=self.parent, 

2548 _right_memo=path[self.mapper], 

2549 _extra_criteria=extra_join_criteria, 

2550 ) 

2551 else: 

2552 # all other cases are innerjoin=='nested' approach 

2553 eagerjoin = self._splice_nested_inner_join( 

2554 path, path[-2], towrap, clauses, onclause, extra_join_criteria 

2555 ) 

2556 

2557 compile_state.eager_joins[query_entity_key] = eagerjoin 

2558 

2559 # send a hint to the Query as to where it may "splice" this join 

2560 eagerjoin.stop_on = query_entity.selectable 

2561 

2562 if not parentmapper: 

2563 # for parentclause that is the non-eager end of the join, 

2564 # ensure all the parent cols in the primaryjoin are actually 

2565 # in the 

2566 # columns clause (i.e. are not deferred), so that aliasing applied 

2567 # by the Query propagates those columns outward. 

2568 # This has the effect 

2569 # of "undefering" those columns. 

2570 for col in sql_util._find_columns( 

2571 self.parent_property.primaryjoin 

2572 ): 

2573 if localparent.persist_selectable.c.contains_column(col): 

2574 if adapter: 

2575 col = adapter.columns[col] 

2576 compile_state._append_dedupe_col_collection( 

2577 col, compile_state.primary_columns 

2578 ) 

2579 

2580 if self.parent_property.order_by: 

2581 compile_state.eager_order_by += tuple( 

2582 (eagerjoin._target_adapter.copy_and_process)( 

2583 util.to_list(self.parent_property.order_by) 

2584 ) 

2585 ) 

2586 

2587 def _splice_nested_inner_join( 

2588 self, 

2589 path, 

2590 entity_we_want_to_splice_onto, 

2591 join_obj, 

2592 clauses, 

2593 onclause, 

2594 extra_criteria, 

2595 entity_inside_join_structure: Union[ 

2596 Mapper, None, Literal[False] 

2597 ] = False, 

2598 detected_existing_path: Optional[path_registry.PathRegistry] = None, 

2599 ): 

2600 # recursive fn to splice a nested join into an existing one. 

2601 # entity_inside_join_structure=False means this is the outermost call, 

2602 # and it should return a value. entity_inside_join_structure=<mapper> 

2603 # indicates we've descended into a join and are looking at a FROM 

2604 # clause representing this mapper; if this is not 

2605 # entity_we_want_to_splice_onto then return None to end the recursive 

2606 # branch 

2607 

2608 assert entity_we_want_to_splice_onto is path[-2] 

2609 

2610 if entity_inside_join_structure is False: 

2611 assert isinstance(join_obj, orm_util._ORMJoin) 

2612 

2613 if isinstance(join_obj, sql.selectable.FromGrouping): 

2614 # FromGrouping - continue descending into the structure 

2615 return self._splice_nested_inner_join( 

2616 path, 

2617 entity_we_want_to_splice_onto, 

2618 join_obj.element, 

2619 clauses, 

2620 onclause, 

2621 extra_criteria, 

2622 entity_inside_join_structure, 

2623 ) 

2624 elif isinstance(join_obj, orm_util._ORMJoin): 

2625 # _ORMJoin - continue descending into the structure 

2626 

2627 join_right_path = join_obj._right_memo 

2628 

2629 # see if right side of join is viable 

2630 target_join = self._splice_nested_inner_join( 

2631 path, 

2632 entity_we_want_to_splice_onto, 

2633 join_obj.right, 

2634 clauses, 

2635 onclause, 

2636 extra_criteria, 

2637 entity_inside_join_structure=( 

2638 join_right_path[-1].mapper 

2639 if join_right_path is not None 

2640 else None 

2641 ), 

2642 ) 

2643 

2644 if target_join is not None: 

2645 # for a right splice, attempt to flatten out 

2646 # a JOIN b JOIN c JOIN .. to avoid needless 

2647 # parenthesis nesting 

2648 if not join_obj.isouter and not target_join.isouter: 

2649 eagerjoin = join_obj._splice_into_center(target_join) 

2650 else: 

2651 eagerjoin = orm_util._ORMJoin( 

2652 join_obj.left, 

2653 target_join, 

2654 join_obj.onclause, 

2655 isouter=join_obj.isouter, 

2656 _left_memo=join_obj._left_memo, 

2657 ) 

2658 

2659 eagerjoin._target_adapter = target_join._target_adapter 

2660 return eagerjoin 

2661 

2662 else: 

2663 # see if left side of join is viable 

2664 target_join = self._splice_nested_inner_join( 

2665 path, 

2666 entity_we_want_to_splice_onto, 

2667 join_obj.left, 

2668 clauses, 

2669 onclause, 

2670 extra_criteria, 

2671 entity_inside_join_structure=join_obj._left_memo, 

2672 detected_existing_path=join_right_path, 

2673 ) 

2674 

2675 if target_join is not None: 

2676 eagerjoin = orm_util._ORMJoin( 

2677 target_join, 

2678 join_obj.right, 

2679 join_obj.onclause, 

2680 isouter=join_obj.isouter, 

2681 _right_memo=join_obj._right_memo, 

2682 ) 

2683 eagerjoin._target_adapter = target_join._target_adapter 

2684 return eagerjoin 

2685 

2686 # neither side viable, return None, or fail if this was the top 

2687 # most call 

2688 if entity_inside_join_structure is False: 

2689 assert ( 

2690 False 

2691 ), "assertion failed attempting to produce joined eager loads" 

2692 return None 

2693 

2694 # reached an endpoint (e.g. a table that's mapped, or an alias of that 

2695 # table). determine if we can use this endpoint to splice onto 

2696 

2697 # is this the entity we want to splice onto in the first place? 

2698 if not entity_we_want_to_splice_onto.isa(entity_inside_join_structure): 

2699 return None 

2700 

2701 # path check. if we know the path how this join endpoint got here, 

2702 # lets look at our path we are satisfying and see if we're in the 

2703 # wrong place. This is specifically for when our entity may 

2704 # appear more than once in the path, issue #11449 

2705 # updated in issue #11965. 

2706 if detected_existing_path and len(detected_existing_path) > 2: 

2707 # this assertion is currently based on how this call is made, 

2708 # where given a join_obj, the call will have these parameters as 

2709 # entity_inside_join_structure=join_obj._left_memo 

2710 # and entity_inside_join_structure=join_obj._right_memo.mapper 

2711 assert detected_existing_path[-3] is entity_inside_join_structure 

2712 

2713 # from that, see if the path we are targeting matches the 

2714 # "existing" path of this join all the way up to the midpoint 

2715 # of this join object (e.g. the relationship). 

2716 # if not, then this is not our target 

2717 # 

2718 # a test condition where this test is false looks like: 

2719 # 

2720 # desired splice: Node->kind->Kind 

2721 # path of desired splice: NodeGroup->nodes->Node->kind 

2722 # path we've located: NodeGroup->nodes->Node->common_node->Node 

2723 # 

2724 # above, because we want to splice kind->Kind onto 

2725 # NodeGroup->nodes->Node, this is not our path because it actually 

2726 # goes more steps than we want into self-referential 

2727 # ->common_node->Node 

2728 # 

2729 # a test condition where this test is true looks like: 

2730 # 

2731 # desired splice: B->c2s->C2 

2732 # path of desired splice: A->bs->B->c2s 

2733 # path we've located: A->bs->B->c1s->C1 

2734 # 

2735 # above, we want to splice c2s->C2 onto B, and the located path 

2736 # shows that the join ends with B->c1s->C1. so we will 

2737 # add another join onto that, which would create a "branch" that 

2738 # we might represent in a pseudopath as: 

2739 # 

2740 # B->c1s->C1 

2741 # ->c2s->C2 

2742 # 

2743 # i.e. A JOIN B ON <bs> JOIN C1 ON <c1s> 

2744 # JOIN C2 ON <c2s> 

2745 # 

2746 

2747 if detected_existing_path[0:-2] != path.path[0:-1]: 

2748 return None 

2749 

2750 return orm_util._ORMJoin( 

2751 join_obj, 

2752 clauses.aliased_insp, 

2753 onclause, 

2754 isouter=False, 

2755 _left_memo=entity_inside_join_structure, 

2756 _right_memo=path[path[-1].mapper], 

2757 _extra_criteria=extra_criteria, 

2758 ) 

2759 

2760 def _create_eager_adapter(self, context, result, adapter, path, loadopt): 

2761 compile_state = context.compile_state 

2762 

2763 user_defined_adapter = ( 

2764 self._init_user_defined_eager_proc( 

2765 loadopt, compile_state, context.attributes 

2766 ) 

2767 if loadopt 

2768 else False 

2769 ) 

2770 

2771 if user_defined_adapter is not False: 

2772 decorator = user_defined_adapter 

2773 # user defined eagerloads are part of the "primary" 

2774 # portion of the load. 

2775 # the adapters applied to the Query should be honored. 

2776 if compile_state.compound_eager_adapter and decorator: 

2777 decorator = decorator.wrap( 

2778 compile_state.compound_eager_adapter 

2779 ) 

2780 elif compile_state.compound_eager_adapter: 

2781 decorator = compile_state.compound_eager_adapter 

2782 else: 

2783 decorator = path.get( 

2784 compile_state.attributes, "eager_row_processor" 

2785 ) 

2786 if decorator is None: 

2787 return False 

2788 

2789 if self.mapper._result_has_identity_key(result, decorator): 

2790 return decorator 

2791 else: 

2792 # no identity key - don't return a row 

2793 # processor, will cause a degrade to lazy 

2794 return False 

2795 

2796 def create_row_processor( 

2797 self, 

2798 context, 

2799 query_entity, 

2800 path, 

2801 loadopt, 

2802 mapper, 

2803 result, 

2804 adapter, 

2805 populators, 

2806 ): 

2807 

2808 if not context.compile_state.compile_options._enable_eagerloads: 

2809 return 

2810 

2811 if not self.parent.class_manager[self.key].impl.supports_population: 

2812 raise sa_exc.InvalidRequestError( 

2813 "'%s' does not support object " 

2814 "population - eager loading cannot be applied." % self 

2815 ) 

2816 

2817 if self.uselist: 

2818 context.loaders_require_uniquing = True 

2819 

2820 our_path = path[self.parent_property] 

2821 

2822 eager_adapter = self._create_eager_adapter( 

2823 context, result, adapter, our_path, loadopt 

2824 ) 

2825 

2826 if eager_adapter is not False: 

2827 key = self.key 

2828 

2829 _instance = loading._instance_processor( 

2830 query_entity, 

2831 self.mapper, 

2832 context, 

2833 result, 

2834 our_path[self.entity], 

2835 eager_adapter, 

2836 ) 

2837 

2838 if not self.uselist: 

2839 self._create_scalar_loader(context, key, _instance, populators) 

2840 else: 

2841 self._create_collection_loader( 

2842 context, key, _instance, populators 

2843 ) 

2844 else: 

2845 self.parent_property._get_strategy( 

2846 (("lazy", "select"),) 

2847 ).create_row_processor( 

2848 context, 

2849 query_entity, 

2850 path, 

2851 loadopt, 

2852 mapper, 

2853 result, 

2854 adapter, 

2855 populators, 

2856 ) 

2857 

2858 def _create_collection_loader(self, context, key, _instance, populators): 

2859 def load_collection_from_joined_new_row(state, dict_, row): 

2860 # note this must unconditionally clear out any existing collection. 

2861 # an existing collection would be present only in the case of 

2862 # populate_existing(). 

2863 collection = attributes.init_state_collection(state, dict_, key) 

2864 result_list = util.UniqueAppender( 

2865 collection, "append_without_event" 

2866 ) 

2867 context.attributes[(state, key)] = result_list 

2868 inst = _instance(row) 

2869 if inst is not None: 

2870 result_list.append(inst) 

2871 

2872 def load_collection_from_joined_existing_row(state, dict_, row): 

2873 if (state, key) in context.attributes: 

2874 result_list = context.attributes[(state, key)] 

2875 else: 

2876 # appender_key can be absent from context.attributes 

2877 # with isnew=False when self-referential eager loading 

2878 # is used; the same instance may be present in two 

2879 # distinct sets of result columns 

2880 collection = attributes.init_state_collection( 

2881 state, dict_, key 

2882 ) 

2883 result_list = util.UniqueAppender( 

2884 collection, "append_without_event" 

2885 ) 

2886 context.attributes[(state, key)] = result_list 

2887 inst = _instance(row) 

2888 if inst is not None: 

2889 result_list.append(inst) 

2890 

2891 def load_collection_from_joined_exec(state, dict_, row): 

2892 _instance(row) 

2893 

2894 populators["new"].append( 

2895 (self.key, load_collection_from_joined_new_row) 

2896 ) 

2897 populators["existing"].append( 

2898 (self.key, load_collection_from_joined_existing_row) 

2899 ) 

2900 if context.invoke_all_eagers: 

2901 populators["eager"].append( 

2902 (self.key, load_collection_from_joined_exec) 

2903 ) 

2904 

2905 def _create_scalar_loader(self, context, key, _instance, populators): 

2906 def load_scalar_from_joined_new_row(state, dict_, row): 

2907 # set a scalar object instance directly on the parent 

2908 # object, bypassing InstrumentedAttribute event handlers. 

2909 dict_[key] = _instance(row) 

2910 

2911 def load_scalar_from_joined_existing_row(state, dict_, row): 

2912 # call _instance on the row, even though the object has 

2913 # been created, so that we further descend into properties 

2914 existing = _instance(row) 

2915 

2916 # conflicting value already loaded, this shouldn't happen 

2917 if key in dict_: 

2918 if existing is not dict_[key]: 

2919 util.warn( 

2920 "Multiple rows returned with " 

2921 "uselist=False for eagerly-loaded attribute '%s' " 

2922 % self 

2923 ) 

2924 else: 

2925 # this case is when one row has multiple loads of the 

2926 # same entity (e.g. via aliasing), one has an attribute 

2927 # that the other doesn't. 

2928 dict_[key] = existing 

2929 

2930 def load_scalar_from_joined_exec(state, dict_, row): 

2931 _instance(row) 

2932 

2933 populators["new"].append((self.key, load_scalar_from_joined_new_row)) 

2934 populators["existing"].append( 

2935 (self.key, load_scalar_from_joined_existing_row) 

2936 ) 

2937 if context.invoke_all_eagers: 

2938 populators["eager"].append( 

2939 (self.key, load_scalar_from_joined_exec) 

2940 ) 

2941 

2942 

2943@log.class_logger 

2944@relationships.RelationshipProperty.strategy_for(lazy="selectin") 

2945class _SelectInLoader(_PostLoader, util.MemoizedSlots): 

2946 __slots__ = ( 

2947 "join_depth", 

2948 "omit_join", 

2949 "_parent_alias", 

2950 "_query_info", 

2951 "_fallback_query_info", 

2952 ) 

2953 

2954 query_info = collections.namedtuple( 

2955 "queryinfo", 

2956 [ 

2957 "load_only_child", 

2958 "load_with_join", 

2959 "in_expr", 

2960 "pk_cols", 

2961 "zero_idx", 

2962 "child_lookup_cols", 

2963 ], 

2964 ) 

2965 

2966 _chunksize = 500 

2967 

2968 def __init__(self, parent, strategy_key): 

2969 super().__init__(parent, strategy_key) 

2970 self.join_depth = self.parent_property.join_depth 

2971 is_m2o = self.parent_property.direction is interfaces.MANYTOONE 

2972 

2973 if self.parent_property.omit_join is not None: 

2974 self.omit_join = self.parent_property.omit_join 

2975 else: 

2976 lazyloader = self.parent_property._get_strategy( 

2977 (("lazy", "select"),) 

2978 ) 

2979 if is_m2o: 

2980 self.omit_join = lazyloader.use_get 

2981 else: 

2982 self.omit_join = self.parent._get_clause[0].compare( 

2983 lazyloader._rev_lazywhere, 

2984 use_proxies=True, 

2985 compare_keys=False, 

2986 equivalents=self.parent._equivalent_columns, 

2987 ) 

2988 

2989 if self.omit_join: 

2990 if is_m2o: 

2991 self._query_info = self._init_for_omit_join_m2o() 

2992 self._fallback_query_info = self._init_for_join() 

2993 else: 

2994 self._query_info = self._init_for_omit_join() 

2995 else: 

2996 self._query_info = self._init_for_join() 

2997 

2998 def _init_for_omit_join(self): 

2999 pk_to_fk = dict( 

3000 self.parent_property._join_condition.local_remote_pairs 

3001 ) 

3002 pk_to_fk.update( 

3003 (equiv, pk_to_fk[k]) 

3004 for k in list(pk_to_fk) 

3005 for equiv in self.parent._equivalent_columns.get(k, ()) 

3006 ) 

3007 

3008 pk_cols = fk_cols = [ 

3009 pk_to_fk[col] for col in self.parent.primary_key if col in pk_to_fk 

3010 ] 

3011 if len(fk_cols) > 1: 

3012 in_expr = sql.tuple_(*fk_cols) 

3013 zero_idx = False 

3014 else: 

3015 in_expr = fk_cols[0] 

3016 zero_idx = True 

3017 

3018 return self.query_info(False, False, in_expr, pk_cols, zero_idx, None) 

3019 

3020 def _init_for_omit_join_m2o(self): 

3021 pk_cols = self.mapper.primary_key 

3022 if len(pk_cols) > 1: 

3023 in_expr = sql.tuple_(*pk_cols) 

3024 zero_idx = False 

3025 else: 

3026 in_expr = pk_cols[0] 

3027 zero_idx = True 

3028 

3029 lazyloader = self.parent_property._get_strategy((("lazy", "select"),)) 

3030 lookup_cols = [lazyloader._equated_columns[pk] for pk in pk_cols] 

3031 

3032 return self.query_info( 

3033 True, False, in_expr, pk_cols, zero_idx, lookup_cols 

3034 ) 

3035 

3036 def _init_for_join(self): 

3037 self._parent_alias = AliasedClass(self.parent.class_) 

3038 pa_insp = inspect(self._parent_alias) 

3039 pk_cols = [ 

3040 pa_insp._adapt_element(col) for col in self.parent.primary_key 

3041 ] 

3042 if len(pk_cols) > 1: 

3043 in_expr = sql.tuple_(*pk_cols) 

3044 zero_idx = False 

3045 else: 

3046 in_expr = pk_cols[0] 

3047 zero_idx = True 

3048 return self.query_info(False, True, in_expr, pk_cols, zero_idx, None) 

3049 

3050 def init_class_attribute(self, mapper): 

3051 self.parent_property._get_strategy( 

3052 (("lazy", "select"),) 

3053 ).init_class_attribute(mapper) 

3054 

3055 def create_row_processor( 

3056 self, 

3057 context, 

3058 query_entity, 

3059 path, 

3060 loadopt, 

3061 mapper, 

3062 result, 

3063 adapter, 

3064 populators, 

3065 ): 

3066 if context.refresh_state: 

3067 return self._immediateload_create_row_processor( 

3068 context, 

3069 query_entity, 

3070 path, 

3071 loadopt, 

3072 mapper, 

3073 result, 

3074 adapter, 

3075 populators, 

3076 ) 

3077 

3078 ( 

3079 effective_path, 

3080 run_loader, 

3081 execution_options, 

3082 recursion_depth, 

3083 ) = self._setup_for_recursion( 

3084 context, path, loadopt, join_depth=self.join_depth 

3085 ) 

3086 

3087 if not run_loader: 

3088 return 

3089 

3090 if not context.compile_state.compile_options._enable_eagerloads: 

3091 return 

3092 

3093 if not self.parent.class_manager[self.key].impl.supports_population: 

3094 raise sa_exc.InvalidRequestError( 

3095 "'%s' does not support object " 

3096 "population - eager loading cannot be applied." % self 

3097 ) 

3098 

3099 # a little dance here as the "path" is still something that only 

3100 # semi-tracks the exact series of things we are loading, still not 

3101 # telling us about with_polymorphic() and stuff like that when it's at 

3102 # the root.. the initial MapperEntity is more accurate for this case. 

3103 if len(path) == 1: 

3104 if not orm_util._entity_isa(query_entity.entity_zero, self.parent): 

3105 return 

3106 elif not orm_util._entity_isa(path[-1], self.parent): 

3107 return 

3108 

3109 selectin_path = effective_path 

3110 

3111 path_w_prop = path[self.parent_property] 

3112 

3113 # build up a path indicating the path from the leftmost 

3114 # entity to the thing we're subquery loading. 

3115 with_poly_entity = path_w_prop.get( 

3116 context.attributes, "path_with_polymorphic", None 

3117 ) 

3118 if with_poly_entity is not None: 

3119 effective_entity = inspect(with_poly_entity) 

3120 else: 

3121 effective_entity = self.entity 

3122 

3123 loading._PostLoad.callable_for_path( 

3124 context, 

3125 selectin_path, 

3126 self.parent, 

3127 self.parent_property, 

3128 self._load_for_path, 

3129 effective_entity, 

3130 loadopt, 

3131 recursion_depth, 

3132 execution_options, 

3133 ) 

3134 

3135 def _load_for_path( 

3136 self, 

3137 context, 

3138 path, 

3139 states, 

3140 load_only, 

3141 effective_entity, 

3142 loadopt, 

3143 recursion_depth, 

3144 execution_options, 

3145 ): 

3146 if load_only and self.key not in load_only: 

3147 return 

3148 

3149 query_info = self._query_info 

3150 

3151 if query_info.load_only_child: 

3152 our_states = collections.defaultdict(list) 

3153 none_states = [] 

3154 

3155 mapper = self.parent 

3156 

3157 for state, overwrite in states: 

3158 state_dict = state.dict 

3159 related_ident = tuple( 

3160 mapper._get_state_attr_by_column( 

3161 state, 

3162 state_dict, 

3163 lk, 

3164 passive=attributes.PASSIVE_NO_FETCH, 

3165 ) 

3166 for lk in query_info.child_lookup_cols 

3167 ) 

3168 # if the loaded parent objects do not have the foreign key 

3169 # to the related item loaded, then degrade into the joined 

3170 # version of selectinload 

3171 if LoaderCallableStatus.PASSIVE_NO_RESULT in related_ident: 

3172 query_info = self._fallback_query_info 

3173 break 

3174 

3175 # organize states into lists keyed to particular foreign 

3176 # key values. 

3177 if None not in related_ident: 

3178 our_states[related_ident].append( 

3179 (state, state_dict, overwrite) 

3180 ) 

3181 else: 

3182 # For FK values that have None, add them to a 

3183 # separate collection that will be populated separately 

3184 none_states.append((state, state_dict, overwrite)) 

3185 

3186 # note the above conditional may have changed query_info 

3187 if not query_info.load_only_child: 

3188 our_states = [ 

3189 (state.key[1], state, state.dict, overwrite) 

3190 for state, overwrite in states 

3191 ] 

3192 

3193 pk_cols = query_info.pk_cols 

3194 in_expr = query_info.in_expr 

3195 

3196 if not query_info.load_with_join: 

3197 # in "omit join" mode, the primary key column and the 

3198 # "in" expression are in terms of the related entity. So 

3199 # if the related entity is polymorphic or otherwise aliased, 

3200 # we need to adapt our "pk_cols" and "in_expr" to that 

3201 # entity. in non-"omit join" mode, these are against the 

3202 # parent entity and do not need adaption. 

3203 if effective_entity.is_aliased_class: 

3204 pk_cols = [ 

3205 effective_entity._adapt_element(col) for col in pk_cols 

3206 ] 

3207 in_expr = effective_entity._adapt_element(in_expr) 

3208 

3209 bundle_ent = orm_util.Bundle("pk", *pk_cols) 

3210 bundle_sql = bundle_ent.__clause_element__() 

3211 

3212 entity_sql = effective_entity.__clause_element__() 

3213 q = Select._create_raw_select( 

3214 _raw_columns=[bundle_sql, entity_sql], 

3215 _label_style=LABEL_STYLE_TABLENAME_PLUS_COL, 

3216 _compile_options=_ORMCompileState.default_compile_options, 

3217 _propagate_attrs={ 

3218 "compile_state_plugin": "orm", 

3219 "plugin_subject": effective_entity, 

3220 }, 

3221 ) 

3222 

3223 if not query_info.load_with_join: 

3224 # the Bundle we have in the "omit_join" case is against raw, non 

3225 # annotated columns, so to ensure the Query knows its primary 

3226 # entity, we add it explicitly. If we made the Bundle against 

3227 # annotated columns, we hit a performance issue in this specific 

3228 # case, which is detailed in issue #4347. 

3229 q = q.select_from(effective_entity) 

3230 else: 

3231 # in the non-omit_join case, the Bundle is against the annotated/ 

3232 # mapped column of the parent entity, but the #4347 issue does not 

3233 # occur in this case. 

3234 q = q.select_from(self._parent_alias).join( 

3235 getattr(self._parent_alias, self.parent_property.key).of_type( 

3236 effective_entity 

3237 ) 

3238 ) 

3239 

3240 q = q.filter(in_expr.in_(sql.bindparam("primary_keys"))) 

3241 

3242 # a test which exercises what these comments talk about is 

3243 # test_selectin_relations.py -> test_twolevel_selectin_w_polymorphic 

3244 # 

3245 # effective_entity above is given to us in terms of the cached 

3246 # statement, namely this one: 

3247 orig_query = context.compile_state.select_statement 

3248 

3249 # the actual statement that was requested is this one: 

3250 # context_query = context.user_passed_query 

3251 # 

3252 # that's not the cached one, however. So while it is of the identical 

3253 # structure, if it has entities like AliasedInsp, which we get from 

3254 # aliased() or with_polymorphic(), the AliasedInsp will likely be a 

3255 # different object identity each time, and will not match up 

3256 # hashing-wise to the corresponding AliasedInsp that's in the 

3257 # cached query, meaning it won't match on paths and loader lookups 

3258 # and loaders like this one will be skipped if it is used in options. 

3259 # 

3260 # as it turns out, standard loader options like selectinload(), 

3261 # lazyload() that have a path need 

3262 # to come from the cached query so that the AliasedInsp etc. objects 

3263 # that are in the query line up with the object that's in the path 

3264 # of the strategy object. however other options like 

3265 # with_loader_criteria() that doesn't have a path (has a fixed entity) 

3266 # and needs to have access to the latest closure state in order to 

3267 # be correct, we need to use the uncached one. 

3268 # 

3269 # as of #8399 we let the loader option itself figure out what it 

3270 # wants to do given cached and uncached version of itself. 

3271 

3272 effective_path = path[self.parent_property] 

3273 

3274 if orig_query is context.user_passed_query: 

3275 new_options = orig_query._with_options 

3276 else: 

3277 cached_options = orig_query._with_options 

3278 uncached_options = context.user_passed_query._with_options 

3279 

3280 # propagate compile state options from the original query, 

3281 # updating their "extra_criteria" as necessary. 

3282 # note this will create a different cache key than 

3283 # "orig" options if extra_criteria is present, because the copy 

3284 # of extra_criteria will have different boundparam than that of 

3285 # the QueryableAttribute in the path 

3286 new_options = [ 

3287 orig_opt._adapt_cached_option_to_uncached_option( 

3288 context, uncached_opt 

3289 ) 

3290 for orig_opt, uncached_opt in zip( 

3291 cached_options, uncached_options 

3292 ) 

3293 ] 

3294 

3295 if loadopt and loadopt._extra_criteria: 

3296 new_options += ( 

3297 orm_util.LoaderCriteriaOption( 

3298 effective_entity, 

3299 loadopt._generate_extra_criteria(context), 

3300 ), 

3301 ) 

3302 

3303 if recursion_depth is not None: 

3304 effective_path = effective_path._truncate_recursive() 

3305 

3306 q = q.options(*new_options) 

3307 

3308 q = q._update_compile_options({"_current_path": effective_path}) 

3309 if context.populate_existing: 

3310 q = q.execution_options(populate_existing=True) 

3311 

3312 if self.parent_property.order_by: 

3313 if not query_info.load_with_join: 

3314 eager_order_by = self.parent_property.order_by 

3315 if effective_entity.is_aliased_class: 

3316 eager_order_by = [ 

3317 effective_entity._adapt_element(elem) 

3318 for elem in eager_order_by 

3319 ] 

3320 q = q.order_by(*eager_order_by) 

3321 else: 

3322 

3323 def _setup_outermost_orderby(compile_context): 

3324 compile_context.eager_order_by += tuple( 

3325 util.to_list(self.parent_property.order_by) 

3326 ) 

3327 

3328 q = q._add_compile_state_func( 

3329 _setup_outermost_orderby, self.parent_property 

3330 ) 

3331 

3332 if query_info.load_only_child: 

3333 self._load_via_child( 

3334 our_states, 

3335 none_states, 

3336 query_info, 

3337 q, 

3338 context, 

3339 execution_options, 

3340 ) 

3341 else: 

3342 self._load_via_parent( 

3343 our_states, query_info, q, context, execution_options 

3344 ) 

3345 

3346 def _load_via_child( 

3347 self, 

3348 our_states, 

3349 none_states, 

3350 query_info, 

3351 q, 

3352 context, 

3353 execution_options, 

3354 ): 

3355 uselist = self.uselist 

3356 

3357 # this sort is really for the benefit of the unit tests 

3358 our_keys = sorted(our_states) 

3359 while our_keys: 

3360 chunk = our_keys[0 : self._chunksize] 

3361 our_keys = our_keys[self._chunksize :] 

3362 data = { 

3363 k: v 

3364 for k, v in context.session.execute( 

3365 q, 

3366 params={ 

3367 "primary_keys": [ 

3368 key[0] if query_info.zero_idx else key 

3369 for key in chunk 

3370 ] 

3371 }, 

3372 execution_options=execution_options, 

3373 ).unique() 

3374 } 

3375 

3376 for key in chunk: 

3377 # for a real foreign key and no concurrent changes to the 

3378 # DB while running this method, "key" is always present in 

3379 # data. However, for primaryjoins without real foreign keys 

3380 # a non-None primaryjoin condition may still refer to no 

3381 # related object. 

3382 related_obj = data.get(key, None) 

3383 for state, dict_, overwrite in our_states[key]: 

3384 if not overwrite and self.key in dict_: 

3385 continue 

3386 

3387 state.get_impl(self.key).set_committed_value( 

3388 state, 

3389 dict_, 

3390 related_obj if not uselist else [related_obj], 

3391 ) 

3392 # populate none states with empty value / collection 

3393 for state, dict_, overwrite in none_states: 

3394 if not overwrite and self.key in dict_: 

3395 continue 

3396 

3397 # note it's OK if this is a uselist=True attribute, the empty 

3398 # collection will be populated 

3399 state.get_impl(self.key).set_committed_value(state, dict_, None) 

3400 

3401 def _load_via_parent( 

3402 self, our_states, query_info, q, context, execution_options 

3403 ): 

3404 uselist = self.uselist 

3405 _empty_result = () if uselist else None 

3406 

3407 while our_states: 

3408 chunk = our_states[0 : self._chunksize] 

3409 our_states = our_states[self._chunksize :] 

3410 

3411 primary_keys = [ 

3412 key[0] if query_info.zero_idx else key 

3413 for key, state, state_dict, overwrite in chunk 

3414 ] 

3415 

3416 data = collections.defaultdict(list) 

3417 for k, v in itertools.groupby( 

3418 context.session.execute( 

3419 q, 

3420 params={"primary_keys": primary_keys}, 

3421 execution_options=execution_options, 

3422 ).unique(), 

3423 lambda x: x[0], 

3424 ): 

3425 data[k].extend(vv[1] for vv in v) 

3426 

3427 for key, state, state_dict, overwrite in chunk: 

3428 if not overwrite and self.key in state_dict: 

3429 continue 

3430 

3431 collection = data.get(key, _empty_result) 

3432 

3433 if not uselist and collection: 

3434 if len(collection) > 1: 

3435 util.warn( 

3436 "Multiple rows returned with " 

3437 "uselist=False for eagerly-loaded " 

3438 "attribute '%s' " % self 

3439 ) 

3440 state.get_impl(self.key).set_committed_value( 

3441 state, state_dict, collection[0] 

3442 ) 

3443 else: 

3444 # note that empty tuple set on uselist=False sets the 

3445 # value to None 

3446 state.get_impl(self.key).set_committed_value( 

3447 state, state_dict, collection 

3448 ) 

3449 

3450 

3451def _single_parent_validator(desc, prop): 

3452 def _do_check(state, value, oldvalue, initiator): 

3453 if value is not None and initiator.key == prop.key: 

3454 hasparent = initiator.hasparent(attributes.instance_state(value)) 

3455 if hasparent and oldvalue is not value: 

3456 raise sa_exc.InvalidRequestError( 

3457 "Instance %s is already associated with an instance " 

3458 "of %s via its %s attribute, and is only allowed a " 

3459 "single parent." 

3460 % (orm_util.instance_str(value), state.class_, prop), 

3461 code="bbf1", 

3462 ) 

3463 return value 

3464 

3465 def append(state, value, initiator): 

3466 return _do_check(state, value, None, initiator) 

3467 

3468 def set_(state, value, oldvalue, initiator): 

3469 return _do_check(state, value, oldvalue, initiator) 

3470 

3471 event.listen( 

3472 desc, "append", append, raw=True, retval=True, active_history=True 

3473 ) 

3474 event.listen(desc, "set", set_, raw=True, retval=True, active_history=True)