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

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

1162 statements  

1# orm/strategies.py 

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

3# <see AUTHORS file> 

4# 

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

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

7# mypy: ignore-errors 

8 

9 

10"""sqlalchemy.orm.interfaces.LoaderStrategy 

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 Optional 

20from typing import Tuple 

21from typing import TYPE_CHECKING 

22from typing import Union 

23 

24from . import attributes 

25from . import exc as orm_exc 

26from . import interfaces 

27from . import loading 

28from . import path_registry 

29from . import properties 

30from . import query 

31from . import relationships 

32from . import unitofwork 

33from . import util as orm_util 

34from .base import _DEFER_FOR_STATE 

35from .base import _RAISE_FOR_STATE 

36from .base import _SET_DEFERRED_EXPIRED 

37from .base import ATTR_WAS_SET 

38from .base import LoaderCallableStatus 

39from .base import PASSIVE_OFF 

40from .base import PassiveFlag 

41from .context import _column_descriptions 

42from .context import ORMCompileState 

43from .context import ORMSelectCompileState 

44from .context import QueryContext 

45from .interfaces import LoaderStrategy 

46from .interfaces import StrategizedProperty 

47from .session import _state_session 

48from .state import InstanceState 

49from .strategy_options import Load 

50from .util import _none_only_set 

51from .util import AliasedClass 

52from .. import event 

53from .. import exc as sa_exc 

54from .. import inspect 

55from .. import log 

56from .. import sql 

57from .. import util 

58from ..sql import util as sql_util 

59from ..sql import visitors 

60from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL 

61from ..sql.selectable import Select 

62from ..util.typing import Literal 

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 **kw, 

81): 

82 listen_hooks = [] 

83 

84 uselist = useobject and prop.uselist 

85 

86 if useobject and prop.single_parent: 

87 listen_hooks.append(single_parent_validator) 

88 

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

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

91 listen_hooks.append( 

92 lambda desc, prop: orm_util._validator_events( 

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

94 ) 

95 ) 

96 

97 if useobject: 

98 listen_hooks.append(unitofwork.track_cascade_events) 

99 

100 # need to assemble backref listeners 

101 # after the singleparentvalidator, mapper validator 

102 if useobject: 

103 backref = prop.back_populates 

104 if backref and prop._effective_sync_backref: 

105 listen_hooks.append( 

106 lambda desc, prop: attributes.backref_listeners( 

107 desc, backref, uselist 

108 ) 

109 ) 

110 

111 # a single MapperProperty is shared down a class inheritance 

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

113 # for each mapper down the hierarchy. 

114 

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

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

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

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

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

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

121 

122 for m in mapper.self_and_descendants: 

123 if prop is m._props.get( 

124 prop.key 

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

126 desc = attributes.register_attribute_impl( 

127 m.class_, 

128 prop.key, 

129 parent_token=prop, 

130 uselist=uselist, 

131 compare_function=compare_function, 

132 useobject=useobject, 

133 trackparent=useobject 

134 and ( 

135 prop.single_parent 

136 or prop.direction is interfaces.ONETOMANY 

137 ), 

138 typecallable=typecallable, 

139 callable_=callable_, 

140 active_history=active_history, 

141 impl_class=impl_class, 

142 send_modified_events=not useobject or not prop.viewonly, 

143 doc=prop.doc, 

144 **kw, 

145 ) 

146 

147 for hook in listen_hooks: 

148 hook(desc, prop) 

149 

150 

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

152class UninstrumentedColumnLoader(LoaderStrategy): 

153 """Represent a non-instrumented MapperProperty. 

154 

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

156 if the argument is against the with_polymorphic selectable. 

157 

158 """ 

159 

160 __slots__ = ("columns",) 

161 

162 def __init__(self, parent, strategy_key): 

163 super().__init__(parent, strategy_key) 

164 self.columns = self.parent_property.columns 

165 

166 def setup_query( 

167 self, 

168 compile_state, 

169 query_entity, 

170 path, 

171 loadopt, 

172 adapter, 

173 column_collection=None, 

174 **kwargs, 

175 ): 

176 for c in self.columns: 

177 if adapter: 

178 c = adapter.columns[c] 

179 compile_state._append_dedupe_col_collection(c, column_collection) 

180 

181 def create_row_processor( 

182 self, 

183 context, 

184 query_entity, 

185 path, 

186 loadopt, 

187 mapper, 

188 result, 

189 adapter, 

190 populators, 

191 ): 

192 pass 

193 

194 

195@log.class_logger 

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

197class ColumnLoader(LoaderStrategy): 

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

199 

200 __slots__ = "columns", "is_composite" 

201 

202 def __init__(self, parent, strategy_key): 

203 super().__init__(parent, strategy_key) 

204 self.columns = self.parent_property.columns 

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

206 

207 def setup_query( 

208 self, 

209 compile_state, 

210 query_entity, 

211 path, 

212 loadopt, 

213 adapter, 

214 column_collection, 

215 memoized_populators, 

216 check_for_adapt=False, 

217 **kwargs, 

218 ): 

219 for c in self.columns: 

220 if adapter: 

221 if check_for_adapt: 

222 c = adapter.adapt_check_present(c) 

223 if c is None: 

224 return 

225 else: 

226 c = adapter.columns[c] 

227 

228 compile_state._append_dedupe_col_collection(c, column_collection) 

229 

230 fetch = self.columns[0] 

231 if adapter: 

232 fetch = adapter.columns[fetch] 

233 if fetch is None: 

234 # None happens here only for dml bulk_persistence cases 

235 # when context.DMLReturningColFilter is used 

236 return 

237 

238 memoized_populators[self.parent_property] = fetch 

239 

240 def init_class_attribute(self, mapper): 

241 self.is_class_level = True 

242 coltype = self.columns[0].type 

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

244 active_history = ( 

245 self.parent_property.active_history 

246 or self.columns[0].primary_key 

247 or ( 

248 mapper.version_id_col is not None 

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

250 is self.parent_property 

251 ) 

252 ) 

253 

254 _register_attribute( 

255 self.parent_property, 

256 mapper, 

257 useobject=False, 

258 compare_function=coltype.compare_values, 

259 active_history=active_history, 

260 ) 

261 

262 def create_row_processor( 

263 self, 

264 context, 

265 query_entity, 

266 path, 

267 loadopt, 

268 mapper, 

269 result, 

270 adapter, 

271 populators, 

272 ): 

273 # look through list of columns represented here 

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

275 

276 for col in self.columns: 

277 if adapter: 

278 col = adapter.columns[col] 

279 getter = result._getter(col, False) 

280 if getter: 

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

282 break 

283 else: 

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

285 

286 

287@log.class_logger 

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

289class ExpressionColumnLoader(ColumnLoader): 

290 def __init__(self, parent, strategy_key): 

291 super().__init__(parent, strategy_key) 

292 

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

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

295 # unless an expr is passed in the options. 

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

297 self._have_default_expression = any( 

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

299 ) 

300 

301 def setup_query( 

302 self, 

303 compile_state, 

304 query_entity, 

305 path, 

306 loadopt, 

307 adapter, 

308 column_collection, 

309 memoized_populators, 

310 **kwargs, 

311 ): 

312 columns = None 

313 if loadopt and loadopt._extra_criteria: 

314 columns = loadopt._extra_criteria 

315 

316 elif self._have_default_expression: 

317 columns = self.parent_property.columns 

318 

319 if columns is None: 

320 return 

321 

322 for c in columns: 

323 if adapter: 

324 c = adapter.columns[c] 

325 compile_state._append_dedupe_col_collection(c, column_collection) 

326 

327 fetch = columns[0] 

328 if adapter: 

329 fetch = adapter.columns[fetch] 

330 if fetch is None: 

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

332 # adapter implementation here, however there may be theoretical 

333 # usages of returning() with context.DMLReturningColFilter 

334 return 

335 

336 memoized_populators[self.parent_property] = fetch 

337 

338 def create_row_processor( 

339 self, 

340 context, 

341 query_entity, 

342 path, 

343 loadopt, 

344 mapper, 

345 result, 

346 adapter, 

347 populators, 

348 ): 

349 # look through list of columns represented here 

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

351 if loadopt and loadopt._extra_criteria: 

352 columns = loadopt._extra_criteria 

353 

354 for col in columns: 

355 if adapter: 

356 col = adapter.columns[col] 

357 getter = result._getter(col, False) 

358 if getter: 

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

360 break 

361 else: 

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

363 

364 def init_class_attribute(self, mapper): 

365 self.is_class_level = True 

366 

367 _register_attribute( 

368 self.parent_property, 

369 mapper, 

370 useobject=False, 

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

372 accepts_scalar_loader=False, 

373 ) 

374 

375 

376@log.class_logger 

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

378@properties.ColumnProperty.strategy_for( 

379 deferred=True, instrument=True, raiseload=True 

380) 

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

382class DeferredColumnLoader(LoaderStrategy): 

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

384 

385 __slots__ = "columns", "group", "raiseload" 

386 

387 def __init__(self, parent, strategy_key): 

388 super().__init__(parent, strategy_key) 

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

390 raise NotImplementedError( 

391 "Deferred loading for composite types not implemented yet" 

392 ) 

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

394 self.columns = self.parent_property.columns 

395 self.group = self.parent_property.group 

396 

397 def create_row_processor( 

398 self, 

399 context, 

400 query_entity, 

401 path, 

402 loadopt, 

403 mapper, 

404 result, 

405 adapter, 

406 populators, 

407 ): 

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

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

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

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

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

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

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

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

416 

417 if ( 

418 context.refresh_state 

419 and context.query._compile_options._only_load_props 

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

421 ): 

422 self.parent_property._get_strategy( 

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

424 ).create_row_processor( 

425 context, 

426 query_entity, 

427 path, 

428 loadopt, 

429 mapper, 

430 result, 

431 adapter, 

432 populators, 

433 ) 

434 

435 elif not self.is_class_level: 

436 if self.raiseload: 

437 set_deferred_for_local_state = ( 

438 self.parent_property._raise_column_loader 

439 ) 

440 else: 

441 set_deferred_for_local_state = ( 

442 self.parent_property._deferred_column_loader 

443 ) 

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

445 else: 

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

447 

448 def init_class_attribute(self, mapper): 

449 self.is_class_level = True 

450 

451 _register_attribute( 

452 self.parent_property, 

453 mapper, 

454 useobject=False, 

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

456 callable_=self._load_for_state, 

457 load_on_unexpire=False, 

458 ) 

459 

460 def setup_query( 

461 self, 

462 compile_state, 

463 query_entity, 

464 path, 

465 loadopt, 

466 adapter, 

467 column_collection, 

468 memoized_populators, 

469 only_load_props=None, 

470 **kw, 

471 ): 

472 if ( 

473 ( 

474 compile_state.compile_options._render_for_subquery 

475 and self.parent_property._renders_in_subqueries 

476 ) 

477 or ( 

478 loadopt 

479 and set(self.columns).intersection( 

480 self.parent._should_undefer_in_wildcard 

481 ) 

482 ) 

483 or ( 

484 loadopt 

485 and self.group 

486 and loadopt.local_opts.get( 

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

488 ) 

489 ) 

490 or (only_load_props and self.key in only_load_props) 

491 ): 

492 self.parent_property._get_strategy( 

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

494 ).setup_query( 

495 compile_state, 

496 query_entity, 

497 path, 

498 loadopt, 

499 adapter, 

500 column_collection, 

501 memoized_populators, 

502 **kw, 

503 ) 

504 elif self.is_class_level: 

505 memoized_populators[self.parent_property] = _SET_DEFERRED_EXPIRED 

506 elif not self.raiseload: 

507 memoized_populators[self.parent_property] = _DEFER_FOR_STATE 

508 else: 

509 memoized_populators[self.parent_property] = _RAISE_FOR_STATE 

510 

511 def _load_for_state(self, state, passive): 

512 if not state.key: 

513 return LoaderCallableStatus.ATTR_EMPTY 

514 

515 if not passive & PassiveFlag.SQL_OK: 

516 return LoaderCallableStatus.PASSIVE_NO_RESULT 

517 

518 localparent = state.manager.mapper 

519 

520 if self.group: 

521 toload = [ 

522 p.key 

523 for p in localparent.iterate_properties 

524 if isinstance(p, StrategizedProperty) 

525 and isinstance(p.strategy, DeferredColumnLoader) 

526 and p.group == self.group 

527 ] 

528 else: 

529 toload = [self.key] 

530 

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

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

533 

534 session = _state_session(state) 

535 if session is None: 

536 raise orm_exc.DetachedInstanceError( 

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

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

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

540 ) 

541 

542 if self.raiseload: 

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

544 

545 loading.load_scalar_attributes( 

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

547 ) 

548 

549 return LoaderCallableStatus.ATTR_WAS_SET 

550 

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

552 raise sa_exc.InvalidRequestError( 

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

554 ) 

555 

556 

557class LoadDeferredColumns: 

558 """serializable loader object used by DeferredColumnLoader""" 

559 

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

561 self.key = key 

562 self.raiseload = raiseload 

563 

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

565 key = self.key 

566 

567 localparent = state.manager.mapper 

568 prop = localparent._props[key] 

569 if self.raiseload: 

570 strategy_key = ( 

571 ("deferred", True), 

572 ("instrument", True), 

573 ("raiseload", True), 

574 ) 

575 else: 

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

577 strategy = prop._get_strategy(strategy_key) 

578 return strategy._load_for_state(state, passive) 

579 

580 

581class AbstractRelationshipLoader(LoaderStrategy): 

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

583 

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

585 

586 def __init__(self, parent, strategy_key): 

587 super().__init__(parent, strategy_key) 

588 self.mapper = self.parent_property.mapper 

589 self.entity = self.parent_property.entity 

590 self.target = self.parent_property.target 

591 self.uselist = self.parent_property.uselist 

592 

593 def _immediateload_create_row_processor( 

594 self, 

595 context, 

596 query_entity, 

597 path, 

598 loadopt, 

599 mapper, 

600 result, 

601 adapter, 

602 populators, 

603 ): 

604 return self.parent_property._get_strategy( 

605 (("lazy", "immediate"),) 

606 ).create_row_processor( 

607 context, 

608 query_entity, 

609 path, 

610 loadopt, 

611 mapper, 

612 result, 

613 adapter, 

614 populators, 

615 ) 

616 

617 

618@log.class_logger 

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

620class DoNothingLoader(LoaderStrategy): 

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

622 

623 Compared to NoLoader, this loader does not initialize the 

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

625 take effect. 

626 

627 """ 

628 

629 

630@log.class_logger 

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

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

633class NoLoader(AbstractRelationshipLoader): 

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

635 with "lazy=None". 

636 

637 """ 

638 

639 __slots__ = () 

640 

641 def init_class_attribute(self, mapper): 

642 self.is_class_level = True 

643 

644 _register_attribute( 

645 self.parent_property, 

646 mapper, 

647 useobject=True, 

648 typecallable=self.parent_property.collection_class, 

649 ) 

650 

651 def create_row_processor( 

652 self, 

653 context, 

654 query_entity, 

655 path, 

656 loadopt, 

657 mapper, 

658 result, 

659 adapter, 

660 populators, 

661 ): 

662 def invoke_no_load(state, dict_, row): 

663 if self.uselist: 

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

665 else: 

666 dict_[self.key] = None 

667 

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

669 

670 

671@log.class_logger 

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

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

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

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

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

677class LazyLoader( 

678 AbstractRelationshipLoader, util.MemoizedSlots, log.Identified 

679): 

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

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

682 

683 """ 

684 

685 __slots__ = ( 

686 "_lazywhere", 

687 "_rev_lazywhere", 

688 "_lazyload_reverse_option", 

689 "_order_by", 

690 "use_get", 

691 "is_aliased_class", 

692 "_bind_to_col", 

693 "_equated_columns", 

694 "_rev_bind_to_col", 

695 "_rev_equated_columns", 

696 "_simple_lazy_clause", 

697 "_raise_always", 

698 "_raise_on_sql", 

699 ) 

700 

701 _lazywhere: ColumnElement[bool] 

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

703 _rev_lazywhere: ColumnElement[bool] 

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

705 

706 parent_property: RelationshipProperty[Any] 

707 

708 def __init__( 

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

710 ): 

711 super().__init__(parent, strategy_key) 

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

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

714 

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

716 

717 join_condition = self.parent_property._join_condition 

718 ( 

719 self._lazywhere, 

720 self._bind_to_col, 

721 self._equated_columns, 

722 ) = join_condition.create_lazy_clause() 

723 

724 ( 

725 self._rev_lazywhere, 

726 self._rev_bind_to_col, 

727 self._rev_equated_columns, 

728 ) = join_condition.create_lazy_clause(reverse_direction=True) 

729 

730 if self.parent_property.order_by: 

731 self._order_by = [ 

732 sql_util._deep_annotate(elem, {"_orm_adapt": True}) 

733 for elem in util.to_list(self.parent_property.order_by) 

734 ] 

735 else: 

736 self._order_by = None 

737 

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

739 

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

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

742 # 

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

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

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

746 # populate as an empty list. 

747 self.use_get = ( 

748 not self.is_aliased_class 

749 and not self.uselist 

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

751 self._lazywhere, 

752 use_proxies=True, 

753 compare_keys=False, 

754 equivalents=self.mapper._equivalent_columns, 

755 ) 

756 ) 

757 

758 if self.use_get: 

759 for col in list(self._equated_columns): 

760 if col in self.mapper._equivalent_columns: 

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

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

763 

764 self.logger.info( 

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

766 ) 

767 

768 def init_class_attribute(self, mapper): 

769 self.is_class_level = True 

770 

771 _legacy_inactive_history_style = ( 

772 self.parent_property._legacy_inactive_history_style 

773 ) 

774 

775 if self.parent_property.active_history: 

776 active_history = True 

777 _deferred_history = False 

778 

779 elif ( 

780 self.parent_property.direction is not interfaces.MANYTOONE 

781 or not self.use_get 

782 ): 

783 if _legacy_inactive_history_style: 

784 active_history = True 

785 _deferred_history = False 

786 else: 

787 active_history = False 

788 _deferred_history = True 

789 else: 

790 active_history = _deferred_history = False 

791 

792 _register_attribute( 

793 self.parent_property, 

794 mapper, 

795 useobject=True, 

796 callable_=self._load_for_state, 

797 typecallable=self.parent_property.collection_class, 

798 active_history=active_history, 

799 _deferred_history=_deferred_history, 

800 ) 

801 

802 def _memoized_attr__simple_lazy_clause(self): 

803 lazywhere = sql_util._deep_annotate( 

804 self._lazywhere, {"_orm_adapt": True} 

805 ) 

806 

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

808 

809 params = [] 

810 

811 def visit_bindparam(bindparam): 

812 bindparam.unique = False 

813 

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

815 

816 def visit_bindparam(bindparam): 

817 if bindparam._identifying_key in bind_to_col: 

818 params.append( 

819 ( 

820 bindparam.key, 

821 bind_to_col[bindparam._identifying_key], 

822 None, 

823 ) 

824 ) 

825 elif bindparam.callable is None: 

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

827 

828 criterion = visitors.cloned_traverse( 

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

830 ) 

831 

832 return criterion, params 

833 

834 def _generate_lazy_clause(self, state, passive): 

835 criterion, param_keys = self._simple_lazy_clause 

836 

837 if state is None: 

838 return sql_util.adapt_criterion_to_null( 

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

840 ) 

841 

842 mapper = self.parent_property.parent 

843 

844 o = state.obj() # strong ref 

845 dict_ = attributes.instance_dict(o) 

846 

847 if passive & PassiveFlag.INIT_OK: 

848 passive ^= PassiveFlag.INIT_OK 

849 

850 params = {} 

851 for key, ident, value in param_keys: 

852 if ident is not None: 

853 if passive and passive & PassiveFlag.LOAD_AGAINST_COMMITTED: 

854 value = mapper._get_committed_state_attr_by_column( 

855 state, dict_, ident, passive 

856 ) 

857 else: 

858 value = mapper._get_state_attr_by_column( 

859 state, dict_, ident, passive 

860 ) 

861 

862 params[key] = value 

863 

864 return criterion, params 

865 

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

867 raise sa_exc.InvalidRequestError( 

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

869 ) 

870 

871 def _load_for_state( 

872 self, 

873 state, 

874 passive, 

875 loadopt=None, 

876 extra_criteria=(), 

877 extra_options=(), 

878 alternate_effective_path=None, 

879 execution_options=util.EMPTY_DICT, 

880 ): 

881 if not state.key and ( 

882 ( 

883 not self.parent_property.load_on_pending 

884 and not state._load_pending 

885 ) 

886 or not state.session_id 

887 ): 

888 return LoaderCallableStatus.ATTR_EMPTY 

889 

890 pending = not state.key 

891 primary_key_identity = None 

892 

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

894 

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

896 not passive & attributes.NON_PERSISTENT_OK and pending 

897 ): 

898 return LoaderCallableStatus.PASSIVE_NO_RESULT 

899 

900 if ( 

901 # we were given lazy="raise" 

902 self._raise_always 

903 # the no_raise history-related flag was not passed 

904 and not passive & PassiveFlag.NO_RAISE 

905 and ( 

906 # if we are use_get and related_object_ok is disabled, 

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

908 # for history purposes or otherwise returning 

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

910 # history-related flag 

911 not use_get 

912 or passive & PassiveFlag.RELATED_OBJECT_OK 

913 ) 

914 ): 

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

916 

917 session = _state_session(state) 

918 if not session: 

919 if passive & PassiveFlag.NO_RAISE: 

920 return LoaderCallableStatus.PASSIVE_NO_RESULT 

921 

922 raise orm_exc.DetachedInstanceError( 

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

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

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

926 ) 

927 

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

929 # identity map without generating a Query at all 

930 if use_get: 

931 primary_key_identity = self._get_ident_for_use_get( 

932 session, state, passive 

933 ) 

934 if LoaderCallableStatus.PASSIVE_NO_RESULT in primary_key_identity: 

935 return LoaderCallableStatus.PASSIVE_NO_RESULT 

936 elif LoaderCallableStatus.NEVER_SET in primary_key_identity: 

937 return LoaderCallableStatus.NEVER_SET 

938 

939 # test for None alone in primary_key_identity based on 

940 # allow_partial_pks preference. PASSIVE_NO_RESULT and NEVER_SET 

941 # have already been tested above 

942 if not self.mapper.allow_partial_pks: 

943 if _none_only_set.intersection(primary_key_identity): 

944 return None 

945 else: 

946 if _none_only_set.issuperset(primary_key_identity): 

947 return None 

948 

949 if ( 

950 self.key in state.dict 

951 and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD 

952 ): 

953 return LoaderCallableStatus.ATTR_WAS_SET 

954 

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

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

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

958 # identity_token would be for this identity. 

959 

960 instance = session._identity_lookup( 

961 self.entity, 

962 primary_key_identity, 

963 passive=passive, 

964 lazy_loaded_from=state, 

965 ) 

966 

967 if instance is not None: 

968 if instance is LoaderCallableStatus.PASSIVE_CLASS_MISMATCH: 

969 return None 

970 else: 

971 return instance 

972 elif ( 

973 not passive & PassiveFlag.SQL_OK 

974 or not passive & PassiveFlag.RELATED_OBJECT_OK 

975 ): 

976 return LoaderCallableStatus.PASSIVE_NO_RESULT 

977 

978 return self._emit_lazyload( 

979 session, 

980 state, 

981 primary_key_identity, 

982 passive, 

983 loadopt, 

984 extra_criteria, 

985 extra_options, 

986 alternate_effective_path, 

987 execution_options, 

988 ) 

989 

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

991 instance_mapper = state.manager.mapper 

992 

993 if passive & PassiveFlag.LOAD_AGAINST_COMMITTED: 

994 get_attr = instance_mapper._get_committed_state_attr_by_column 

995 else: 

996 get_attr = instance_mapper._get_state_attr_by_column 

997 

998 dict_ = state.dict 

999 

1000 return [ 

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

1002 for pk in self.mapper.primary_key 

1003 ] 

1004 

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

1006 def _emit_lazyload( 

1007 self, 

1008 session, 

1009 state, 

1010 primary_key_identity, 

1011 passive, 

1012 loadopt, 

1013 extra_criteria, 

1014 extra_options, 

1015 alternate_effective_path, 

1016 execution_options, 

1017 ): 

1018 strategy_options = util.preloaded.orm_strategy_options 

1019 

1020 clauseelement = self.entity.__clause_element__() 

1021 stmt = Select._create_raw_select( 

1022 _raw_columns=[clauseelement], 

1023 _propagate_attrs=clauseelement._propagate_attrs, 

1024 _label_style=LABEL_STYLE_TABLENAME_PLUS_COL, 

1025 _compile_options=ORMCompileState.default_compile_options, 

1026 ) 

1027 load_options = QueryContext.default_load_options 

1028 

1029 load_options += { 

1030 "_invoke_all_eagers": False, 

1031 "_lazy_loaded_from": state, 

1032 } 

1033 

1034 if self.parent_property.secondary is not None: 

1035 stmt = stmt.select_from( 

1036 self.mapper, self.parent_property.secondary 

1037 ) 

1038 

1039 pending = not state.key 

1040 

1041 # don't autoflush on pending 

1042 if pending or passive & attributes.NO_AUTOFLUSH: 

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

1044 

1045 use_get = self.use_get 

1046 

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

1048 if alternate_effective_path is None: 

1049 effective_path = state.load_path[self.parent_property] 

1050 else: 

1051 effective_path = alternate_effective_path[self.parent_property] 

1052 

1053 opts = state.load_options 

1054 

1055 if loadopt and loadopt._extra_criteria: 

1056 use_get = False 

1057 opts += ( 

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

1059 ) 

1060 

1061 stmt._with_options = opts 

1062 elif alternate_effective_path is None: 

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

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

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

1066 else: 

1067 # added by immediateloader 

1068 effective_path = alternate_effective_path[self.parent_property] 

1069 

1070 if extra_options: 

1071 stmt._with_options += extra_options 

1072 

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

1074 

1075 if use_get: 

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

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

1078 

1079 return loading.load_on_pk_identity( 

1080 session, 

1081 stmt, 

1082 primary_key_identity, 

1083 load_options=load_options, 

1084 execution_options=execution_options, 

1085 ) 

1086 

1087 if self._order_by: 

1088 stmt._order_by_clauses = self._order_by 

1089 

1090 def _lazyload_reverse(compile_context): 

1091 for rev in self.parent_property._reverse_property: 

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

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

1094 if ( 

1095 rev.direction is interfaces.MANYTOONE 

1096 and rev._use_get 

1097 and not isinstance(rev.strategy, LazyLoader) 

1098 ): 

1099 strategy_options.Load._construct_for_existing_path( 

1100 compile_context.compile_options._current_path[ 

1101 rev.parent 

1102 ] 

1103 ).lazyload(rev).process_compile_state(compile_context) 

1104 

1105 stmt._with_context_options += ( 

1106 (_lazyload_reverse, self.parent_property), 

1107 ) 

1108 

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

1110 

1111 if execution_options: 

1112 execution_options = util.EMPTY_DICT.merge_with( 

1113 execution_options, 

1114 { 

1115 "_sa_orm_load_options": load_options, 

1116 }, 

1117 ) 

1118 else: 

1119 execution_options = { 

1120 "_sa_orm_load_options": load_options, 

1121 } 

1122 

1123 if ( 

1124 self.key in state.dict 

1125 and not passive & PassiveFlag.DEFERRED_HISTORY_LOAD 

1126 ): 

1127 return LoaderCallableStatus.ATTR_WAS_SET 

1128 

1129 if pending: 

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

1131 return None 

1132 

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

1134 return None 

1135 

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

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

1138 

1139 stmt._where_criteria = (lazy_clause,) 

1140 

1141 result = session.execute( 

1142 stmt, params, execution_options=execution_options 

1143 ) 

1144 

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

1146 

1147 if self.uselist: 

1148 return result 

1149 else: 

1150 l = len(result) 

1151 if l: 

1152 if l > 1: 

1153 util.warn( 

1154 "Multiple rows returned with " 

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

1156 % self.parent_property 

1157 ) 

1158 

1159 return result[0] 

1160 else: 

1161 return None 

1162 

1163 def create_row_processor( 

1164 self, 

1165 context, 

1166 query_entity, 

1167 path, 

1168 loadopt, 

1169 mapper, 

1170 result, 

1171 adapter, 

1172 populators, 

1173 ): 

1174 key = self.key 

1175 

1176 if ( 

1177 context.load_options._is_user_refresh 

1178 and context.query._compile_options._only_load_props 

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

1180 ): 

1181 return self._immediateload_create_row_processor( 

1182 context, 

1183 query_entity, 

1184 path, 

1185 loadopt, 

1186 mapper, 

1187 result, 

1188 adapter, 

1189 populators, 

1190 ) 

1191 

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

1193 # we are not the primary manager for this attribute 

1194 # on this class - set up a 

1195 # per-instance lazyloader, which will override the 

1196 # class-level behavior. 

1197 # this currently only happens when using a 

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

1199 # attribute - "eager" attributes always have a 

1200 # class-level lazyloader installed. 

1201 set_lazy_callable = ( 

1202 InstanceState._instance_level_callable_processor 

1203 )( 

1204 mapper.class_manager, 

1205 LoadLazyAttribute( 

1206 key, 

1207 self, 

1208 loadopt, 

1209 ( 

1210 loadopt._generate_extra_criteria(context) 

1211 if loadopt._extra_criteria 

1212 else None 

1213 ), 

1214 ), 

1215 key, 

1216 ) 

1217 

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

1219 elif context.populate_existing or mapper.always_refresh: 

1220 

1221 def reset_for_lazy_callable(state, dict_, row): 

1222 # we are the primary manager for this attribute on 

1223 # this class - reset its 

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

1225 # lazy loader is 

1226 # executed when next referenced on this instance. 

1227 # this is needed in 

1228 # populate_existing() types of scenarios to reset 

1229 # any existing state. 

1230 state._reset(dict_, key) 

1231 

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

1233 

1234 

1235class LoadLazyAttribute: 

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

1237 

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

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

1240 cached instances. 

1241 

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

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

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

1245 

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

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

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

1249 approach. 

1250 

1251 """ 

1252 

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

1254 self.key = key 

1255 self.strategy_key = initiating_strategy.strategy_key 

1256 self.loadopt = loadopt 

1257 self.extra_criteria = extra_criteria 

1258 

1259 def __getstate__(self): 

1260 if self.extra_criteria is not None: 

1261 util.warn( 

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

1263 "contains additional criteria; please use eager loading " 

1264 "for this case" 

1265 ) 

1266 return { 

1267 "key": self.key, 

1268 "strategy_key": self.strategy_key, 

1269 "loadopt": self.loadopt, 

1270 "extra_criteria": (), 

1271 } 

1272 

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

1274 key = self.key 

1275 instance_mapper = state.manager.mapper 

1276 prop = instance_mapper._props[key] 

1277 strategy = prop._strategies[self.strategy_key] 

1278 

1279 return strategy._load_for_state( 

1280 state, 

1281 passive, 

1282 loadopt=self.loadopt, 

1283 extra_criteria=self.extra_criteria, 

1284 ) 

1285 

1286 

1287class PostLoader(AbstractRelationshipLoader): 

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

1289 

1290 __slots__ = () 

1291 

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

1293 effective_path = ( 

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

1295 ) + path 

1296 

1297 top_level_context = context._get_top_level_context() 

1298 execution_options = util.immutabledict( 

1299 {"sa_top_level_orm_context": top_level_context} 

1300 ) 

1301 

1302 if loadopt: 

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

1304 unlimited_recursion = recursion_depth == -1 

1305 else: 

1306 recursion_depth = None 

1307 unlimited_recursion = False 

1308 

1309 if recursion_depth is not None: 

1310 if not self.parent_property._is_self_referential: 

1311 raise sa_exc.InvalidRequestError( 

1312 f"recursion_depth option on relationship " 

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

1314 "non-self-referential relationship" 

1315 ) 

1316 recursion_depth = context.execution_options.get( 

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

1318 ) 

1319 

1320 if not unlimited_recursion and recursion_depth < 0: 

1321 return ( 

1322 effective_path, 

1323 False, 

1324 execution_options, 

1325 recursion_depth, 

1326 ) 

1327 

1328 if not unlimited_recursion: 

1329 execution_options = execution_options.union( 

1330 { 

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

1332 } 

1333 ) 

1334 

1335 if loading.PostLoad.path_exists( 

1336 context, effective_path, self.parent_property 

1337 ): 

1338 return effective_path, False, execution_options, recursion_depth 

1339 

1340 path_w_prop = path[self.parent_property] 

1341 effective_path_w_prop = effective_path[self.parent_property] 

1342 

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

1344 if join_depth: 

1345 if effective_path_w_prop.length / 2 > join_depth: 

1346 return ( 

1347 effective_path, 

1348 False, 

1349 execution_options, 

1350 recursion_depth, 

1351 ) 

1352 elif effective_path_w_prop.contains_mapper(self.mapper): 

1353 return ( 

1354 effective_path, 

1355 False, 

1356 execution_options, 

1357 recursion_depth, 

1358 ) 

1359 

1360 return effective_path, True, execution_options, recursion_depth 

1361 

1362 

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

1364class ImmediateLoader(PostLoader): 

1365 __slots__ = ("join_depth",) 

1366 

1367 def __init__(self, parent, strategy_key): 

1368 super().__init__(parent, strategy_key) 

1369 self.join_depth = self.parent_property.join_depth 

1370 

1371 def init_class_attribute(self, mapper): 

1372 self.parent_property._get_strategy( 

1373 (("lazy", "select"),) 

1374 ).init_class_attribute(mapper) 

1375 

1376 def create_row_processor( 

1377 self, 

1378 context, 

1379 query_entity, 

1380 path, 

1381 loadopt, 

1382 mapper, 

1383 result, 

1384 adapter, 

1385 populators, 

1386 ): 

1387 if not context.compile_state.compile_options._enable_eagerloads: 

1388 return 

1389 

1390 ( 

1391 effective_path, 

1392 run_loader, 

1393 execution_options, 

1394 recursion_depth, 

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

1396 

1397 if not run_loader: 

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

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

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

1401 # load operation. 

1402 flags = attributes.PASSIVE_NO_FETCH_RELATED | PassiveFlag.NO_RAISE 

1403 else: 

1404 flags = attributes.PASSIVE_OFF | PassiveFlag.NO_RAISE 

1405 

1406 loading.PostLoad.callable_for_path( 

1407 context, 

1408 effective_path, 

1409 self.parent, 

1410 self.parent_property, 

1411 self._load_for_path, 

1412 loadopt, 

1413 flags, 

1414 recursion_depth, 

1415 execution_options, 

1416 ) 

1417 

1418 def _load_for_path( 

1419 self, 

1420 context, 

1421 path, 

1422 states, 

1423 load_only, 

1424 loadopt, 

1425 flags, 

1426 recursion_depth, 

1427 execution_options, 

1428 ): 

1429 if recursion_depth: 

1430 new_opt = Load(loadopt.path.entity) 

1431 new_opt.context = ( 

1432 loadopt, 

1433 loadopt._recurse(), 

1434 ) 

1435 alternate_effective_path = path._truncate_recursive() 

1436 extra_options = (new_opt,) 

1437 else: 

1438 alternate_effective_path = path 

1439 extra_options = () 

1440 

1441 key = self.key 

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

1443 for state, overwrite in states: 

1444 dict_ = state.dict 

1445 

1446 if overwrite or key not in dict_: 

1447 value = lazyloader._load_for_state( 

1448 state, 

1449 flags, 

1450 extra_options=extra_options, 

1451 alternate_effective_path=alternate_effective_path, 

1452 execution_options=execution_options, 

1453 ) 

1454 if value not in ( 

1455 ATTR_WAS_SET, 

1456 LoaderCallableStatus.PASSIVE_NO_RESULT, 

1457 ): 

1458 state.get_impl(key).set_committed_value( 

1459 state, dict_, value 

1460 ) 

1461 

1462 

1463@log.class_logger 

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

1465class SubqueryLoader(PostLoader): 

1466 __slots__ = ("join_depth",) 

1467 

1468 def __init__(self, parent, strategy_key): 

1469 super().__init__(parent, strategy_key) 

1470 self.join_depth = self.parent_property.join_depth 

1471 

1472 def init_class_attribute(self, mapper): 

1473 self.parent_property._get_strategy( 

1474 (("lazy", "select"),) 

1475 ).init_class_attribute(mapper) 

1476 

1477 def _get_leftmost( 

1478 self, 

1479 orig_query_entity_index, 

1480 subq_path, 

1481 current_compile_state, 

1482 is_root, 

1483 ): 

1484 given_subq_path = subq_path 

1485 subq_path = subq_path.path 

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

1487 

1488 # determine attributes of the leftmost mapper 

1489 if ( 

1490 self.parent.isa(subq_mapper) 

1491 and self.parent_property is subq_path[1] 

1492 ): 

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

1494 else: 

1495 leftmost_mapper, leftmost_prop = subq_mapper, subq_path[1] 

1496 

1497 if is_root: 

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

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

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

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

1502 # to adapt 

1503 new_subq_path = current_compile_state._entities[ 

1504 orig_query_entity_index 

1505 ].entity_zero._path_registry[leftmost_prop] 

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

1507 if additional: 

1508 new_subq_path += path_registry.PathRegistry.coerce( 

1509 subq_path[-additional:] 

1510 ) 

1511 else: 

1512 new_subq_path = given_subq_path 

1513 

1514 leftmost_cols = leftmost_prop.local_columns 

1515 

1516 leftmost_attr = [ 

1517 getattr( 

1518 new_subq_path.path[0].entity, 

1519 leftmost_mapper._columntoproperty[c].key, 

1520 ) 

1521 for c in leftmost_cols 

1522 ] 

1523 

1524 return leftmost_mapper, leftmost_attr, leftmost_prop, new_subq_path 

1525 

1526 def _generate_from_original_query( 

1527 self, 

1528 orig_compile_state, 

1529 orig_query, 

1530 leftmost_mapper, 

1531 leftmost_attr, 

1532 leftmost_relationship, 

1533 orig_entity, 

1534 ): 

1535 # reformat the original query 

1536 # to look only for significant columns 

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

1538 

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

1540 # This suits at least two legacy cases: 

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

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

1543 # 2. applications which are doing subqueryload with complex 

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

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

1546 # from_self() (Neutron) - see test_subqload_from_self 

1547 # for demo. 

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

1549 q2.__dict__.update(q.__dict__) 

1550 q = q2 

1551 

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

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

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

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

1556 if not q._from_obj: 

1557 q._enable_assertions = False 

1558 q.select_from.non_generative( 

1559 q, 

1560 *{ 

1561 ent["entity"] 

1562 for ent in _column_descriptions( 

1563 orig_query, compile_state=orig_compile_state 

1564 ) 

1565 if ent["entity"] is not None 

1566 }, 

1567 ) 

1568 

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

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

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

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

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

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

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

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

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

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

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

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

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

1582 target_cols = orig_compile_state._adapt_col_list( 

1583 [ 

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

1585 for o in leftmost_attr 

1586 ], 

1587 orig_compile_state._get_current_adapter(), 

1588 ) 

1589 q._raw_columns = target_cols 

1590 

1591 distinct_target_key = leftmost_relationship.distinct_target_key 

1592 

1593 if distinct_target_key is True: 

1594 q._distinct = True 

1595 elif distinct_target_key is None: 

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

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

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

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

1600 q._distinct = True 

1601 break 

1602 

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

1604 if not q._has_row_limiting_clause: 

1605 q._order_by_clauses = () 

1606 

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

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

1609 # when distinct is True is deprecated in the query 

1610 to_add = sql_util.expand_column_list_from_order_by( 

1611 target_cols, q._order_by_clauses 

1612 ) 

1613 if to_add: 

1614 q._set_entities(target_cols + to_add) 

1615 

1616 # the original query now becomes a subquery 

1617 # which we'll join onto. 

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

1619 # here. 

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

1621 left_alias = orm_util.AliasedClass( 

1622 leftmost_mapper, embed_q, use_mapper_path=True 

1623 ) 

1624 return left_alias 

1625 

1626 def _prep_for_joins(self, left_alias, subq_path): 

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

1628 to_join = [] 

1629 pairs = list(subq_path.pairs()) 

1630 

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

1632 if i > 0: 

1633 # look at the previous mapper in the chain - 

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

1635 # mapper, use that instead. 

1636 # note we have an assumption here that 

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

1638 # not an AliasedClass 

1639 

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

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

1642 else: 

1643 to_append = mapper 

1644 

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

1646 

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

1648 # which needs to be aliased. 

1649 

1650 if len(to_join) < 2: 

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

1652 # leftmost "left_alias". 

1653 parent_alias = left_alias 

1654 else: 

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

1656 if info.is_aliased_class: 

1657 parent_alias = info.entity 

1658 else: 

1659 # alias a plain mapper as we may be 

1660 # joining multiple times 

1661 parent_alias = orm_util.AliasedClass( 

1662 info.entity, use_mapper_path=True 

1663 ) 

1664 

1665 local_cols = self.parent_property.local_columns 

1666 

1667 local_attr = [ 

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

1669 for c in local_cols 

1670 ] 

1671 return to_join, local_attr, parent_alias 

1672 

1673 def _apply_joins( 

1674 self, q, to_join, left_alias, parent_alias, effective_entity 

1675 ): 

1676 ltj = len(to_join) 

1677 if ltj == 1: 

1678 to_join = [ 

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

1680 ] 

1681 elif ltj == 2: 

1682 to_join = [ 

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

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

1685 effective_entity 

1686 ), 

1687 ] 

1688 elif ltj > 2: 

1689 middle = [ 

1690 ( 

1691 ( 

1692 orm_util.AliasedClass(item[0]) 

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

1694 else item[0].entity 

1695 ), 

1696 item[1], 

1697 ) 

1698 for item in to_join[1:-1] 

1699 ] 

1700 inner = [] 

1701 

1702 while middle: 

1703 item = middle.pop(0) 

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

1705 if middle: 

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

1707 else: 

1708 attr = attr.of_type(parent_alias) 

1709 

1710 inner.append(attr) 

1711 

1712 to_join = ( 

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

1714 + inner 

1715 + [ 

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

1717 effective_entity 

1718 ) 

1719 ] 

1720 ) 

1721 

1722 for attr in to_join: 

1723 q = q.join(attr) 

1724 

1725 return q 

1726 

1727 def _setup_options( 

1728 self, 

1729 context, 

1730 q, 

1731 subq_path, 

1732 rewritten_path, 

1733 orig_query, 

1734 effective_entity, 

1735 loadopt, 

1736 ): 

1737 # note that because the subqueryload object 

1738 # does not reuse the cached query, instead always making 

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

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

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

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

1743 # brought this into question. 

1744 new_options = orig_query._with_options 

1745 

1746 if loadopt and loadopt._extra_criteria: 

1747 new_options += ( 

1748 orm_util.LoaderCriteriaOption( 

1749 self.entity, 

1750 loadopt._generate_extra_criteria(context), 

1751 ), 

1752 ) 

1753 

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

1755 # these will fire relative to subq_path. 

1756 q = q._with_current_path(rewritten_path) 

1757 q = q.options(*new_options) 

1758 

1759 return q 

1760 

1761 def _setup_outermost_orderby(self, q): 

1762 if self.parent_property.order_by: 

1763 

1764 def _setup_outermost_orderby(compile_context): 

1765 compile_context.eager_order_by += tuple( 

1766 util.to_list(self.parent_property.order_by) 

1767 ) 

1768 

1769 q = q._add_context_option( 

1770 _setup_outermost_orderby, self.parent_property 

1771 ) 

1772 

1773 return q 

1774 

1775 class _SubqCollections: 

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

1777 provide a load interface that executes the query at the 

1778 first moment a value is needed. 

1779 

1780 """ 

1781 

1782 __slots__ = ( 

1783 "session", 

1784 "execution_options", 

1785 "load_options", 

1786 "params", 

1787 "subq", 

1788 "_data", 

1789 ) 

1790 

1791 def __init__(self, context, subq): 

1792 # avoid creating a cycle by storing context 

1793 # even though that's preferable 

1794 self.session = context.session 

1795 self.execution_options = context.execution_options 

1796 self.load_options = context.load_options 

1797 self.params = context.params or {} 

1798 self.subq = subq 

1799 self._data = None 

1800 

1801 def get(self, key, default): 

1802 if self._data is None: 

1803 self._load() 

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

1805 

1806 def _load(self): 

1807 self._data = collections.defaultdict(list) 

1808 

1809 q = self.subq 

1810 assert q.session is None 

1811 

1812 q = q.with_session(self.session) 

1813 

1814 if self.load_options._populate_existing: 

1815 q = q.populate_existing() 

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

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

1818 

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

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

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

1822 

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

1824 if self._data is None: 

1825 self._load() 

1826 

1827 def _setup_query_from_rowproc( 

1828 self, 

1829 context, 

1830 query_entity, 

1831 path, 

1832 entity, 

1833 loadopt, 

1834 adapter, 

1835 ): 

1836 compile_state = context.compile_state 

1837 if ( 

1838 not compile_state.compile_options._enable_eagerloads 

1839 or compile_state.compile_options._for_refresh_state 

1840 ): 

1841 return 

1842 

1843 orig_query_entity_index = compile_state._entities.index(query_entity) 

1844 context.loaders_require_buffering = True 

1845 

1846 path = path[self.parent_property] 

1847 

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

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

1850 with_poly_entity = path.get( 

1851 compile_state.attributes, "path_with_polymorphic", None 

1852 ) 

1853 if with_poly_entity is not None: 

1854 effective_entity = with_poly_entity 

1855 else: 

1856 effective_entity = self.entity 

1857 

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

1859 ("subquery_paths", None), 

1860 (orm_util.PathRegistry.root, orm_util.PathRegistry.root), 

1861 ) 

1862 is_root = subq_path is orm_util.PathRegistry.root 

1863 subq_path = subq_path + path 

1864 rewritten_path = rewritten_path + path 

1865 

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

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

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

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

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

1871 # less time spent but more complicated 

1872 orig_query = context.query._execution_options.get( 

1873 ("orig_query", SubqueryLoader), context.query 

1874 ) 

1875 

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

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

1878 compile_state_cls = ORMCompileState._get_plugin_class_for_plugin( 

1879 orig_query, "orm" 

1880 ) 

1881 

1882 if orig_query._is_lambda_element: 

1883 if context.load_options._lazy_loaded_from is None: 

1884 util.warn( 

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

1886 "at %r in " 

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

1888 "of caching for this statement. Consider using " 

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

1890 % (self, orig_query) 

1891 ) 

1892 orig_query = orig_query._resolved 

1893 

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

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

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

1897 orig_compile_state = compile_state_cls._create_entities_collection( 

1898 orig_query, legacy=False 

1899 ) 

1900 

1901 ( 

1902 leftmost_mapper, 

1903 leftmost_attr, 

1904 leftmost_relationship, 

1905 rewritten_path, 

1906 ) = self._get_leftmost( 

1907 orig_query_entity_index, 

1908 rewritten_path, 

1909 orig_compile_state, 

1910 is_root, 

1911 ) 

1912 

1913 # generate a new Query from the original, then 

1914 # produce a subquery from it. 

1915 left_alias = self._generate_from_original_query( 

1916 orig_compile_state, 

1917 orig_query, 

1918 leftmost_mapper, 

1919 leftmost_attr, 

1920 leftmost_relationship, 

1921 entity, 

1922 ) 

1923 

1924 # generate another Query that will join the 

1925 # left alias to the target relationships. 

1926 # basically doing a longhand 

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

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

1929 

1930 q = query.Query(effective_entity) 

1931 

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

1933 context.execution_options, 

1934 { 

1935 ("orig_query", SubqueryLoader): orig_query, 

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

1937 }, 

1938 ) 

1939 

1940 q = q._set_enable_single_crit(False) 

1941 to_join, local_attr, parent_alias = self._prep_for_joins( 

1942 left_alias, subq_path 

1943 ) 

1944 

1945 q = q.add_columns(*local_attr) 

1946 q = self._apply_joins( 

1947 q, to_join, left_alias, parent_alias, effective_entity 

1948 ) 

1949 

1950 q = self._setup_options( 

1951 context, 

1952 q, 

1953 subq_path, 

1954 rewritten_path, 

1955 orig_query, 

1956 effective_entity, 

1957 loadopt, 

1958 ) 

1959 q = self._setup_outermost_orderby(q) 

1960 

1961 return q 

1962 

1963 def create_row_processor( 

1964 self, 

1965 context, 

1966 query_entity, 

1967 path, 

1968 loadopt, 

1969 mapper, 

1970 result, 

1971 adapter, 

1972 populators, 

1973 ): 

1974 if ( 

1975 loadopt 

1976 and context.compile_state.statement is not None 

1977 and context.compile_state.statement.is_dml 

1978 ): 

1979 util.warn_deprecated( 

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

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

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

1983 "2.0", 

1984 ) 

1985 

1986 if context.refresh_state: 

1987 return self._immediateload_create_row_processor( 

1988 context, 

1989 query_entity, 

1990 path, 

1991 loadopt, 

1992 mapper, 

1993 result, 

1994 adapter, 

1995 populators, 

1996 ) 

1997 

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

1999 context, path, loadopt, self.join_depth 

2000 ) 

2001 if not run_loader: 

2002 return 

2003 

2004 if not isinstance(context.compile_state, ORMSelectCompileState): 

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

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

2007 # is restored here 

2008 return 

2009 

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

2011 raise sa_exc.InvalidRequestError( 

2012 "'%s' does not support object " 

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

2014 ) 

2015 

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

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

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

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

2020 if len(path) == 1: 

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

2022 return 

2023 elif not orm_util._entity_isa( 

2024 path[-1], self.parent 

2025 ) and not self.parent.isa(path[-1].mapper): 

2026 # second check accommodates a polymorphic entity where 

2027 # the path has been normalized to the base mapper but 

2028 # self.parent is a subclass mapper. Fixes #13209. 

2029 return 

2030 

2031 subq = self._setup_query_from_rowproc( 

2032 context, 

2033 query_entity, 

2034 path, 

2035 path[-1], 

2036 loadopt, 

2037 adapter, 

2038 ) 

2039 

2040 if subq is None: 

2041 return 

2042 

2043 assert subq.session is None 

2044 

2045 path = path[self.parent_property] 

2046 

2047 local_cols = self.parent_property.local_columns 

2048 

2049 # cache the loaded collections in the context 

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

2051 # call upon create_row_processor again 

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

2053 if collections is None: 

2054 collections = self._SubqCollections(context, subq) 

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

2056 

2057 if adapter: 

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

2059 

2060 if self.uselist: 

2061 self._create_collection_loader( 

2062 context, result, collections, local_cols, populators 

2063 ) 

2064 else: 

2065 self._create_scalar_loader( 

2066 context, result, collections, local_cols, populators 

2067 ) 

2068 

2069 def _create_collection_loader( 

2070 self, context, result, collections, local_cols, populators 

2071 ): 

2072 tuple_getter = result._tuple_getter(local_cols) 

2073 

2074 def load_collection_from_subq(state, dict_, row): 

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

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

2077 state, dict_, collection 

2078 ) 

2079 

2080 def load_collection_from_subq_existing_row(state, dict_, row): 

2081 if self.key not in dict_: 

2082 load_collection_from_subq(state, dict_, row) 

2083 

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

2085 populators["existing"].append( 

2086 (self.key, load_collection_from_subq_existing_row) 

2087 ) 

2088 

2089 if context.invoke_all_eagers: 

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

2091 

2092 def _create_scalar_loader( 

2093 self, context, result, collections, local_cols, populators 

2094 ): 

2095 tuple_getter = result._tuple_getter(local_cols) 

2096 

2097 def load_scalar_from_subq(state, dict_, row): 

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

2099 if len(collection) > 1: 

2100 util.warn( 

2101 "Multiple rows returned with " 

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

2103 ) 

2104 

2105 scalar = collection[0] 

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

2107 

2108 def load_scalar_from_subq_existing_row(state, dict_, row): 

2109 if self.key not in dict_: 

2110 load_scalar_from_subq(state, dict_, row) 

2111 

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

2113 populators["existing"].append( 

2114 (self.key, load_scalar_from_subq_existing_row) 

2115 ) 

2116 if context.invoke_all_eagers: 

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

2118 

2119 

2120@log.class_logger 

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

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

2123class JoinedLoader(AbstractRelationshipLoader): 

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

2125 using joined eager loading. 

2126 

2127 """ 

2128 

2129 __slots__ = "join_depth" 

2130 

2131 def __init__(self, parent, strategy_key): 

2132 super().__init__(parent, strategy_key) 

2133 self.join_depth = self.parent_property.join_depth 

2134 

2135 def init_class_attribute(self, mapper): 

2136 self.parent_property._get_strategy( 

2137 (("lazy", "select"),) 

2138 ).init_class_attribute(mapper) 

2139 

2140 def setup_query( 

2141 self, 

2142 compile_state, 

2143 query_entity, 

2144 path, 

2145 loadopt, 

2146 adapter, 

2147 column_collection=None, 

2148 parentmapper=None, 

2149 chained_from_outerjoin=False, 

2150 **kwargs, 

2151 ): 

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

2153 

2154 if not compile_state.compile_options._enable_eagerloads: 

2155 return 

2156 elif ( 

2157 loadopt 

2158 and compile_state.statement is not None 

2159 and compile_state.statement.is_dml 

2160 ): 

2161 util.warn_deprecated( 

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

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

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

2165 "2.0", 

2166 ) 

2167 elif self.uselist: 

2168 compile_state.multi_row_eager_loaders = True 

2169 

2170 path = path[self.parent_property] 

2171 

2172 user_defined_adapter = ( 

2173 self._init_user_defined_eager_proc( 

2174 loadopt, compile_state, compile_state.attributes 

2175 ) 

2176 if loadopt 

2177 else False 

2178 ) 

2179 

2180 if user_defined_adapter is not False: 

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

2182 # in the query 

2183 ( 

2184 clauses, 

2185 adapter, 

2186 add_to_collection, 

2187 ) = self._setup_query_on_user_defined_adapter( 

2188 compile_state, 

2189 query_entity, 

2190 path, 

2191 adapter, 

2192 user_defined_adapter, 

2193 ) 

2194 

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

2196 # limited/distinct SELECT, 

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

2198 

2199 else: 

2200 # if not via query option, check for 

2201 # a cycle 

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

2203 if self.join_depth: 

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

2205 return 

2206 elif path.contains_mapper(self.mapper): 

2207 return 

2208 

2209 # add the JOIN and create an adapter 

2210 ( 

2211 clauses, 

2212 adapter, 

2213 add_to_collection, 

2214 chained_from_outerjoin, 

2215 ) = self._generate_row_adapter( 

2216 compile_state, 

2217 query_entity, 

2218 path, 

2219 loadopt, 

2220 adapter, 

2221 column_collection, 

2222 parentmapper, 

2223 chained_from_outerjoin, 

2224 ) 

2225 

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

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

2228 compile_state.eager_adding_joins = True 

2229 

2230 with_poly_entity = path.get( 

2231 compile_state.attributes, "path_with_polymorphic", None 

2232 ) 

2233 if with_poly_entity is not None: 

2234 with_polymorphic = inspect( 

2235 with_poly_entity 

2236 ).with_polymorphic_mappers 

2237 else: 

2238 with_polymorphic = None 

2239 

2240 path = path[self.entity] 

2241 

2242 loading._setup_entity_query( 

2243 compile_state, 

2244 self.mapper, 

2245 query_entity, 

2246 path, 

2247 clauses, 

2248 add_to_collection, 

2249 with_polymorphic=with_polymorphic, 

2250 parentmapper=self.mapper, 

2251 chained_from_outerjoin=chained_from_outerjoin, 

2252 ) 

2253 

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

2255 

2256 if has_nones: 

2257 if with_poly_entity is not None: 

2258 raise sa_exc.InvalidRequestError( 

2259 "Detected unaliased columns when generating joined " 

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

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

2262 ) 

2263 else: 

2264 compile_state.secondary_columns = [ 

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

2266 ] 

2267 

2268 def _init_user_defined_eager_proc( 

2269 self, loadopt, compile_state, target_attributes 

2270 ): 

2271 # check if the opt applies at all 

2272 if "eager_from_alias" not in loadopt.local_opts: 

2273 # nope 

2274 return False 

2275 

2276 path = loadopt.path.parent 

2277 

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

2279 # has been built up. 

2280 adapter = path.get( 

2281 compile_state.attributes, "user_defined_eager_row_processor", False 

2282 ) 

2283 if adapter is not False: 

2284 # just return it 

2285 return adapter 

2286 

2287 # otherwise figure it out. 

2288 alias = loadopt.local_opts["eager_from_alias"] 

2289 root_mapper, prop = path[-2:] 

2290 

2291 if alias is not None: 

2292 if isinstance(alias, str): 

2293 alias = prop.target.alias(alias) 

2294 adapter = orm_util.ORMAdapter( 

2295 orm_util._TraceAdaptRole.JOINEDLOAD_USER_DEFINED_ALIAS, 

2296 prop.mapper, 

2297 selectable=alias, 

2298 equivalents=prop.mapper._equivalent_columns, 

2299 limit_on_entity=False, 

2300 ) 

2301 else: 

2302 if path.contains( 

2303 compile_state.attributes, "path_with_polymorphic" 

2304 ): 

2305 with_poly_entity = path.get( 

2306 compile_state.attributes, "path_with_polymorphic" 

2307 ) 

2308 adapter = orm_util.ORMAdapter( 

2309 orm_util._TraceAdaptRole.JOINEDLOAD_PATH_WITH_POLYMORPHIC, 

2310 with_poly_entity, 

2311 equivalents=prop.mapper._equivalent_columns, 

2312 ) 

2313 else: 

2314 adapter = compile_state._polymorphic_adapters.get( 

2315 prop.mapper, None 

2316 ) 

2317 path.set( 

2318 target_attributes, 

2319 "user_defined_eager_row_processor", 

2320 adapter, 

2321 ) 

2322 

2323 return adapter 

2324 

2325 def _setup_query_on_user_defined_adapter( 

2326 self, context, entity, path, adapter, user_defined_adapter 

2327 ): 

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

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

2330 adapter = entity._get_entity_clauses(context) 

2331 

2332 if adapter and user_defined_adapter: 

2333 user_defined_adapter = user_defined_adapter.wrap(adapter) 

2334 path.set( 

2335 context.attributes, 

2336 "user_defined_eager_row_processor", 

2337 user_defined_adapter, 

2338 ) 

2339 elif adapter: 

2340 user_defined_adapter = adapter 

2341 path.set( 

2342 context.attributes, 

2343 "user_defined_eager_row_processor", 

2344 user_defined_adapter, 

2345 ) 

2346 

2347 add_to_collection = context.primary_columns 

2348 return user_defined_adapter, adapter, add_to_collection 

2349 

2350 def _generate_row_adapter( 

2351 self, 

2352 compile_state, 

2353 entity, 

2354 path, 

2355 loadopt, 

2356 adapter, 

2357 column_collection, 

2358 parentmapper, 

2359 chained_from_outerjoin, 

2360 ): 

2361 with_poly_entity = path.get( 

2362 compile_state.attributes, "path_with_polymorphic", None 

2363 ) 

2364 if with_poly_entity: 

2365 to_adapt = with_poly_entity 

2366 else: 

2367 insp = inspect(self.entity) 

2368 if insp.is_aliased_class: 

2369 alt_selectable = insp.selectable 

2370 else: 

2371 alt_selectable = None 

2372 

2373 to_adapt = orm_util.AliasedClass( 

2374 self.mapper, 

2375 alias=( 

2376 alt_selectable._anonymous_fromclause(flat=True) 

2377 if alt_selectable is not None 

2378 else None 

2379 ), 

2380 flat=True, 

2381 use_mapper_path=True, 

2382 ) 

2383 

2384 to_adapt_insp = inspect(to_adapt) 

2385 

2386 clauses = to_adapt_insp._memo( 

2387 ("joinedloader_ormadapter", self), 

2388 orm_util.ORMAdapter, 

2389 orm_util._TraceAdaptRole.JOINEDLOAD_MEMOIZED_ADAPTER, 

2390 to_adapt_insp, 

2391 equivalents=self.mapper._equivalent_columns, 

2392 adapt_required=True, 

2393 allow_label_resolve=False, 

2394 anonymize_labels=True, 

2395 ) 

2396 

2397 assert clauses.is_aliased_class 

2398 

2399 innerjoin = ( 

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

2401 if loadopt is not None 

2402 else self.parent_property.innerjoin 

2403 ) 

2404 

2405 if not innerjoin: 

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

2407 # this path must also be outer joins 

2408 chained_from_outerjoin = True 

2409 

2410 compile_state.create_eager_joins.append( 

2411 ( 

2412 self._create_eager_join, 

2413 entity, 

2414 path, 

2415 adapter, 

2416 parentmapper, 

2417 clauses, 

2418 innerjoin, 

2419 chained_from_outerjoin, 

2420 loadopt._extra_criteria if loadopt else (), 

2421 ) 

2422 ) 

2423 

2424 add_to_collection = compile_state.secondary_columns 

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

2426 

2427 return clauses, adapter, add_to_collection, chained_from_outerjoin 

2428 

2429 def _create_eager_join( 

2430 self, 

2431 compile_state, 

2432 query_entity, 

2433 path, 

2434 adapter, 

2435 parentmapper, 

2436 clauses, 

2437 innerjoin, 

2438 chained_from_outerjoin, 

2439 extra_criteria, 

2440 ): 

2441 if parentmapper is None: 

2442 localparent = query_entity.mapper 

2443 else: 

2444 localparent = parentmapper 

2445 

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

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

2448 # LIMIT/OFFSET etc.) 

2449 should_nest_selectable = ( 

2450 compile_state.multi_row_eager_loaders 

2451 and compile_state._should_nest_selectable 

2452 ) 

2453 

2454 query_entity_key = None 

2455 

2456 if ( 

2457 query_entity not in compile_state.eager_joins 

2458 and not should_nest_selectable 

2459 and compile_state.from_clauses 

2460 ): 

2461 indexes = sql_util.find_left_clause_that_matches_given( 

2462 compile_state.from_clauses, query_entity.selectable 

2463 ) 

2464 

2465 if len(indexes) > 1: 

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

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

2468 raise sa_exc.InvalidRequestError( 

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

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

2471 "specifying the join path." 

2472 ) 

2473 

2474 if indexes: 

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

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

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

2478 # Query._compile_context will adapt as needed and 

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

2480 query_entity_key, default_towrap = indexes[0], clause 

2481 

2482 if query_entity_key is None: 

2483 query_entity_key, default_towrap = ( 

2484 query_entity, 

2485 query_entity.selectable, 

2486 ) 

2487 

2488 towrap = compile_state.eager_joins.setdefault( 

2489 query_entity_key, default_towrap 

2490 ) 

2491 

2492 if adapter: 

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

2494 # joining from an adapted entity. The adapted entity 

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

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

2497 # name on it. 

2498 efm = adapter.aliased_insp._entity_for_mapper( 

2499 localparent 

2500 if localparent.isa(self.parent) 

2501 else self.parent 

2502 ) 

2503 

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

2505 # to our straight property 

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

2507 else: 

2508 onclause = getattr( 

2509 orm_util.AliasedClass( 

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

2511 ), 

2512 self.key, 

2513 self.parent_property, 

2514 ) 

2515 

2516 else: 

2517 onclause = self.parent_property 

2518 

2519 assert clauses.is_aliased_class 

2520 

2521 attach_on_outside = ( 

2522 not chained_from_outerjoin 

2523 or not innerjoin 

2524 or innerjoin == "unnested" 

2525 or query_entity.entity_zero.represents_outer_join 

2526 ) 

2527 

2528 extra_join_criteria = extra_criteria 

2529 additional_entity_criteria = compile_state.global_attributes.get( 

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

2531 ) 

2532 if additional_entity_criteria: 

2533 extra_join_criteria += tuple( 

2534 ae._resolve_where_criteria(self.mapper) 

2535 for ae in additional_entity_criteria 

2536 if ae.propagate_to_loaders 

2537 ) 

2538 

2539 if attach_on_outside: 

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

2541 eagerjoin = orm_util._ORMJoin( 

2542 towrap, 

2543 clauses.aliased_insp, 

2544 onclause, 

2545 isouter=not innerjoin 

2546 or query_entity.entity_zero.represents_outer_join 

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

2548 _left_memo=self.parent, 

2549 _right_memo=path[self.mapper], 

2550 _extra_criteria=extra_join_criteria, 

2551 ) 

2552 else: 

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

2554 eagerjoin = self._splice_nested_inner_join( 

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

2556 ) 

2557 

2558 compile_state.eager_joins[query_entity_key] = eagerjoin 

2559 

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

2561 eagerjoin.stop_on = query_entity.selectable 

2562 

2563 if not parentmapper: 

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

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

2566 # in the 

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

2568 # by the Query propagates those columns outward. 

2569 # This has the effect 

2570 # of "undefering" those columns. 

2571 for col in sql_util._find_columns( 

2572 self.parent_property.primaryjoin 

2573 ): 

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

2575 if adapter: 

2576 col = adapter.columns[col] 

2577 compile_state._append_dedupe_col_collection( 

2578 col, compile_state.primary_columns 

2579 ) 

2580 

2581 if self.parent_property.order_by: 

2582 compile_state.eager_order_by += tuple( 

2583 (eagerjoin._target_adapter.copy_and_process)( 

2584 util.to_list(self.parent_property.order_by) 

2585 ) 

2586 ) 

2587 

2588 def _splice_nested_inner_join( 

2589 self, 

2590 path, 

2591 entity_we_want_to_splice_onto, 

2592 join_obj, 

2593 clauses, 

2594 onclause, 

2595 extra_criteria, 

2596 entity_inside_join_structure: Union[ 

2597 Mapper, None, Literal[False] 

2598 ] = False, 

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

2600 ): 

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

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

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

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

2605 # clause representing this mapper; if this is not 

2606 # entity_we_want_to_splice_onto then return None to end the recursive 

2607 # branch 

2608 

2609 assert entity_we_want_to_splice_onto is path[-2] 

2610 

2611 if entity_inside_join_structure is False: 

2612 assert isinstance(join_obj, orm_util._ORMJoin) 

2613 

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

2615 # FromGrouping - continue descending into the structure 

2616 return self._splice_nested_inner_join( 

2617 path, 

2618 entity_we_want_to_splice_onto, 

2619 join_obj.element, 

2620 clauses, 

2621 onclause, 

2622 extra_criteria, 

2623 entity_inside_join_structure, 

2624 ) 

2625 elif isinstance(join_obj, orm_util._ORMJoin): 

2626 # _ORMJoin - continue descending into the structure 

2627 

2628 join_right_path = join_obj._right_memo 

2629 

2630 # see if right side of join is viable 

2631 target_join = self._splice_nested_inner_join( 

2632 path, 

2633 entity_we_want_to_splice_onto, 

2634 join_obj.right, 

2635 clauses, 

2636 onclause, 

2637 extra_criteria, 

2638 entity_inside_join_structure=( 

2639 join_right_path[-1].mapper 

2640 if join_right_path is not None 

2641 else None 

2642 ), 

2643 ) 

2644 

2645 if target_join is not None: 

2646 # for a right splice, attempt to flatten out 

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

2648 # parenthesis nesting 

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

2650 eagerjoin = join_obj._splice_into_center(target_join) 

2651 else: 

2652 eagerjoin = orm_util._ORMJoin( 

2653 join_obj.left, 

2654 target_join, 

2655 join_obj.onclause, 

2656 isouter=join_obj.isouter, 

2657 _left_memo=join_obj._left_memo, 

2658 ) 

2659 

2660 eagerjoin._target_adapter = target_join._target_adapter 

2661 return eagerjoin 

2662 

2663 else: 

2664 # see if left side of join is viable 

2665 target_join = self._splice_nested_inner_join( 

2666 path, 

2667 entity_we_want_to_splice_onto, 

2668 join_obj.left, 

2669 clauses, 

2670 onclause, 

2671 extra_criteria, 

2672 entity_inside_join_structure=join_obj._left_memo, 

2673 detected_existing_path=join_right_path, 

2674 ) 

2675 

2676 if target_join is not None: 

2677 eagerjoin = orm_util._ORMJoin( 

2678 target_join, 

2679 join_obj.right, 

2680 join_obj.onclause, 

2681 isouter=join_obj.isouter, 

2682 _right_memo=join_obj._right_memo, 

2683 ) 

2684 eagerjoin._target_adapter = target_join._target_adapter 

2685 return eagerjoin 

2686 

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

2688 # most call 

2689 if entity_inside_join_structure is False: 

2690 assert ( 

2691 False 

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

2693 return None 

2694 

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

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

2697 

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

2699 if not entity_we_want_to_splice_onto.isa(entity_inside_join_structure): 

2700 return None 

2701 

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

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

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

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

2706 # updated in issue #11965. 

2707 if detected_existing_path and len(detected_existing_path) > 2: 

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

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

2710 # entity_inside_join_structure=join_obj._left_memo 

2711 # and entity_inside_join_structure=join_obj._right_memo.mapper 

2712 assert detected_existing_path[-3] is entity_inside_join_structure 

2713 

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

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

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

2717 # if not, then this is not our target 

2718 # 

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

2720 # 

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

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

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

2724 # 

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

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

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

2728 # ->common_node->Node 

2729 # 

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

2731 # 

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

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

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

2735 # 

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

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

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

2739 # we might represent in a pseudopath as: 

2740 # 

2741 # B->c1s->C1 

2742 # ->c2s->C2 

2743 # 

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

2745 # JOIN C2 ON <c2s> 

2746 # 

2747 

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

2749 return None 

2750 

2751 return orm_util._ORMJoin( 

2752 join_obj, 

2753 clauses.aliased_insp, 

2754 onclause, 

2755 isouter=False, 

2756 _left_memo=entity_inside_join_structure, 

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

2758 _extra_criteria=extra_criteria, 

2759 ) 

2760 

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

2762 compile_state = context.compile_state 

2763 

2764 user_defined_adapter = ( 

2765 self._init_user_defined_eager_proc( 

2766 loadopt, compile_state, context.attributes 

2767 ) 

2768 if loadopt 

2769 else False 

2770 ) 

2771 

2772 if user_defined_adapter is not False: 

2773 decorator = user_defined_adapter 

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

2775 # portion of the load. 

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

2777 if compile_state.compound_eager_adapter and decorator: 

2778 decorator = decorator.wrap( 

2779 compile_state.compound_eager_adapter 

2780 ) 

2781 elif compile_state.compound_eager_adapter: 

2782 decorator = compile_state.compound_eager_adapter 

2783 else: 

2784 decorator = path.get( 

2785 compile_state.attributes, "eager_row_processor" 

2786 ) 

2787 if decorator is None: 

2788 return False 

2789 

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

2791 return decorator 

2792 else: 

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

2794 # processor, will cause a degrade to lazy 

2795 return False 

2796 

2797 def create_row_processor( 

2798 self, 

2799 context, 

2800 query_entity, 

2801 path, 

2802 loadopt, 

2803 mapper, 

2804 result, 

2805 adapter, 

2806 populators, 

2807 ): 

2808 

2809 if not context.compile_state.compile_options._enable_eagerloads: 

2810 return 

2811 

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

2813 raise sa_exc.InvalidRequestError( 

2814 "'%s' does not support object " 

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

2816 ) 

2817 

2818 if self.uselist: 

2819 context.loaders_require_uniquing = True 

2820 

2821 our_path = path[self.parent_property] 

2822 

2823 eager_adapter = self._create_eager_adapter( 

2824 context, result, adapter, our_path, loadopt 

2825 ) 

2826 

2827 if eager_adapter is not False: 

2828 key = self.key 

2829 

2830 _instance = loading._instance_processor( 

2831 query_entity, 

2832 self.mapper, 

2833 context, 

2834 result, 

2835 our_path[self.entity], 

2836 eager_adapter, 

2837 ) 

2838 

2839 if not self.uselist: 

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

2841 else: 

2842 self._create_collection_loader( 

2843 context, key, _instance, populators 

2844 ) 

2845 else: 

2846 self.parent_property._get_strategy( 

2847 (("lazy", "select"),) 

2848 ).create_row_processor( 

2849 context, 

2850 query_entity, 

2851 path, 

2852 loadopt, 

2853 mapper, 

2854 result, 

2855 adapter, 

2856 populators, 

2857 ) 

2858 

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

2860 def load_collection_from_joined_new_row(state, dict_, row): 

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

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

2863 # populate_existing(). 

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

2865 result_list = util.UniqueAppender( 

2866 collection, "append_without_event" 

2867 ) 

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

2869 inst = _instance(row) 

2870 if inst is not None: 

2871 result_list.append(inst) 

2872 

2873 def load_collection_from_joined_existing_row(state, dict_, row): 

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

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

2876 else: 

2877 # appender_key can be absent from context.attributes 

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

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

2880 # distinct sets of result columns 

2881 collection = attributes.init_state_collection( 

2882 state, dict_, key 

2883 ) 

2884 result_list = util.UniqueAppender( 

2885 collection, "append_without_event" 

2886 ) 

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

2888 inst = _instance(row) 

2889 if inst is not None: 

2890 result_list.append(inst) 

2891 

2892 def load_collection_from_joined_exec(state, dict_, row): 

2893 _instance(row) 

2894 

2895 populators["new"].append( 

2896 (self.key, load_collection_from_joined_new_row) 

2897 ) 

2898 populators["existing"].append( 

2899 (self.key, load_collection_from_joined_existing_row) 

2900 ) 

2901 if context.invoke_all_eagers: 

2902 populators["eager"].append( 

2903 (self.key, load_collection_from_joined_exec) 

2904 ) 

2905 

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

2907 def load_scalar_from_joined_new_row(state, dict_, row): 

2908 # set a scalar object instance directly on the parent 

2909 # object, bypassing InstrumentedAttribute event handlers. 

2910 dict_[key] = _instance(row) 

2911 

2912 def load_scalar_from_joined_existing_row(state, dict_, row): 

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

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

2915 existing = _instance(row) 

2916 

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

2918 if key in dict_: 

2919 if existing is not dict_[key]: 

2920 util.warn( 

2921 "Multiple rows returned with " 

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

2923 % self 

2924 ) 

2925 else: 

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

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

2928 # that the other doesn't. 

2929 dict_[key] = existing 

2930 

2931 def load_scalar_from_joined_exec(state, dict_, row): 

2932 _instance(row) 

2933 

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

2935 populators["existing"].append( 

2936 (self.key, load_scalar_from_joined_existing_row) 

2937 ) 

2938 if context.invoke_all_eagers: 

2939 populators["eager"].append( 

2940 (self.key, load_scalar_from_joined_exec) 

2941 ) 

2942 

2943 

2944@log.class_logger 

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

2946class SelectInLoader(PostLoader, util.MemoizedSlots): 

2947 __slots__ = ( 

2948 "join_depth", 

2949 "omit_join", 

2950 "_parent_alias", 

2951 "_query_info", 

2952 "_fallback_query_info", 

2953 ) 

2954 

2955 query_info = collections.namedtuple( 

2956 "queryinfo", 

2957 [ 

2958 "load_only_child", 

2959 "load_with_join", 

2960 "in_expr", 

2961 "pk_cols", 

2962 "zero_idx", 

2963 "child_lookup_cols", 

2964 ], 

2965 ) 

2966 

2967 _chunksize = 500 

2968 

2969 def __init__(self, parent, strategy_key): 

2970 super().__init__(parent, strategy_key) 

2971 self.join_depth = self.parent_property.join_depth 

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

2973 

2974 if self.parent_property.omit_join is not None: 

2975 self.omit_join = self.parent_property.omit_join 

2976 else: 

2977 lazyloader = self.parent_property._get_strategy( 

2978 (("lazy", "select"),) 

2979 ) 

2980 if is_m2o: 

2981 self.omit_join = lazyloader.use_get 

2982 else: 

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

2984 lazyloader._rev_lazywhere, 

2985 use_proxies=True, 

2986 compare_keys=False, 

2987 equivalents=self.parent._equivalent_columns, 

2988 ) 

2989 

2990 if self.omit_join: 

2991 if is_m2o: 

2992 self._query_info = self._init_for_omit_join_m2o() 

2993 self._fallback_query_info = self._init_for_join() 

2994 else: 

2995 self._query_info = self._init_for_omit_join() 

2996 else: 

2997 self._query_info = self._init_for_join() 

2998 

2999 def _init_for_omit_join(self): 

3000 pk_to_fk = dict( 

3001 self.parent_property._join_condition.local_remote_pairs 

3002 ) 

3003 pk_to_fk.update( 

3004 (equiv, pk_to_fk[k]) 

3005 for k in list(pk_to_fk) 

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

3007 ) 

3008 

3009 pk_cols = fk_cols = [ 

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

3011 ] 

3012 if len(fk_cols) > 1: 

3013 in_expr = sql.tuple_(*fk_cols) 

3014 zero_idx = False 

3015 else: 

3016 in_expr = fk_cols[0] 

3017 zero_idx = True 

3018 

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

3020 

3021 def _init_for_omit_join_m2o(self): 

3022 pk_cols = self.mapper.primary_key 

3023 if len(pk_cols) > 1: 

3024 in_expr = sql.tuple_(*pk_cols) 

3025 zero_idx = False 

3026 else: 

3027 in_expr = pk_cols[0] 

3028 zero_idx = True 

3029 

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

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

3032 

3033 return self.query_info( 

3034 True, False, in_expr, pk_cols, zero_idx, lookup_cols 

3035 ) 

3036 

3037 def _init_for_join(self): 

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

3039 pa_insp = inspect(self._parent_alias) 

3040 pk_cols = [ 

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

3042 ] 

3043 if len(pk_cols) > 1: 

3044 in_expr = sql.tuple_(*pk_cols) 

3045 zero_idx = False 

3046 else: 

3047 in_expr = pk_cols[0] 

3048 zero_idx = True 

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

3050 

3051 def init_class_attribute(self, mapper): 

3052 self.parent_property._get_strategy( 

3053 (("lazy", "select"),) 

3054 ).init_class_attribute(mapper) 

3055 

3056 def create_row_processor( 

3057 self, 

3058 context, 

3059 query_entity, 

3060 path, 

3061 loadopt, 

3062 mapper, 

3063 result, 

3064 adapter, 

3065 populators, 

3066 ): 

3067 if context.refresh_state: 

3068 return self._immediateload_create_row_processor( 

3069 context, 

3070 query_entity, 

3071 path, 

3072 loadopt, 

3073 mapper, 

3074 result, 

3075 adapter, 

3076 populators, 

3077 ) 

3078 

3079 ( 

3080 effective_path, 

3081 run_loader, 

3082 execution_options, 

3083 recursion_depth, 

3084 ) = self._setup_for_recursion( 

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

3086 ) 

3087 

3088 if not run_loader: 

3089 return 

3090 

3091 if not context.compile_state.compile_options._enable_eagerloads: 

3092 return 

3093 

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

3095 raise sa_exc.InvalidRequestError( 

3096 "'%s' does not support object " 

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

3098 ) 

3099 

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

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

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

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

3104 if len(path) == 1: 

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

3106 return 

3107 elif not orm_util._entity_isa( 

3108 path[-1], self.parent 

3109 ) and not self.parent.isa(path[-1].mapper): 

3110 # second check accommodates a polymorphic entity where 

3111 # the path has been normalized to the base mapper but 

3112 # self.parent is a subclass mapper, e.g. 

3113 # joinedload(A.b.of_type(poly)).selectinload(poly.Sub.rel) 

3114 # Fixes #13209. 

3115 return 

3116 

3117 selectin_path = effective_path 

3118 

3119 path_w_prop = path[self.parent_property] 

3120 

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

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

3123 with_poly_entity = path_w_prop.get( 

3124 context.attributes, "path_with_polymorphic", None 

3125 ) 

3126 if with_poly_entity is not None: 

3127 effective_entity = inspect(with_poly_entity) 

3128 else: 

3129 effective_entity = self.entity 

3130 

3131 loading.PostLoad.callable_for_path( 

3132 context, 

3133 selectin_path, 

3134 self.parent, 

3135 self.parent_property, 

3136 self._load_for_path, 

3137 effective_entity, 

3138 loadopt, 

3139 recursion_depth, 

3140 execution_options, 

3141 ) 

3142 

3143 def _load_for_path( 

3144 self, 

3145 context, 

3146 path, 

3147 states, 

3148 load_only, 

3149 effective_entity, 

3150 loadopt, 

3151 recursion_depth, 

3152 execution_options, 

3153 ): 

3154 if load_only and self.key not in load_only: 

3155 return 

3156 

3157 query_info = self._query_info 

3158 

3159 if query_info.load_only_child: 

3160 our_states = collections.defaultdict(list) 

3161 none_states = [] 

3162 

3163 mapper = self.parent 

3164 

3165 for state, overwrite in states: 

3166 state_dict = state.dict 

3167 related_ident = tuple( 

3168 mapper._get_state_attr_by_column( 

3169 state, 

3170 state_dict, 

3171 lk, 

3172 passive=attributes.PASSIVE_NO_FETCH, 

3173 ) 

3174 for lk in query_info.child_lookup_cols 

3175 ) 

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

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

3178 # version of selectinload 

3179 if LoaderCallableStatus.PASSIVE_NO_RESULT in related_ident: 

3180 query_info = self._fallback_query_info 

3181 break 

3182 

3183 # organize states into lists keyed to particular foreign 

3184 # key values. 

3185 if None not in related_ident: 

3186 our_states[related_ident].append( 

3187 (state, state_dict, overwrite) 

3188 ) 

3189 else: 

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

3191 # separate collection that will be populated separately 

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

3193 

3194 # note the above conditional may have changed query_info 

3195 if not query_info.load_only_child: 

3196 our_states = [ 

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

3198 for state, overwrite in states 

3199 ] 

3200 

3201 pk_cols = query_info.pk_cols 

3202 in_expr = query_info.in_expr 

3203 

3204 if not query_info.load_with_join: 

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

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

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

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

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

3210 # parent entity and do not need adaption. 

3211 if effective_entity.is_aliased_class: 

3212 pk_cols = [ 

3213 effective_entity._adapt_element(col) for col in pk_cols 

3214 ] 

3215 in_expr = effective_entity._adapt_element(in_expr) 

3216 

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

3218 bundle_sql = bundle_ent.__clause_element__() 

3219 

3220 entity_sql = effective_entity.__clause_element__() 

3221 q = Select._create_raw_select( 

3222 _raw_columns=[bundle_sql, entity_sql], 

3223 _label_style=LABEL_STYLE_TABLENAME_PLUS_COL, 

3224 _compile_options=ORMCompileState.default_compile_options, 

3225 _propagate_attrs={ 

3226 "compile_state_plugin": "orm", 

3227 "plugin_subject": effective_entity, 

3228 }, 

3229 ) 

3230 

3231 if not query_info.load_with_join: 

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

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

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

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

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

3237 q = q.select_from(effective_entity) 

3238 else: 

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

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

3241 # occur in this case. 

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

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

3244 effective_entity 

3245 ) 

3246 ) 

3247 

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

3249 

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

3251 # test_selectin_relations.py -> test_twolevel_selectin_w_polymorphic 

3252 # 

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

3254 # statement, namely this one: 

3255 orig_query = context.compile_state.select_statement 

3256 

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

3258 # context_query = context.user_passed_query 

3259 # 

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

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

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

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

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

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

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

3267 # 

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

3269 # lazyload() that have a path need 

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

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

3272 # of the strategy object. however other options like 

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

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

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

3276 # 

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

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

3279 

3280 effective_path = path[self.parent_property] 

3281 

3282 if orig_query is context.user_passed_query: 

3283 new_options = orig_query._with_options 

3284 else: 

3285 cached_options = orig_query._with_options 

3286 uncached_options = context.user_passed_query._with_options 

3287 

3288 # propagate compile state options from the original query, 

3289 # updating their "extra_criteria" as necessary. 

3290 # note this will create a different cache key than 

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

3292 # of extra_criteria will have different boundparam than that of 

3293 # the QueryableAttribute in the path 

3294 new_options = [ 

3295 orig_opt._adapt_cached_option_to_uncached_option( 

3296 context, uncached_opt 

3297 ) 

3298 for orig_opt, uncached_opt in zip( 

3299 cached_options, uncached_options 

3300 ) 

3301 ] 

3302 

3303 if loadopt and loadopt._extra_criteria: 

3304 new_options += ( 

3305 orm_util.LoaderCriteriaOption( 

3306 effective_entity, 

3307 loadopt._generate_extra_criteria(context), 

3308 ), 

3309 ) 

3310 

3311 if recursion_depth is not None: 

3312 effective_path = effective_path._truncate_recursive() 

3313 

3314 q = q.options(*new_options) 

3315 

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

3317 if context.populate_existing: 

3318 q = q.execution_options(populate_existing=True) 

3319 

3320 if self.parent_property.order_by: 

3321 if not query_info.load_with_join: 

3322 eager_order_by = self.parent_property.order_by 

3323 if effective_entity.is_aliased_class: 

3324 eager_order_by = [ 

3325 effective_entity._adapt_element(elem) 

3326 for elem in eager_order_by 

3327 ] 

3328 q = q.order_by(*eager_order_by) 

3329 else: 

3330 

3331 def _setup_outermost_orderby(compile_context): 

3332 compile_context.eager_order_by += tuple( 

3333 util.to_list(self.parent_property.order_by) 

3334 ) 

3335 

3336 q = q._add_context_option( 

3337 _setup_outermost_orderby, self.parent_property 

3338 ) 

3339 

3340 if query_info.load_only_child: 

3341 self._load_via_child( 

3342 our_states, 

3343 none_states, 

3344 query_info, 

3345 q, 

3346 context, 

3347 execution_options, 

3348 ) 

3349 else: 

3350 self._load_via_parent( 

3351 our_states, query_info, q, context, execution_options 

3352 ) 

3353 

3354 def _load_via_child( 

3355 self, 

3356 our_states, 

3357 none_states, 

3358 query_info, 

3359 q, 

3360 context, 

3361 execution_options, 

3362 ): 

3363 uselist = self.uselist 

3364 

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

3366 our_keys = sorted(our_states) 

3367 while our_keys: 

3368 chunk = our_keys[0 : self._chunksize] 

3369 our_keys = our_keys[self._chunksize :] 

3370 data = { 

3371 k: v 

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

3373 q, 

3374 params={ 

3375 "primary_keys": [ 

3376 key[0] if query_info.zero_idx else key 

3377 for key in chunk 

3378 ] 

3379 }, 

3380 execution_options=execution_options, 

3381 ).unique() 

3382 } 

3383 

3384 for key in chunk: 

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

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

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

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

3389 # related object. 

3390 related_obj = data.get(key, None) 

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

3392 if not overwrite and self.key in dict_: 

3393 continue 

3394 

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

3396 state, 

3397 dict_, 

3398 related_obj if not uselist else [related_obj], 

3399 ) 

3400 # populate none states with empty value / collection 

3401 for state, dict_, overwrite in none_states: 

3402 if not overwrite and self.key in dict_: 

3403 continue 

3404 

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

3406 # collection will be populated 

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

3408 

3409 def _load_via_parent( 

3410 self, our_states, query_info, q, context, execution_options 

3411 ): 

3412 uselist = self.uselist 

3413 _empty_result = () if uselist else None 

3414 

3415 while our_states: 

3416 chunk = our_states[0 : self._chunksize] 

3417 our_states = our_states[self._chunksize :] 

3418 

3419 primary_keys = [ 

3420 key[0] if query_info.zero_idx else key 

3421 for key, state, state_dict, overwrite in chunk 

3422 ] 

3423 

3424 data = collections.defaultdict(list) 

3425 for k, v in itertools.groupby( 

3426 context.session.execute( 

3427 q, 

3428 params={"primary_keys": primary_keys}, 

3429 execution_options=execution_options, 

3430 ).unique(), 

3431 lambda x: x[0], 

3432 ): 

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

3434 

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

3436 if not overwrite and self.key in state_dict: 

3437 continue 

3438 

3439 collection = data.get(key, _empty_result) 

3440 

3441 if not uselist and collection: 

3442 if len(collection) > 1: 

3443 util.warn( 

3444 "Multiple rows returned with " 

3445 "uselist=False for eagerly-loaded " 

3446 "attribute '%s' " % self 

3447 ) 

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

3449 state, state_dict, collection[0] 

3450 ) 

3451 else: 

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

3453 # value to None 

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

3455 state, state_dict, collection 

3456 ) 

3457 

3458 

3459def single_parent_validator(desc, prop): 

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

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

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

3463 if hasparent and oldvalue is not value: 

3464 raise sa_exc.InvalidRequestError( 

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

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

3467 "single parent." 

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

3469 code="bbf1", 

3470 ) 

3471 return value 

3472 

3473 def append(state, value, initiator): 

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

3475 

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

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

3478 

3479 event.listen( 

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

3481 ) 

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