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

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

814 statements  

1# orm/strategy_options.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: allow-untyped-defs, allow-untyped-calls 

8 

9""" """ 

10 

11from __future__ import annotations 

12 

13import typing 

14from typing import Any 

15from typing import Callable 

16from typing import cast 

17from typing import Dict 

18from typing import Iterable 

19from typing import Literal 

20from typing import Optional 

21from typing import overload 

22from typing import Sequence 

23from typing import Tuple 

24from typing import Type 

25from typing import TypeVar 

26from typing import Union 

27 

28from . import util as orm_util 

29from ._typing import insp_is_attribute 

30from ._typing import insp_is_mapper 

31from ._typing import insp_is_mapper_property 

32from .attributes import QueryableAttribute 

33from .base import entity_str 

34from .base import InspectionAttr 

35from .interfaces import LoaderOption 

36from .path_registry import _AbstractEntityRegistry 

37from .path_registry import _ACCEPTED_TOKENS 

38from .path_registry import _COLUMN_TOKEN 

39from .path_registry import _DEFAULT_TOKEN 

40from .path_registry import _RELATIONSHIP_TOKEN 

41from .path_registry import _StrPathToken 

42from .path_registry import _TokenRegistry 

43from .path_registry import _WILDCARD_TOKEN 

44from .path_registry import path_is_property 

45from .path_registry import PathRegistry 

46from .util import _orm_full_deannotate 

47from .util import AliasedInsp 

48from .. import exc as sa_exc 

49from .. import inspect 

50from .. import util 

51from ..sql import and_ 

52from ..sql import cache_key 

53from ..sql import coercions 

54from ..sql import roles 

55from ..sql import traversals 

56from ..sql import visitors 

57from ..sql.base import _generative 

58from ..util.typing import Self 

59 

60_FN = TypeVar("_FN", bound="Callable[..., Any]") 

61 

62if typing.TYPE_CHECKING: 

63 from ._typing import _EntityType 

64 from ._typing import _InternalEntityType 

65 from .context import _MapperEntity 

66 from .context import _ORMCompileState 

67 from .context import QueryContext 

68 from .interfaces import _StrategyKey 

69 from .interfaces import MapperProperty 

70 from .interfaces import ORMOption 

71 from .mapper import Mapper 

72 from .path_registry import _PathRepresentation 

73 from ..sql._typing import _ColumnExpressionArgument 

74 from ..sql._typing import _FromClauseArgument 

75 from ..sql.cache_key import _CacheKeyTraversalType 

76 from ..sql.cache_key import CacheKey 

77 

78 

79_AttrType = Union[Literal["*"], "QueryableAttribute[Any]"] 

80 

81_WildcardKeyType = Literal["relationship", "column"] 

82_StrategySpec = Dict[str, Any] 

83_OptsType = Dict[str, Any] 

84_AttrGroupType = Tuple[_AttrType, ...] 

85 

86# maps _StrategyKey tuples to the user-facing loader function name, 

87# populated by the @_strategy_labels() decorator below 

88_STRATEGY_FN_LABELS: Dict[Any, str] = {} 

89 

90 

91class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption): 

92 __slots__ = ("propagate_to_loaders",) 

93 

94 _is_strategy_option = True 

95 propagate_to_loaders: bool 

96 

97 def contains_eager( 

98 self, 

99 attr: _AttrType, 

100 alias: Optional[_FromClauseArgument] = None, 

101 _is_chain: bool = False, 

102 _propagate_to_loaders: bool = False, 

103 ) -> Self: 

104 r"""Indicate that the given attribute should be eagerly loaded from 

105 columns stated manually in the query. 

106 

107 This function is part of the :class:`_orm.Load` interface and supports 

108 both method-chained and standalone operation. 

109 

110 The option is used in conjunction with an explicit join that loads 

111 the desired rows, i.e.:: 

112 

113 sess.query(Order).join(Order.user).options(contains_eager(Order.user)) 

114 

115 The above query would join from the ``Order`` entity to its related 

116 ``User`` entity, and the returned ``Order`` objects would have the 

117 ``Order.user`` attribute pre-populated. 

118 

119 It may also be used for customizing the entries in an eagerly loaded 

120 collection; queries will normally want to use the 

121 :ref:`orm_queryguide_populate_existing` execution option assuming the 

122 primary collection of parent objects may already have been loaded:: 

123 

124 sess.query(User).join(User.addresses).filter( 

125 Address.email_address.like("%@aol.com") 

126 ).options(contains_eager(User.addresses)).populate_existing() 

127 

128 See the section :ref:`contains_eager` for complete usage details. 

129 

130 .. seealso:: 

131 

132 :ref:`loading_toplevel` 

133 

134 :ref:`contains_eager` 

135 

136 """ 

137 if alias is not None: 

138 if not isinstance(alias, str): 

139 coerced_alias = coercions.expect(roles.FromClauseRole, alias) 

140 else: 

141 util.warn_deprecated( 

142 "Passing a string name for the 'alias' argument to " 

143 "'contains_eager()` is deprecated, and will not work in a " 

144 "future release. Please use a sqlalchemy.alias() or " 

145 "sqlalchemy.orm.aliased() construct.", 

146 version="1.4", 

147 ) 

148 coerced_alias = alias 

149 

150 elif getattr(attr, "_of_type", None): 

151 assert isinstance(attr, QueryableAttribute) 

152 ot: Optional[_InternalEntityType[Any]] = inspect(attr._of_type) 

153 assert ot is not None 

154 coerced_alias = ot.selectable 

155 else: 

156 coerced_alias = None 

157 

158 cloned = self._set_relationship_strategy( 

159 attr, 

160 {"lazy": "joined"}, 

161 propagate_to_loaders=_propagate_to_loaders, 

162 opts={"eager_from_alias": coerced_alias}, 

163 _reconcile_to_other=True if _is_chain else None, 

164 ) 

165 return cloned 

166 

167 def load_only(self, *attrs: _AttrType, raiseload: bool = False) -> Self: 

168 r"""Indicate that for a particular entity, only the given list 

169 of column-based attribute names should be loaded; all others will be 

170 deferred. 

171 

172 This function is part of the :class:`_orm.Load` interface and supports 

173 both method-chained and standalone operation. 

174 

175 Example - given a class ``User``, load only the ``name`` and 

176 ``fullname`` attributes:: 

177 

178 session.query(User).options(load_only(User.name, User.fullname)) 

179 

180 Example - given a relationship ``User.addresses -> Address``, specify 

181 subquery loading for the ``User.addresses`` collection, but on each 

182 ``Address`` object load only the ``email_address`` attribute:: 

183 

184 session.query(User).options( 

185 subqueryload(User.addresses).load_only(Address.email_address) 

186 ) 

187 

188 For a statement that has multiple entities, 

189 the lead entity can be 

190 specifically referred to using the :class:`_orm.Load` constructor:: 

191 

192 stmt = ( 

193 select(User, Address) 

194 .join(User.addresses) 

195 .options( 

196 Load(User).load_only(User.name, User.fullname), 

197 Load(Address).load_only(Address.email_address), 

198 ) 

199 ) 

200 

201 When used together with the 

202 :ref:`populate_existing <orm_queryguide_populate_existing>` 

203 execution option only the attributes listed will be refreshed. 

204 

205 :param \*attrs: Attributes to be loaded, all others will be deferred. 

206 

207 :param raiseload: raise :class:`.InvalidRequestError` rather than 

208 lazy loading a value when a deferred attribute is accessed. Used 

209 to prevent unwanted SQL from being emitted. 

210 

211 .. versionadded:: 2.0 

212 

213 .. seealso:: 

214 

215 :ref:`orm_queryguide_column_deferral` - in the 

216 :ref:`queryguide_toplevel` 

217 

218 :param \*attrs: Attributes to be loaded, all others will be deferred. 

219 

220 :param raiseload: raise :class:`.InvalidRequestError` rather than 

221 lazy loading a value when a deferred attribute is accessed. Used 

222 to prevent unwanted SQL from being emitted. 

223 

224 .. versionadded:: 2.0 

225 

226 """ 

227 cloned = self._set_column_strategy( 

228 _expand_column_strategy_attrs(attrs), 

229 {"deferred": False, "instrument": True}, 

230 ) 

231 

232 wildcard_strategy = {"deferred": True, "instrument": True} 

233 if raiseload: 

234 wildcard_strategy["raiseload"] = True 

235 

236 cloned = cloned._set_column_strategy( 

237 ("*",), 

238 wildcard_strategy, 

239 ) 

240 return cloned 

241 

242 def joinedload( 

243 self, 

244 attr: _AttrType, 

245 innerjoin: Optional[bool] = None, 

246 ) -> Self: 

247 """Indicate that the given attribute should be loaded using joined 

248 eager loading. 

249 

250 This function is part of the :class:`_orm.Load` interface and supports 

251 both method-chained and standalone operation. 

252 

253 examples:: 

254 

255 # joined-load the "orders" collection on "User" 

256 select(User).options(joinedload(User.orders)) 

257 

258 # joined-load Order.items and then Item.keywords 

259 select(Order).options(joinedload(Order.items).joinedload(Item.keywords)) 

260 

261 # lazily load Order.items, but when Items are loaded, 

262 # joined-load the keywords collection 

263 select(Order).options(lazyload(Order.items).joinedload(Item.keywords)) 

264 

265 :param innerjoin: if ``True``, indicates that the joined eager load 

266 should use an inner join instead of the default of left outer join:: 

267 

268 select(Order).options(joinedload(Order.user, innerjoin=True)) 

269 

270 In order to chain multiple eager joins together where some may be 

271 OUTER and others INNER, right-nested joins are used to link them:: 

272 

273 select(A).options( 

274 joinedload(A.bs, innerjoin=False).joinedload(B.cs, innerjoin=True) 

275 ) 

276 

277 The above query, linking A.bs via "outer" join and B.cs via "inner" 

278 join would render the joins as "a LEFT OUTER JOIN (b JOIN c)". When 

279 using older versions of SQLite (< 3.7.16), this form of JOIN is 

280 translated to use full subqueries as this syntax is otherwise not 

281 directly supported. 

282 

283 The ``innerjoin`` flag can also be stated with the term ``"unnested"``. 

284 This indicates that an INNER JOIN should be used, *unless* the join 

285 is linked to a LEFT OUTER JOIN to the left, in which case it 

286 will render as LEFT OUTER JOIN. For example, supposing ``A.bs`` 

287 is an outerjoin:: 

288 

289 select(A).options(joinedload(A.bs).joinedload(B.cs, innerjoin="unnested")) 

290 

291 The above join will render as "a LEFT OUTER JOIN b LEFT OUTER JOIN c", 

292 rather than as "a LEFT OUTER JOIN (b JOIN c)". 

293 

294 .. note:: The "unnested" flag does **not** affect the JOIN rendered 

295 from a many-to-many association table, e.g. a table configured as 

296 :paramref:`_orm.relationship.secondary`, to the target table; for 

297 correctness of results, these joins are always INNER and are 

298 therefore right-nested if linked to an OUTER join. 

299 

300 .. note:: 

301 

302 The joins produced by :func:`_orm.joinedload` are **anonymously 

303 aliased**. The criteria by which the join proceeds cannot be 

304 modified, nor can the ORM-enabled :class:`_sql.Select` or legacy 

305 :class:`_query.Query` refer to these joins in any way, including 

306 ordering. See :ref:`zen_of_eager_loading` for further detail. 

307 

308 To produce a specific SQL JOIN which is explicitly available, use 

309 :meth:`_sql.Select.join` and :meth:`_query.Query.join`. To combine 

310 explicit JOINs with eager loading of collections, use 

311 :func:`_orm.contains_eager`; see :ref:`contains_eager`. 

312 

313 .. seealso:: 

314 

315 :ref:`loading_toplevel` 

316 

317 :ref:`joined_eager_loading` 

318 

319 """ # noqa: E501 

320 loader = self._set_relationship_strategy( 

321 attr, 

322 {"lazy": "joined"}, 

323 opts=( 

324 {"innerjoin": innerjoin} 

325 if innerjoin is not None 

326 else util.EMPTY_DICT 

327 ), 

328 ) 

329 return loader 

330 

331 def subqueryload(self, attr: _AttrType) -> Self: 

332 """Indicate that the given attribute should be loaded using 

333 subquery eager loading. 

334 

335 This function is part of the :class:`_orm.Load` interface and supports 

336 both method-chained and standalone operation. 

337 

338 examples:: 

339 

340 # subquery-load the "orders" collection on "User" 

341 select(User).options(subqueryload(User.orders)) 

342 

343 # subquery-load Order.items and then Item.keywords 

344 select(Order).options( 

345 subqueryload(Order.items).subqueryload(Item.keywords) 

346 ) 

347 

348 # lazily load Order.items, but when Items are loaded, 

349 # subquery-load the keywords collection 

350 select(Order).options(lazyload(Order.items).subqueryload(Item.keywords)) 

351 

352 .. seealso:: 

353 

354 :ref:`loading_toplevel` 

355 

356 :ref:`subquery_eager_loading` 

357 

358 """ 

359 return self._set_relationship_strategy(attr, {"lazy": "subquery"}) 

360 

361 def selectinload( 

362 self, 

363 attr: _AttrType, 

364 recursion_depth: Optional[int] = None, 

365 chunksize: Optional[int] = None, 

366 ) -> Self: 

367 """Indicate that the given attribute should be loaded using 

368 SELECT IN eager loading. 

369 

370 This function is part of the :class:`_orm.Load` interface and supports 

371 both method-chained and standalone operation. 

372 

373 examples:: 

374 

375 # selectin-load the "orders" collection on "User" 

376 select(User).options(selectinload(User.orders)) 

377 

378 # selectin-load Order.items and then Item.keywords 

379 select(Order).options( 

380 selectinload(Order.items).selectinload(Item.keywords) 

381 ) 

382 

383 # lazily load Order.items, but when Items are loaded, 

384 # selectin-load the keywords collection 

385 select(Order).options(lazyload(Order.items).selectinload(Item.keywords)) 

386 

387 :param recursion_depth: optional int; when set to a positive integer 

388 in conjunction with a self-referential relationship, 

389 indicates "selectin" loading will continue that many levels deep 

390 automatically until no items are found. 

391 

392 .. note:: The :paramref:`_orm.selectinload.recursion_depth` option 

393 currently supports only self-referential relationships. There 

394 is not yet an option to automatically traverse recursive structures 

395 with more than one relationship involved. 

396 

397 Additionally, the :paramref:`_orm.selectinload.recursion_depth` 

398 parameter is new and experimental and should be treated as "alpha" 

399 status for the 2.0 series. 

400 

401 .. versionadded:: 2.0 added 

402 :paramref:`_orm.selectinload.recursion_depth` 

403 

404 :param chunksize: optional int; when set to a positive non-zero 

405 integer, the keys from the IN statement will be chunked relative 

406 to the passed parameter 

407 

408 .. versionadded:: 2.1.0b3 

409 

410 .. seealso:: 

411 

412 :ref:`loading_toplevel` 

413 

414 :ref:`selectin_eager_loading` 

415 

416 """ 

417 return self._set_relationship_strategy( 

418 attr, 

419 {"lazy": "selectin"}, 

420 opts={"recursion_depth": recursion_depth, "chunksize": chunksize}, 

421 ) 

422 

423 def lazyload(self, attr: _AttrType) -> Self: 

424 """Indicate that the given attribute should be loaded using "lazy" 

425 loading. 

426 

427 This function is part of the :class:`_orm.Load` interface and supports 

428 both method-chained and standalone operation. 

429 

430 .. seealso:: 

431 

432 :ref:`loading_toplevel` 

433 

434 :ref:`lazy_loading` 

435 

436 """ 

437 return self._set_relationship_strategy(attr, {"lazy": "select"}) 

438 

439 def immediateload( 

440 self, 

441 attr: _AttrType, 

442 recursion_depth: Optional[int] = None, 

443 ) -> Self: 

444 """Indicate that the given attribute should be loaded using 

445 an immediate load with a per-attribute SELECT statement. 

446 

447 The load is achieved using the "lazyloader" strategy and does not 

448 fire off any additional eager loaders. 

449 

450 The :func:`.immediateload` option is superseded in general 

451 by the :func:`.selectinload` option, which performs the same task 

452 more efficiently by emitting a SELECT for all loaded objects. 

453 

454 This function is part of the :class:`_orm.Load` interface and supports 

455 both method-chained and standalone operation. 

456 

457 :param recursion_depth: optional int; when set to a positive integer 

458 in conjunction with a self-referential relationship, 

459 indicates "selectin" loading will continue that many levels deep 

460 automatically until no items are found. 

461 

462 .. note:: The :paramref:`_orm.immediateload.recursion_depth` option 

463 currently supports only self-referential relationships. There 

464 is not yet an option to automatically traverse recursive structures 

465 with more than one relationship involved. 

466 

467 .. warning:: This parameter is new and experimental and should be 

468 treated as "alpha" status 

469 

470 .. versionadded:: 2.0 added 

471 :paramref:`_orm.immediateload.recursion_depth` 

472 

473 

474 .. seealso:: 

475 

476 :ref:`loading_toplevel` 

477 

478 :ref:`selectin_eager_loading` 

479 

480 """ 

481 loader = self._set_relationship_strategy( 

482 attr, 

483 {"lazy": "immediate"}, 

484 opts={"recursion_depth": recursion_depth}, 

485 ) 

486 return loader 

487 

488 @util.deprecated( 

489 "2.1", 

490 "The :func:`_orm.noload` option is deprecated and will be removed " 

491 "in a future release. This option " 

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

493 "items.", 

494 ) 

495 def noload(self, attr: _AttrType) -> Self: 

496 """Indicate that the given relationship attribute should remain 

497 unloaded. 

498 

499 The relationship attribute will return ``None`` when accessed without 

500 producing any loading effect. 

501 

502 :func:`_orm.noload` applies to :func:`_orm.relationship` attributes 

503 only. 

504 

505 .. seealso:: 

506 

507 :ref:`loading_toplevel` 

508 

509 """ 

510 

511 return self._set_relationship_strategy(attr, {"lazy": "noload"}) 

512 

513 def raiseload(self, attr: _AttrType, sql_only: bool = False) -> Self: 

514 """Indicate that the given attribute should raise an error if accessed. 

515 

516 A relationship attribute configured with :func:`_orm.raiseload` will 

517 raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The 

518 typical way this is useful is when an application is attempting to 

519 ensure that all relationship attributes that are accessed in a 

520 particular context would have been already loaded via eager loading. 

521 Instead of having to read through SQL logs to ensure lazy loads aren't 

522 occurring, this strategy will cause them to raise immediately. 

523 

524 :func:`_orm.raiseload` applies to :func:`_orm.relationship` attributes 

525 only. In order to apply raise-on-SQL behavior to a column-based 

526 attribute, use the :paramref:`.orm.defer.raiseload` parameter on the 

527 :func:`.defer` loader option. 

528 

529 :param sql_only: if True, raise only if the lazy load would emit SQL, 

530 but not if it is only checking the identity map, or determining that 

531 the related value should just be None due to missing keys. When False, 

532 the strategy will raise for all varieties of relationship loading. 

533 

534 This function is part of the :class:`_orm.Load` interface and supports 

535 both method-chained and standalone operation. 

536 

537 .. seealso:: 

538 

539 :ref:`loading_toplevel` 

540 

541 :ref:`prevent_lazy_with_raiseload` 

542 

543 :ref:`orm_queryguide_deferred_raiseload` 

544 

545 """ 

546 

547 return self._set_relationship_strategy( 

548 attr, {"lazy": "raise_on_sql" if sql_only else "raise"} 

549 ) 

550 

551 def defaultload(self, attr: _AttrType) -> Self: 

552 """Indicate an attribute should load using its predefined loader style. 

553 

554 The behavior of this loading option is to not change the current 

555 loading style of the attribute, meaning that the previously configured 

556 one is used or, if no previous style was selected, the default 

557 loading will be used. 

558 

559 This method is used to link to other loader options further into 

560 a chain of attributes without altering the loader style of the links 

561 along the chain. For example, to set joined eager loading for an 

562 element of an element:: 

563 

564 session.query(MyClass).options( 

565 defaultload(MyClass.someattribute).joinedload( 

566 MyOtherClass.someotherattribute 

567 ) 

568 ) 

569 

570 :func:`.defaultload` is also useful for setting column-level options on 

571 a related class, namely that of :func:`.defer` and :func:`.undefer`:: 

572 

573 session.scalars( 

574 select(MyClass).options( 

575 defaultload(MyClass.someattribute) 

576 .defer("some_column") 

577 .undefer("some_other_column") 

578 ) 

579 ) 

580 

581 .. seealso:: 

582 

583 :ref:`orm_queryguide_relationship_sub_options` 

584 

585 :meth:`_orm.Load.options` 

586 

587 """ 

588 return self._set_relationship_strategy(attr, None) 

589 

590 def defer(self, key: _AttrType, raiseload: bool = False) -> Self: 

591 r"""Indicate that the given column-oriented attribute should be 

592 deferred, e.g. not loaded until accessed. 

593 

594 This function is part of the :class:`_orm.Load` interface and supports 

595 both method-chained and standalone operation. 

596 

597 e.g.:: 

598 

599 from sqlalchemy.orm import defer 

600 

601 session.query(MyClass).options( 

602 defer(MyClass.attribute_one), defer(MyClass.attribute_two) 

603 ) 

604 

605 To specify a deferred load of an attribute on a related class, 

606 the path can be specified one token at a time, specifying the loading 

607 style for each link along the chain. To leave the loading style 

608 for a link unchanged, use :func:`_orm.defaultload`:: 

609 

610 session.query(MyClass).options( 

611 defaultload(MyClass.someattr).defer(RelatedClass.some_column) 

612 ) 

613 

614 Multiple deferral options related to a relationship can be bundled 

615 at once using :meth:`_orm.Load.options`:: 

616 

617 

618 select(MyClass).options( 

619 defaultload(MyClass.someattr).options( 

620 defer(RelatedClass.some_column), 

621 defer(RelatedClass.some_other_column), 

622 defer(RelatedClass.another_column), 

623 ) 

624 ) 

625 

626 :param key: Attribute to be deferred. 

627 

628 :param raiseload: raise :class:`.InvalidRequestError` rather than 

629 lazy loading a value when the deferred attribute is accessed. Used 

630 to prevent unwanted SQL from being emitted. 

631 

632 .. versionadded:: 1.4 

633 

634 .. seealso:: 

635 

636 :ref:`orm_queryguide_column_deferral` - in the 

637 :ref:`queryguide_toplevel` 

638 

639 :func:`_orm.load_only` 

640 

641 :func:`_orm.undefer` 

642 

643 """ 

644 strategy = {"deferred": True, "instrument": True} 

645 if raiseload: 

646 strategy["raiseload"] = True 

647 return self._set_column_strategy( 

648 _expand_column_strategy_attrs((key,)), strategy 

649 ) 

650 

651 def undefer(self, key: _AttrType) -> Self: 

652 r"""Indicate that the given column-oriented attribute should be 

653 undeferred, e.g. specified within the SELECT statement of the entity 

654 as a whole. 

655 

656 The column being undeferred is typically set up on the mapping as a 

657 :func:`.deferred` attribute. 

658 

659 This function is part of the :class:`_orm.Load` interface and supports 

660 both method-chained and standalone operation. 

661 

662 Examples:: 

663 

664 # undefer two columns 

665 session.query(MyClass).options( 

666 undefer(MyClass.col1), undefer(MyClass.col2) 

667 ) 

668 

669 # undefer all columns specific to a single class using Load + * 

670 session.query(MyClass, MyOtherClass).options(Load(MyClass).undefer("*")) 

671 

672 # undefer a column on a related object 

673 select(MyClass).options(defaultload(MyClass.items).undefer(MyClass.text)) 

674 

675 :param key: Attribute to be undeferred. 

676 

677 .. seealso:: 

678 

679 :ref:`orm_queryguide_column_deferral` - in the 

680 :ref:`queryguide_toplevel` 

681 

682 :func:`_orm.defer` 

683 

684 :func:`_orm.undefer_group` 

685 

686 """ # noqa: E501 

687 return self._set_column_strategy( 

688 _expand_column_strategy_attrs((key,)), 

689 {"deferred": False, "instrument": True}, 

690 ) 

691 

692 def undefer_group(self, name: str) -> Self: 

693 """Indicate that columns within the given deferred group name should be 

694 undeferred. 

695 

696 The columns being undeferred are set up on the mapping as 

697 :func:`.deferred` attributes and include a "group" name. 

698 

699 E.g:: 

700 

701 session.query(MyClass).options(undefer_group("large_attrs")) 

702 

703 To undefer a group of attributes on a related entity, the path can be 

704 spelled out using relationship loader options, such as 

705 :func:`_orm.defaultload`:: 

706 

707 select(MyClass).options( 

708 defaultload("someattr").undefer_group("large_attrs") 

709 ) 

710 

711 .. seealso:: 

712 

713 :ref:`orm_queryguide_column_deferral` - in the 

714 :ref:`queryguide_toplevel` 

715 

716 :func:`_orm.defer` 

717 

718 :func:`_orm.undefer` 

719 

720 """ 

721 return self._set_column_strategy( 

722 (_WILDCARD_TOKEN,), None, {f"undefer_group_{name}": True} 

723 ) 

724 

725 def with_expression( 

726 self, 

727 key: _AttrType, 

728 expression: _ColumnExpressionArgument[Any], 

729 ) -> Self: 

730 r"""Apply an ad-hoc SQL expression to a "deferred expression" 

731 attribute. 

732 

733 This option is used in conjunction with the 

734 :func:`_orm.query_expression` mapper-level construct that indicates an 

735 attribute which should be the target of an ad-hoc SQL expression. 

736 

737 E.g.:: 

738 

739 stmt = select(SomeClass).options( 

740 with_expression(SomeClass.x_y_expr, SomeClass.x + SomeClass.y) 

741 ) 

742 

743 :param key: Attribute to be populated 

744 

745 :param expr: SQL expression to be applied to the attribute. 

746 

747 .. seealso:: 

748 

749 :ref:`orm_queryguide_with_expression` - background and usage 

750 examples 

751 

752 """ 

753 

754 expression = _orm_full_deannotate( 

755 coercions.expect(roles.LabeledColumnExprRole, expression) 

756 ) 

757 

758 return self._set_column_strategy( 

759 (key,), {"query_expression": True}, extra_criteria=(expression,) 

760 ) 

761 

762 def selectin_polymorphic(self, classes: Iterable[Type[Any]]) -> Self: 

763 """Indicate an eager load should take place for all attributes 

764 specific to a subclass. 

765 

766 This uses an additional SELECT with IN against all matched primary 

767 key values, and is the per-query analogue to the ``"selectin"`` 

768 setting on the :paramref:`.mapper.polymorphic_load` parameter. 

769 

770 .. seealso:: 

771 

772 :ref:`polymorphic_selectin` 

773 

774 """ 

775 self = self._set_class_strategy( 

776 {"selectinload_polymorphic": True}, 

777 opts={ 

778 "entities": tuple( 

779 sorted((inspect(cls) for cls in classes), key=id) 

780 ) 

781 }, 

782 ) 

783 return self 

784 

785 @overload 

786 def _coerce_strat(self, strategy: _StrategySpec) -> _StrategyKey: ... 

787 

788 @overload 

789 def _coerce_strat(self, strategy: Literal[None]) -> None: ... 

790 

791 def _coerce_strat( 

792 self, strategy: Optional[_StrategySpec] 

793 ) -> Optional[_StrategyKey]: 

794 if strategy is not None: 

795 strategy_key = tuple(sorted(strategy.items())) 

796 else: 

797 strategy_key = None 

798 return strategy_key 

799 

800 @_generative 

801 def _set_relationship_strategy( 

802 self, 

803 attr: _AttrType, 

804 strategy: Optional[_StrategySpec], 

805 propagate_to_loaders: bool = True, 

806 opts: Optional[_OptsType] = None, 

807 _reconcile_to_other: Optional[bool] = None, 

808 ) -> Self: 

809 strategy_key = self._coerce_strat(strategy) 

810 

811 self._clone_for_bind_strategy( 

812 (attr,), 

813 strategy_key, 

814 _RELATIONSHIP_TOKEN, 

815 opts=opts, 

816 propagate_to_loaders=propagate_to_loaders, 

817 reconcile_to_other=_reconcile_to_other, 

818 ) 

819 return self 

820 

821 @_generative 

822 def _set_column_strategy( 

823 self, 

824 attrs: Tuple[_AttrType, ...], 

825 strategy: Optional[_StrategySpec], 

826 opts: Optional[_OptsType] = None, 

827 extra_criteria: Optional[Tuple[Any, ...]] = None, 

828 ) -> Self: 

829 strategy_key = self._coerce_strat(strategy) 

830 

831 self._clone_for_bind_strategy( 

832 attrs, 

833 strategy_key, 

834 _COLUMN_TOKEN, 

835 opts=opts, 

836 attr_group=attrs, 

837 extra_criteria=extra_criteria, 

838 ) 

839 return self 

840 

841 @_generative 

842 def _set_generic_strategy( 

843 self, 

844 attrs: Tuple[_AttrType, ...], 

845 strategy: _StrategySpec, 

846 _reconcile_to_other: Optional[bool] = None, 

847 ) -> Self: 

848 strategy_key = self._coerce_strat(strategy) 

849 self._clone_for_bind_strategy( 

850 attrs, 

851 strategy_key, 

852 None, 

853 propagate_to_loaders=True, 

854 reconcile_to_other=_reconcile_to_other, 

855 ) 

856 return self 

857 

858 @_generative 

859 def _set_class_strategy( 

860 self, strategy: _StrategySpec, opts: _OptsType 

861 ) -> Self: 

862 strategy_key = self._coerce_strat(strategy) 

863 

864 self._clone_for_bind_strategy(None, strategy_key, None, opts=opts) 

865 return self 

866 

867 def _apply_to_parent(self, parent: Load) -> None: 

868 """apply this :class:`_orm._AbstractLoad` object as a sub-option o 

869 a :class:`_orm.Load` object. 

870 

871 Implementation is provided by subclasses. 

872 

873 """ 

874 raise NotImplementedError() 

875 

876 def options(self, *opts: _AbstractLoad) -> Self: 

877 r"""Apply a series of options as sub-options to this 

878 :class:`_orm._AbstractLoad` object. 

879 

880 Implementation is provided by subclasses. 

881 

882 """ 

883 raise NotImplementedError() 

884 

885 def _clone_for_bind_strategy( 

886 self, 

887 attrs: Optional[Tuple[_AttrType, ...]], 

888 strategy: Optional[_StrategyKey], 

889 wildcard_key: Optional[_WildcardKeyType], 

890 opts: Optional[_OptsType] = None, 

891 attr_group: Optional[_AttrGroupType] = None, 

892 propagate_to_loaders: bool = True, 

893 reconcile_to_other: Optional[bool] = None, 

894 extra_criteria: Optional[Tuple[Any, ...]] = None, 

895 ) -> Self: 

896 raise NotImplementedError() 

897 

898 def process_compile_state_replaced_entities( 

899 self, 

900 compile_state: _ORMCompileState, 

901 mapper_entities: Sequence[_MapperEntity], 

902 ) -> None: 

903 if not compile_state.compile_options._enable_eagerloads: 

904 return 

905 

906 # process is being run here so that the options given are validated 

907 # against what the lead entities were, as well as to accommodate 

908 # for the entities having been replaced with equivalents 

909 self._process( 

910 compile_state, 

911 mapper_entities, 

912 not bool(compile_state.current_path), 

913 ) 

914 

915 def process_compile_state(self, compile_state: _ORMCompileState) -> None: 

916 if not compile_state.compile_options._enable_eagerloads: 

917 return 

918 

919 self._process( 

920 compile_state, 

921 compile_state._lead_mapper_entities, 

922 not bool(compile_state.current_path) 

923 and not compile_state.compile_options._for_refresh_state, 

924 ) 

925 

926 def _process( 

927 self, 

928 compile_state: _ORMCompileState, 

929 mapper_entities: Sequence[_MapperEntity], 

930 raiseerr: bool, 

931 ) -> None: 

932 """implemented by subclasses""" 

933 raise NotImplementedError() 

934 

935 @classmethod 

936 def _chop_path( 

937 cls, 

938 to_chop: _PathRepresentation, 

939 path: PathRegistry, 

940 debug: bool = False, 

941 ) -> Optional[_PathRepresentation]: 

942 i = -1 

943 

944 for i, (c_token, p_token) in enumerate( 

945 zip(to_chop, path.natural_path) 

946 ): 

947 if isinstance(c_token, str): 

948 if i == 0 and ( 

949 c_token.endswith(f":{_DEFAULT_TOKEN}") 

950 or c_token.endswith(f":{_WILDCARD_TOKEN}") 

951 ): 

952 return to_chop 

953 elif ( 

954 c_token != f"{_RELATIONSHIP_TOKEN}:{_WILDCARD_TOKEN}" 

955 and c_token != p_token.key # type: ignore[union-attr] 

956 ): 

957 return None 

958 

959 if c_token is p_token: 

960 continue 

961 elif ( 

962 isinstance(c_token, InspectionAttr) 

963 and insp_is_mapper(c_token) 

964 and insp_is_mapper(p_token) 

965 and c_token.isa(p_token) 

966 ): 

967 continue 

968 

969 else: 

970 return None 

971 return to_chop[i + 1 :] 

972 

973 

974class Load(_AbstractLoad): 

975 """Represents loader options which modify the state of a 

976 ORM-enabled :class:`_sql.Select` or a legacy :class:`_query.Query` in 

977 order to affect how various mapped attributes are loaded. 

978 

979 The :class:`_orm.Load` object is in most cases used implicitly behind the 

980 scenes when one makes use of a query option like :func:`_orm.joinedload`, 

981 :func:`_orm.defer`, or similar. It typically is not instantiated directly 

982 except for in some very specific cases. 

983 

984 .. seealso:: 

985 

986 :ref:`orm_queryguide_relationship_per_entity_wildcard` - illustrates an 

987 example where direct use of :class:`_orm.Load` may be useful 

988 

989 """ 

990 

991 __slots__ = ( 

992 "path", 

993 "context", 

994 "additional_source_entities", 

995 ) 

996 

997 _traverse_internals = [ 

998 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key), 

999 ( 

1000 "context", 

1001 visitors.InternalTraversal.dp_has_cache_key_list, 

1002 ), 

1003 ("propagate_to_loaders", visitors.InternalTraversal.dp_boolean), 

1004 ( 

1005 "additional_source_entities", 

1006 visitors.InternalTraversal.dp_has_cache_key_list, 

1007 ), 

1008 ] 

1009 _cache_key_traversal = None 

1010 

1011 path: PathRegistry 

1012 context: Tuple[_LoadElement, ...] 

1013 additional_source_entities: Tuple[_InternalEntityType[Any], ...] 

1014 

1015 def __init__(self, entity: _EntityType[Any]): 

1016 insp = cast("Union[Mapper[Any], AliasedInsp[Any]]", inspect(entity)) 

1017 insp._post_inspect 

1018 

1019 self.path = insp._path_registry 

1020 self.context = () 

1021 self.propagate_to_loaders = False 

1022 self.additional_source_entities = () 

1023 

1024 def __str__(self) -> str: 

1025 return f"Load({entity_str(self.path[0])})" 

1026 

1027 @classmethod 

1028 def _construct_for_existing_path( 

1029 cls, path: _AbstractEntityRegistry 

1030 ) -> Load: 

1031 load = cls.__new__(cls) 

1032 load.path = path 

1033 load.context = () 

1034 load.propagate_to_loaders = False 

1035 load.additional_source_entities = () 

1036 return load 

1037 

1038 def _adapt_cached_option_to_uncached_option( 

1039 self, context: QueryContext, uncached_opt: ORMOption 

1040 ) -> ORMOption: 

1041 if uncached_opt is self: 

1042 return self 

1043 return self._adjust_for_extra_criteria(context) 

1044 

1045 def _prepend_path(self, path: PathRegistry) -> Load: 

1046 cloned = self._clone() 

1047 cloned.context = tuple( 

1048 element._prepend_path(path) for element in self.context 

1049 ) 

1050 return cloned 

1051 

1052 def _adjust_for_extra_criteria(self, context: QueryContext) -> Load: 

1053 """Apply the current bound parameters in a QueryContext to all 

1054 occurrences "extra_criteria" stored within this ``Load`` object, 

1055 returning a new instance of this ``Load`` object. 

1056 

1057 """ 

1058 

1059 # avoid generating cache keys for the queries if we don't 

1060 # actually have any extra_criteria options, which is the 

1061 # common case 

1062 for value in self.context: 

1063 if value._extra_criteria: 

1064 break 

1065 else: 

1066 return self 

1067 

1068 replacement_cache_key = context.user_passed_query._generate_cache_key() 

1069 

1070 if replacement_cache_key is None: 

1071 return self 

1072 

1073 orig_query = context.compile_state.select_statement 

1074 orig_cache_key = orig_query._generate_cache_key() 

1075 assert orig_cache_key is not None 

1076 

1077 def process( 

1078 opt: _LoadElement, 

1079 replacement_cache_key: CacheKey, 

1080 orig_cache_key: CacheKey, 

1081 ) -> _LoadElement: 

1082 cloned_opt = opt._clone() 

1083 

1084 cloned_opt._extra_criteria = tuple( 

1085 replacement_cache_key._apply_params_to_element( 

1086 orig_cache_key, crit 

1087 ) 

1088 for crit in cloned_opt._extra_criteria 

1089 ) 

1090 

1091 return cloned_opt 

1092 

1093 cloned = self._clone() 

1094 cloned.context = tuple( 

1095 ( 

1096 process(value, replacement_cache_key, orig_cache_key) 

1097 if value._extra_criteria 

1098 else value 

1099 ) 

1100 for value in self.context 

1101 ) 

1102 return cloned 

1103 

1104 def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr): 

1105 """called at process time to allow adjustment of the root 

1106 entity inside of _LoadElement objects. 

1107 

1108 """ 

1109 path = self.path 

1110 

1111 for ent in mapper_entities: 

1112 ezero = ent.entity_zero 

1113 if ezero and orm_util._entity_corresponds_to( 

1114 # technically this can be a token also, but this is 

1115 # safe to pass to _entity_corresponds_to() 

1116 ezero, 

1117 cast("_InternalEntityType[Any]", path[0]), 

1118 ): 

1119 return ezero 

1120 

1121 return None 

1122 

1123 def _process( 

1124 self, 

1125 compile_state: _ORMCompileState, 

1126 mapper_entities: Sequence[_MapperEntity], 

1127 raiseerr: bool, 

1128 ) -> None: 

1129 reconciled_lead_entity = self._reconcile_query_entities_with_us( 

1130 mapper_entities, raiseerr 

1131 ) 

1132 

1133 # if the context has a current path, this is a lazy load 

1134 has_current_path = bool(compile_state.compile_options._current_path) 

1135 

1136 for loader in self.context: 

1137 # issue #11292 

1138 # historically, propagate_to_loaders was only considered at 

1139 # object loading time, whether or not to carry along options 

1140 # onto an object's loaded state where it would be used by lazyload. 

1141 # however, the defaultload() option needs to propagate in case 

1142 # its sub-options propagate_to_loaders, but its sub-options 

1143 # that dont propagate should not be applied for lazy loaders. 

1144 # so we check again 

1145 if has_current_path and not loader.propagate_to_loaders: 

1146 continue 

1147 loader.process_compile_state( 

1148 self, 

1149 compile_state, 

1150 mapper_entities, 

1151 reconciled_lead_entity, 

1152 raiseerr, 

1153 ) 

1154 

1155 def _apply_to_parent(self, parent: Load) -> None: 

1156 """apply this :class:`_orm.Load` object as a sub-option of another 

1157 :class:`_orm.Load` object. 

1158 

1159 This method is used by the :meth:`_orm.Load.options` method. 

1160 

1161 """ 

1162 cloned = self._generate() 

1163 

1164 assert cloned.propagate_to_loaders == self.propagate_to_loaders 

1165 

1166 if not any( 

1167 orm_util._entity_corresponds_to_use_path_impl( 

1168 elem, cloned.path.odd_element(0) 

1169 ) 

1170 for elem in (parent.path.odd_element(-1),) 

1171 + parent.additional_source_entities 

1172 ): 

1173 if len(cloned.path) > 1: 

1174 attrname = cloned.path[1] 

1175 parent_entity = cloned.path[0] 

1176 else: 

1177 attrname = cloned.path[0] 

1178 parent_entity = cloned.path[0] 

1179 _raise_for_does_not_link(parent.path, attrname, parent_entity) 

1180 

1181 cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:]) 

1182 

1183 if self.context: 

1184 cloned.context = tuple( 

1185 value._prepend_path_from(parent) for value in self.context 

1186 ) 

1187 

1188 if cloned.context: 

1189 parent.context += cloned.context 

1190 parent.additional_source_entities += ( 

1191 cloned.additional_source_entities 

1192 ) 

1193 

1194 @_generative 

1195 def options(self, *opts: _AbstractLoad) -> Self: 

1196 r"""Apply a series of options as sub-options to this 

1197 :class:`_orm.Load` 

1198 object. 

1199 

1200 E.g.:: 

1201 

1202 query = session.query(Author) 

1203 query = query.options( 

1204 joinedload(Author.book).options( 

1205 load_only(Book.summary, Book.excerpt), 

1206 joinedload(Book.citations).options(joinedload(Citation.author)), 

1207 ) 

1208 ) 

1209 

1210 :param \*opts: A series of loader option objects (ultimately 

1211 :class:`_orm.Load` objects) which should be applied to the path 

1212 specified by this :class:`_orm.Load` object. 

1213 

1214 .. seealso:: 

1215 

1216 :func:`.defaultload` 

1217 

1218 :ref:`orm_queryguide_relationship_sub_options` 

1219 

1220 """ 

1221 for opt in opts: 

1222 try: 

1223 opt._apply_to_parent(self) 

1224 except AttributeError as ae: 

1225 if not isinstance(opt, _AbstractLoad): 

1226 raise sa_exc.ArgumentError( 

1227 f"Loader option {opt} is not compatible with the " 

1228 "Load.options() method." 

1229 ) from ae 

1230 else: 

1231 raise 

1232 return self 

1233 

1234 def _clone_for_bind_strategy( 

1235 self, 

1236 attrs: Optional[Tuple[_AttrType, ...]], 

1237 strategy: Optional[_StrategyKey], 

1238 wildcard_key: Optional[_WildcardKeyType], 

1239 opts: Optional[_OptsType] = None, 

1240 attr_group: Optional[_AttrGroupType] = None, 

1241 propagate_to_loaders: bool = True, 

1242 reconcile_to_other: Optional[bool] = None, 

1243 extra_criteria: Optional[Tuple[Any, ...]] = None, 

1244 ) -> Self: 

1245 # for individual strategy that needs to propagate, set the whole 

1246 # Load container to also propagate, so that it shows up in 

1247 # InstanceState.load_options 

1248 if propagate_to_loaders: 

1249 self.propagate_to_loaders = True 

1250 

1251 if self.path.is_token: 

1252 raise sa_exc.ArgumentError( 

1253 "Wildcard token cannot be followed by another entity" 

1254 ) 

1255 

1256 elif path_is_property(self.path): 

1257 # reuse the lookup which will raise a nicely formatted 

1258 # LoaderStrategyException 

1259 if strategy: 

1260 self.path.prop._strategy_lookup(self.path.prop, strategy[0]) 

1261 else: 

1262 raise sa_exc.ArgumentError( 

1263 f"Mapped attribute '{self.path.prop}' does not " 

1264 "refer to a mapped entity" 

1265 ) 

1266 

1267 if attrs is None: 

1268 load_element = _ClassStrategyLoad.create( 

1269 self.path, 

1270 None, 

1271 strategy, 

1272 wildcard_key, 

1273 opts, 

1274 propagate_to_loaders, 

1275 attr_group=attr_group, 

1276 reconcile_to_other=reconcile_to_other, 

1277 extra_criteria=extra_criteria, 

1278 ) 

1279 if load_element: 

1280 self.context += (load_element,) 

1281 assert opts is not None 

1282 self.additional_source_entities += cast( 

1283 "Tuple[_InternalEntityType[Any]]", opts["entities"] 

1284 ) 

1285 

1286 else: 

1287 for attr in attrs: 

1288 if isinstance(attr, str): 

1289 load_element = _TokenStrategyLoad.create( 

1290 self.path, 

1291 attr, 

1292 strategy, 

1293 wildcard_key, 

1294 opts, 

1295 propagate_to_loaders, 

1296 attr_group=attr_group, 

1297 reconcile_to_other=reconcile_to_other, 

1298 extra_criteria=extra_criteria, 

1299 ) 

1300 else: 

1301 load_element = _AttributeStrategyLoad.create( 

1302 self.path, 

1303 attr, 

1304 strategy, 

1305 wildcard_key, 

1306 opts, 

1307 propagate_to_loaders, 

1308 attr_group=attr_group, 

1309 reconcile_to_other=reconcile_to_other, 

1310 extra_criteria=extra_criteria, 

1311 ) 

1312 

1313 if load_element: 

1314 # for relationship options, update self.path on this Load 

1315 # object with the latest path. 

1316 if wildcard_key is _RELATIONSHIP_TOKEN: 

1317 self.path = load_element.path 

1318 self.context += (load_element,) 

1319 

1320 # this seems to be effective for selectinloader, 

1321 # giving the extra match to one more level deep. 

1322 # but does not work for immediateloader, which still 

1323 # must add additional options at load time 

1324 if load_element.local_opts.get("recursion_depth", False): 

1325 r1 = load_element._recurse() 

1326 self.context += (r1,) 

1327 

1328 return self 

1329 

1330 def __getstate__(self): 

1331 d = self._shallow_to_dict() 

1332 d["path"] = self.path.serialize() 

1333 return d 

1334 

1335 def __setstate__(self, state): 

1336 state["path"] = PathRegistry.deserialize(state["path"]) 

1337 self._shallow_from_dict(state) 

1338 

1339 

1340class _WildcardLoad(_AbstractLoad): 

1341 """represent a standalone '*' load operation""" 

1342 

1343 __slots__ = ("strategy", "path", "local_opts") 

1344 

1345 _traverse_internals = [ 

1346 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj), 

1347 ("path", visitors.ExtendedInternalTraversal.dp_plain_obj), 

1348 ( 

1349 "local_opts", 

1350 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1351 ), 

1352 ] 

1353 cache_key_traversal: _CacheKeyTraversalType = None 

1354 

1355 strategy: Optional[Tuple[Any, ...]] 

1356 local_opts: _OptsType 

1357 path: Union[Tuple[()], Tuple[str]] 

1358 propagate_to_loaders = False 

1359 

1360 def __init__(self) -> None: 

1361 self.path = () 

1362 self.strategy = None 

1363 self.local_opts = util.EMPTY_DICT 

1364 

1365 def _clone_for_bind_strategy( 

1366 self, 

1367 attrs, 

1368 strategy, 

1369 wildcard_key, 

1370 opts=None, 

1371 attr_group=None, 

1372 propagate_to_loaders=True, 

1373 reconcile_to_other=None, 

1374 extra_criteria=None, 

1375 ): 

1376 assert attrs is not None 

1377 attr = attrs[0] 

1378 assert ( 

1379 wildcard_key 

1380 and isinstance(attr, str) 

1381 and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN) 

1382 ) 

1383 

1384 attr = f"{wildcard_key}:{attr}" 

1385 

1386 self.strategy = strategy 

1387 self.path = (attr,) 

1388 if opts: 

1389 self.local_opts = util.immutabledict(opts) 

1390 

1391 assert extra_criteria is None 

1392 

1393 def options(self, *opts: _AbstractLoad) -> Self: 

1394 raise NotImplementedError("Star option does not support sub-options") 

1395 

1396 def _apply_to_parent(self, parent: Load) -> None: 

1397 """apply this :class:`_orm._WildcardLoad` object as a sub-option of 

1398 a :class:`_orm.Load` object. 

1399 

1400 This method is used by the :meth:`_orm.Load.options` method. Note 

1401 that :class:`_orm.WildcardLoad` itself can't have sub-options, but 

1402 it may be used as the sub-option of a :class:`_orm.Load` object. 

1403 

1404 """ 

1405 assert self.path 

1406 attr = self.path[0] 

1407 if attr.endswith(_DEFAULT_TOKEN): 

1408 attr = f"{attr.split(':')[0]}:{_WILDCARD_TOKEN}" 

1409 

1410 effective_path = cast(_AbstractEntityRegistry, parent.path).token(attr) 

1411 

1412 assert effective_path.is_token 

1413 

1414 loader = _TokenStrategyLoad.create( 

1415 effective_path, 

1416 None, 

1417 self.strategy, 

1418 None, 

1419 self.local_opts, 

1420 self.propagate_to_loaders, 

1421 ) 

1422 

1423 parent.context += (loader,) 

1424 

1425 def _process(self, compile_state, mapper_entities, raiseerr): 

1426 is_refresh = compile_state.compile_options._for_refresh_state 

1427 

1428 if is_refresh and not self.propagate_to_loaders: 

1429 return 

1430 

1431 entities = [ent.entity_zero for ent in mapper_entities] 

1432 current_path = compile_state.current_path 

1433 

1434 start_path: _PathRepresentation = self.path 

1435 

1436 if current_path: 

1437 # TODO: no cases in test suite where we actually get 

1438 # None back here 

1439 new_path = self._chop_path(start_path, current_path) 

1440 if new_path is None: 

1441 return 

1442 

1443 # chop_path does not actually "chop" a wildcard token path, 

1444 # just returns it 

1445 assert new_path == start_path 

1446 

1447 # start_path is a single-token tuple 

1448 assert start_path and len(start_path) == 1 

1449 

1450 token = start_path[0] 

1451 assert isinstance(token, str) 

1452 entity = self._find_entity_basestring(entities, token, raiseerr) 

1453 

1454 if not entity: 

1455 return 

1456 

1457 path_element = entity 

1458 

1459 # transfer our entity-less state into a Load() object 

1460 # with a real entity path. Start with the lead entity 

1461 # we just located, then go through the rest of our path 

1462 # tokens and populate into the Load(). 

1463 

1464 assert isinstance(token, str) 

1465 loader = _TokenStrategyLoad.create( 

1466 path_element._path_registry, 

1467 token, 

1468 self.strategy, 

1469 None, 

1470 self.local_opts, 

1471 self.propagate_to_loaders, 

1472 raiseerr=raiseerr, 

1473 ) 

1474 if not loader: 

1475 return 

1476 

1477 assert loader.path.is_token 

1478 

1479 # don't pass a reconciled lead entity here 

1480 loader.process_compile_state( 

1481 self, compile_state, mapper_entities, None, raiseerr 

1482 ) 

1483 

1484 return loader 

1485 

1486 def _find_entity_basestring( 

1487 self, 

1488 entities: Iterable[_InternalEntityType[Any]], 

1489 token: str, 

1490 raiseerr: bool, 

1491 ) -> Optional[_InternalEntityType[Any]]: 

1492 if token.endswith(f":{_WILDCARD_TOKEN}"): 

1493 if len(list(entities)) != 1: 

1494 if raiseerr: 

1495 raise sa_exc.ArgumentError( 

1496 "Can't apply wildcard ('*') or load_only() " 

1497 f"loader option to multiple entities " 

1498 f"{', '.join(str(ent) for ent in entities)}. Specify " 

1499 "loader options for each entity individually, such as " 

1500 f"""{ 

1501 ", ".join( 

1502 f"Load({ent}).some_option('*')" 

1503 for ent in entities 

1504 ) 

1505 }.""" 

1506 ) 

1507 elif token.endswith(_DEFAULT_TOKEN): 

1508 raiseerr = False 

1509 

1510 for ent in entities: 

1511 # return only the first _MapperEntity when searching 

1512 # based on string prop name. Ideally object 

1513 # attributes are used to specify more exactly. 

1514 return ent 

1515 else: 

1516 if raiseerr: 

1517 raise sa_exc.ArgumentError( 

1518 "Query has only expression-based entities - " 

1519 f'can\'t find property named "{token}".' 

1520 ) 

1521 else: 

1522 return None 

1523 

1524 def __getstate__(self) -> Dict[str, Any]: 

1525 d = self._shallow_to_dict() 

1526 return d 

1527 

1528 def __setstate__(self, state: Dict[str, Any]) -> None: 

1529 self._shallow_from_dict(state) 

1530 

1531 

1532class _LoadElement( 

1533 cache_key.HasCacheKey, traversals.HasShallowCopy, visitors.Traversible 

1534): 

1535 """represents strategy information to select for a LoaderStrategy 

1536 and pass options to it. 

1537 

1538 :class:`._LoadElement` objects provide the inner datastructure 

1539 stored by a :class:`_orm.Load` object and are also the object passed 

1540 to methods like :meth:`.LoaderStrategy.setup_query`. 

1541 

1542 .. versionadded:: 2.0 

1543 

1544 """ 

1545 

1546 __slots__ = ( 

1547 "path", 

1548 "strategy", 

1549 "propagate_to_loaders", 

1550 "local_opts", 

1551 "_extra_criteria", 

1552 "_reconcile_to_other", 

1553 ) 

1554 __visit_name__ = "load_element" 

1555 

1556 _traverse_internals = [ 

1557 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key), 

1558 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj), 

1559 ( 

1560 "local_opts", 

1561 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1562 ), 

1563 ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list), 

1564 ("propagate_to_loaders", visitors.InternalTraversal.dp_plain_obj), 

1565 ("_reconcile_to_other", visitors.InternalTraversal.dp_plain_obj), 

1566 ] 

1567 _cache_key_traversal = None 

1568 

1569 _extra_criteria: Tuple[Any, ...] 

1570 

1571 _reconcile_to_other: Optional[bool] 

1572 strategy: Optional[_StrategyKey] 

1573 path: PathRegistry 

1574 propagate_to_loaders: bool 

1575 

1576 local_opts: util.immutabledict[str, Any] 

1577 

1578 is_token_strategy: bool 

1579 is_class_strategy: bool 

1580 

1581 def __hash__(self) -> int: 

1582 return id(self) 

1583 

1584 def __eq__(self, other): 

1585 return traversals.compare(self, other) 

1586 

1587 @property 

1588 def is_opts_only(self) -> bool: 

1589 return bool(self.local_opts and self.strategy is None) 

1590 

1591 def _clone(self, **kw: Any) -> Self: 

1592 cls = self.__class__ 

1593 s = cls.__new__(cls) 

1594 

1595 self._shallow_copy_to(s) 

1596 return s 

1597 

1598 def _update_opts(self, **kw: Any) -> _LoadElement: 

1599 new = self._clone() 

1600 new.local_opts = new.local_opts.union(kw) 

1601 return new 

1602 

1603 def __getstate__(self) -> Dict[str, Any]: 

1604 d = self._shallow_to_dict() 

1605 d["path"] = self.path.serialize() 

1606 return d 

1607 

1608 def __setstate__(self, state: Dict[str, Any]) -> None: 

1609 state["path"] = PathRegistry.deserialize(state["path"]) 

1610 self._shallow_from_dict(state) 

1611 

1612 def _raise_for_no_match(self, parent_loader, mapper_entities): 

1613 path = parent_loader.path 

1614 

1615 found_entities = False 

1616 for ent in mapper_entities: 

1617 ezero = ent.entity_zero 

1618 if ezero: 

1619 found_entities = True 

1620 break 

1621 

1622 if not found_entities: 

1623 raise sa_exc.ArgumentError( 

1624 "Query has only expression-based entities; " 

1625 f"attribute loader option {self._to_option_method_string()} " 

1626 "can't be applied here." 

1627 ) 

1628 else: 

1629 raise sa_exc.ArgumentError( 

1630 f"Mapped class {entity_str(path[0])} referenced in " 

1631 f"option {self._to_option_method_string()} does not apply " 

1632 f"to any of the root entities in this query, e.g. " 

1633 f"""{ 

1634 ", ".join( 

1635 entity_str(x.entity_zero) 

1636 for x in mapper_entities if x.entity_zero 

1637 )}. Please """ 

1638 "specify the full path " 

1639 "from one of the root entities to the target " 

1640 "attribute. " 

1641 ) 

1642 

1643 def _adjust_effective_path_for_current_path( 

1644 self, effective_path: PathRegistry, current_path: PathRegistry 

1645 ) -> Optional[PathRegistry]: 

1646 """receives the 'current_path' entry from an :class:`.ORMCompileState` 

1647 instance, which is set during lazy loads and secondary loader strategy 

1648 loads, and adjusts the given path to be relative to the 

1649 current_path. 

1650 

1651 E.g. given a loader path and current path: 

1652 

1653 .. sourcecode:: text 

1654 

1655 lp: User -> orders -> Order -> items -> Item -> keywords -> Keyword 

1656 

1657 cp: User -> orders -> Order -> items 

1658 

1659 The adjusted path would be: 

1660 

1661 .. sourcecode:: text 

1662 

1663 Item -> keywords -> Keyword 

1664 

1665 

1666 """ 

1667 chopped_start_path = Load._chop_path( 

1668 effective_path.natural_path, current_path 

1669 ) 

1670 if not chopped_start_path: 

1671 return None 

1672 

1673 tokens_removed_from_start_path = len(effective_path) - len( 

1674 chopped_start_path 

1675 ) 

1676 

1677 loader_lead_path_element = self.path[tokens_removed_from_start_path] 

1678 

1679 effective_path = PathRegistry.coerce( 

1680 (loader_lead_path_element,) + chopped_start_path[1:] 

1681 ) 

1682 

1683 return effective_path 

1684 

1685 def _init_path( 

1686 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

1687 ): 

1688 """Apply ORM attributes and/or wildcard to an existing path, producing 

1689 a new path. 

1690 

1691 This method is used within the :meth:`.create` method to initialize 

1692 a :class:`._LoadElement` object. 

1693 

1694 """ 

1695 raise NotImplementedError() 

1696 

1697 def _prepare_for_compile_state( 

1698 self, 

1699 parent_loader, 

1700 compile_state, 

1701 mapper_entities, 

1702 reconciled_lead_entity, 

1703 raiseerr, 

1704 ): 

1705 """implemented by subclasses.""" 

1706 raise NotImplementedError() 

1707 

1708 def process_compile_state( 

1709 self, 

1710 parent_loader, 

1711 compile_state, 

1712 mapper_entities, 

1713 reconciled_lead_entity, 

1714 raiseerr, 

1715 ): 

1716 """populate ORMCompileState.attributes with loader state for this 

1717 _LoadElement. 

1718 

1719 """ 

1720 keys = self._prepare_for_compile_state( 

1721 parent_loader, 

1722 compile_state, 

1723 mapper_entities, 

1724 reconciled_lead_entity, 

1725 raiseerr, 

1726 ) 

1727 for key in keys: 

1728 if key in compile_state.attributes: 

1729 compile_state.attributes[key] = _LoadElement._reconcile( 

1730 self, compile_state.attributes[key] 

1731 ) 

1732 else: 

1733 compile_state.attributes[key] = self 

1734 

1735 @classmethod 

1736 def create( 

1737 cls, 

1738 path: PathRegistry, 

1739 attr: Union[_AttrType, _StrPathToken, None], 

1740 strategy: Optional[_StrategyKey], 

1741 wildcard_key: Optional[_WildcardKeyType], 

1742 local_opts: Optional[_OptsType], 

1743 propagate_to_loaders: bool, 

1744 raiseerr: bool = True, 

1745 attr_group: Optional[_AttrGroupType] = None, 

1746 reconcile_to_other: Optional[bool] = None, 

1747 extra_criteria: Optional[Tuple[Any, ...]] = None, 

1748 ) -> _LoadElement: 

1749 """Create a new :class:`._LoadElement` object.""" 

1750 

1751 opt = cls.__new__(cls) 

1752 opt.path = path 

1753 opt.strategy = strategy 

1754 opt.propagate_to_loaders = propagate_to_loaders 

1755 opt.local_opts = ( 

1756 util.immutabledict(local_opts) if local_opts else util.EMPTY_DICT 

1757 ) 

1758 opt._extra_criteria = () 

1759 

1760 if reconcile_to_other is not None: 

1761 opt._reconcile_to_other = reconcile_to_other 

1762 elif strategy is None and not local_opts: 

1763 opt._reconcile_to_other = True 

1764 else: 

1765 opt._reconcile_to_other = None 

1766 

1767 path = opt._init_path( 

1768 path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

1769 ) 

1770 

1771 if not path: 

1772 return None # type: ignore[return-value] 

1773 

1774 assert opt.is_token_strategy == path.is_token 

1775 

1776 opt.path = path 

1777 return opt 

1778 

1779 def __init__(self) -> None: 

1780 raise NotImplementedError() 

1781 

1782 def _recurse(self) -> _LoadElement: 

1783 cloned = self._clone() 

1784 cloned.path = PathRegistry.coerce(self.path[:] + self.path[-2:]) 

1785 

1786 return cloned 

1787 

1788 def _prepend_path_from(self, parent: Load) -> _LoadElement: 

1789 """adjust the path of this :class:`._LoadElement` to be 

1790 a subpath of that of the given parent :class:`_orm.Load` object's 

1791 path. 

1792 

1793 This is used by the :meth:`_orm.Load._apply_to_parent` method, 

1794 which is in turn part of the :meth:`_orm.Load.options` method. 

1795 

1796 """ 

1797 

1798 if not any( 

1799 orm_util._entity_corresponds_to_use_path_impl( 

1800 elem, 

1801 self.path.odd_element(0), 

1802 ) 

1803 for elem in (parent.path.odd_element(-1),) 

1804 + parent.additional_source_entities 

1805 ): 

1806 raise sa_exc.ArgumentError( 

1807 f'Attribute "{self.path[1]}" does not link ' 

1808 f'from element "{entity_str(parent.path[-1])}".' 

1809 ) 

1810 

1811 return self._prepend_path(parent.path) 

1812 

1813 def _prepend_path(self, path: PathRegistry) -> Self: 

1814 cloned = self._clone() 

1815 

1816 assert cloned.strategy == self.strategy 

1817 assert cloned.local_opts == self.local_opts 

1818 assert cloned.is_class_strategy == self.is_class_strategy 

1819 

1820 cloned.path = PathRegistry.coerce(path[0:-1] + cloned.path[:]) 

1821 

1822 return cloned 

1823 

1824 @staticmethod 

1825 def _reconcile( 

1826 replacement: _LoadElement, existing: _LoadElement 

1827 ) -> _LoadElement: 

1828 """define behavior for when two Load objects are to be put into 

1829 the context.attributes under the same key. 

1830 

1831 :param replacement: ``_LoadElement`` that seeks to replace the 

1832 existing one 

1833 

1834 :param existing: ``_LoadElement`` that is already present. 

1835 

1836 """ 

1837 # mapper inheritance loading requires fine-grained "block other 

1838 # options" / "allow these options to be overridden" behaviors 

1839 # see test_poly_loading.py 

1840 

1841 if replacement._reconcile_to_other: 

1842 return existing 

1843 elif replacement._reconcile_to_other is False: 

1844 return replacement 

1845 elif existing._reconcile_to_other: 

1846 return replacement 

1847 elif existing._reconcile_to_other is False: 

1848 return existing 

1849 

1850 if existing is replacement: 

1851 return replacement 

1852 elif ( 

1853 existing.strategy == replacement.strategy 

1854 and existing.local_opts == replacement.local_opts 

1855 ): 

1856 return replacement 

1857 elif replacement.is_opts_only: 

1858 existing = existing._clone() 

1859 existing.local_opts = existing.local_opts.union( 

1860 replacement.local_opts 

1861 ) 

1862 existing._extra_criteria += replacement._extra_criteria 

1863 return existing 

1864 elif existing.is_opts_only: 

1865 replacement = replacement._clone() 

1866 replacement.local_opts = replacement.local_opts.union( 

1867 existing.local_opts 

1868 ) 

1869 replacement._extra_criteria += existing._extra_criteria 

1870 return replacement 

1871 elif replacement.path.is_token: 

1872 # use 'last one wins' logic for wildcard options. this is also 

1873 # kind of inconsistent vs. options that are specific paths which 

1874 # will raise as below 

1875 return replacement 

1876 

1877 raise sa_exc.InvalidRequestError( 

1878 f"Loader strategy replacement " 

1879 f"{replacement._to_option_method_string()} is in conflict " 

1880 f"with existing strategy {existing._to_option_method_string()}" 

1881 ) 

1882 

1883 def _to_option_method_string(self) -> str: 

1884 """Return a string representation of this :class:`._LoadElement` 

1885 as it would be written as a loader option method call, e.g. 

1886 ``"joinedload(User.orders)"``. 

1887 

1888 """ 

1889 assert ( 

1890 self.strategy is not None 

1891 ), "to_option_method_string() requires a strategy to be set" 

1892 

1893 for opt_key in self.local_opts: 

1894 fn = _STRATEGY_FN_LABELS.get((self.strategy, opt_key)) 

1895 if fn: 

1896 break 

1897 else: 

1898 fn = _STRATEGY_FN_LABELS.get((self.strategy, None)) 

1899 

1900 assert ( 

1901 fn is not None 

1902 ), f"No _STRATEGY_FN_LABELS entry for strategy {self.strategy!r}" 

1903 

1904 return f"{fn}({self.path.path_string()})" 

1905 

1906 

1907class _AttributeStrategyLoad(_LoadElement): 

1908 """Loader strategies against specific relationship or column paths. 

1909 

1910 e.g.:: 

1911 

1912 joinedload(User.addresses) 

1913 defer(Order.name) 

1914 selectinload(User.orders).lazyload(Order.items) 

1915 

1916 """ 

1917 

1918 __slots__ = ("_of_type", "_path_with_polymorphic_path") 

1919 

1920 __visit_name__ = "attribute_strategy_load_element" 

1921 

1922 _traverse_internals = _LoadElement._traverse_internals + [ 

1923 ("_of_type", visitors.ExtendedInternalTraversal.dp_multi), 

1924 ( 

1925 "_path_with_polymorphic_path", 

1926 visitors.ExtendedInternalTraversal.dp_has_cache_key, 

1927 ), 

1928 ] 

1929 

1930 _of_type: Union[Mapper[Any], AliasedInsp[Any], None] 

1931 _path_with_polymorphic_path: Optional[PathRegistry] 

1932 

1933 is_class_strategy = False 

1934 is_token_strategy = False 

1935 

1936 def _init_path( 

1937 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

1938 ): 

1939 assert attr is not None 

1940 self._of_type = None 

1941 self._path_with_polymorphic_path = None 

1942 insp, _, prop = _parse_attr_argument(attr) 

1943 

1944 if insp.is_property: 

1945 # direct property can be sent from internal strategy logic 

1946 # that sets up specific loaders, such as 

1947 # emit_lazyload->_lazyload_reverse 

1948 # prop = found_property = attr 

1949 prop = attr 

1950 path = path[prop] 

1951 

1952 if path.has_entity: 

1953 path = path.entity_path 

1954 return path 

1955 

1956 elif not insp.is_attribute: 

1957 # should not reach here; 

1958 assert False 

1959 

1960 # here we assume we have user-passed InstrumentedAttribute 

1961 if not orm_util._entity_corresponds_to_use_path_impl( 

1962 path[-1], attr.parent 

1963 ): 

1964 if raiseerr: 

1965 if attr_group and attr is not attr_group[0]: 

1966 raise sa_exc.ArgumentError( 

1967 "Can't apply wildcard ('*') or load_only() " 

1968 "loader option to multiple entities in the " 

1969 "same option. Use separate options per entity." 

1970 ) 

1971 else: 

1972 _raise_for_does_not_link(path, str(attr), attr.parent) 

1973 else: 

1974 return None 

1975 

1976 # note the essential logic of this attribute was very different in 

1977 # 1.4, where there were caching failures in e.g. 

1978 # test_relationship_criteria.py::RelationshipCriteriaTest:: 

1979 # test_selectinload_nested_criteria[True] if an existing 

1980 # "_extra_criteria" on a Load object were replaced with that coming 

1981 # from an attribute. This appears to have been an artifact of how 

1982 # _UnboundLoad / Load interacted together, which was opaque and 

1983 # poorly defined. 

1984 if extra_criteria: 

1985 assert not attr._extra_criteria 

1986 self._extra_criteria = extra_criteria 

1987 else: 

1988 self._extra_criteria = attr._extra_criteria 

1989 

1990 if getattr(attr, "_of_type", None): 

1991 ac = attr._of_type 

1992 ext_info = inspect(ac) 

1993 self._of_type = ext_info 

1994 

1995 self._path_with_polymorphic_path = path.entity_path[prop] 

1996 

1997 path = path[prop][ext_info] 

1998 

1999 else: 

2000 path = path[prop] 

2001 

2002 if path.has_entity: 

2003 path = path.entity_path 

2004 

2005 return path 

2006 

2007 def _prepend_path(self, path: PathRegistry) -> Self: 

2008 """Override to also prepend the path for _path_with_polymorphic_path. 

2009 

2010 When using .options() to chain loader options with of_type(), this 

2011 ensures that the polymorphic path information is correctly updated 

2012 to include the parent path. Fixes issue #13202. 

2013 """ 

2014 cloned = super()._prepend_path(path) 

2015 

2016 # Also prepend the parent path to _path_with_polymorphic_path if 

2017 # present 

2018 if self._path_with_polymorphic_path is not None: 

2019 cloned._path_with_polymorphic_path = PathRegistry.coerce( 

2020 path[0:-1] + self._path_with_polymorphic_path[:] 

2021 ) 

2022 

2023 return cloned 

2024 

2025 def _generate_extra_criteria(self, context): 

2026 """Apply the current bound parameters in a QueryContext to the 

2027 immediate "extra_criteria" stored with this Load object. 

2028 

2029 Load objects are typically pulled from the cached version of 

2030 the statement from a QueryContext. The statement currently being 

2031 executed will have new values (and keys) for bound parameters in the 

2032 extra criteria which need to be applied by loader strategies when 

2033 they handle this criteria for a result set. 

2034 

2035 """ 

2036 

2037 assert ( 

2038 self._extra_criteria 

2039 ), "this should only be called if _extra_criteria is present" 

2040 

2041 orig_query = context.compile_state.select_statement 

2042 current_query = context.query 

2043 

2044 # NOTE: while it seems like we should not do the "apply" operation 

2045 # here if orig_query is current_query, skipping it in the "optimized" 

2046 # case causes the query to be different from a cache key perspective, 

2047 # because we are creating a copy of the criteria which is no longer 

2048 # the same identity of the _extra_criteria in the loader option 

2049 # itself. cache key logic produces a different key for 

2050 # (A, copy_of_A) vs. (A, A), because in the latter case it shortens 

2051 # the second part of the key to just indicate on identity. 

2052 

2053 # if orig_query is current_query: 

2054 # not cached yet. just do the and_() 

2055 # return and_(*self._extra_criteria) 

2056 

2057 k1 = orig_query._generate_cache_key() 

2058 k2 = current_query._generate_cache_key() 

2059 

2060 return k2._apply_params_to_element(k1, and_(*self._extra_criteria)) 

2061 

2062 def _set_of_type_info(self, context, current_path): 

2063 assert self._path_with_polymorphic_path 

2064 

2065 pwpi = self._of_type 

2066 assert pwpi 

2067 if not pwpi.is_aliased_class: 

2068 pwpi = inspect( 

2069 orm_util.AliasedInsp._with_polymorphic_factory( 

2070 pwpi.mapper.base_mapper, 

2071 (pwpi.mapper,), 

2072 aliased=True, 

2073 _use_mapper_path=True, 

2074 ) 

2075 ) 

2076 start_path = self._path_with_polymorphic_path 

2077 if current_path: 

2078 new_path = self._adjust_effective_path_for_current_path( 

2079 start_path, current_path 

2080 ) 

2081 if new_path is None: 

2082 return 

2083 start_path = new_path 

2084 

2085 key = ("path_with_polymorphic", start_path.natural_path) 

2086 if key in context: 

2087 existing_aliased_insp = context[key] 

2088 this_aliased_insp = pwpi 

2089 if existing_aliased_insp.mapper is not this_aliased_insp.mapper: 

2090 raise sa_exc.InvalidRequestError( 

2091 "Can't combine of_type() targets " 

2092 f"{existing_aliased_insp} and {this_aliased_insp} " 

2093 f"along the relationship {start_path[-1]} in " 

2094 "the same query. To load multiple subclasses along a " 

2095 "single relationship, use a single with_polymorphic() " 

2096 "object that includes all of the target classes." 

2097 ) 

2098 new_aliased_insp = existing_aliased_insp._merge_with( 

2099 this_aliased_insp 

2100 ) 

2101 context[key] = new_aliased_insp 

2102 else: 

2103 context[key] = pwpi 

2104 

2105 def _prepare_for_compile_state( 

2106 self, 

2107 parent_loader, 

2108 compile_state, 

2109 mapper_entities, 

2110 reconciled_lead_entity, 

2111 raiseerr, 

2112 ): 

2113 # _AttributeStrategyLoad 

2114 

2115 current_path = compile_state.current_path 

2116 is_refresh = compile_state.compile_options._for_refresh_state 

2117 assert not self.path.is_token 

2118 

2119 if is_refresh and not self.propagate_to_loaders: 

2120 return [] 

2121 

2122 if self._of_type: 

2123 # apply additional with_polymorphic alias that may have been 

2124 # generated. this has to happen even if this is a defaultload 

2125 self._set_of_type_info(compile_state.attributes, current_path) 

2126 

2127 # omit setting loader attributes for a "defaultload" type of option 

2128 if not self.strategy and not self.local_opts: 

2129 return [] 

2130 

2131 if raiseerr and not reconciled_lead_entity: 

2132 self._raise_for_no_match(parent_loader, mapper_entities) 

2133 

2134 if self.path.has_entity: 

2135 effective_path = self.path.parent 

2136 else: 

2137 effective_path = self.path 

2138 

2139 if current_path: 

2140 assert effective_path is not None 

2141 effective_path = self._adjust_effective_path_for_current_path( 

2142 effective_path, current_path 

2143 ) 

2144 if effective_path is None: 

2145 return [] 

2146 

2147 return [("loader", cast(PathRegistry, effective_path).natural_path)] 

2148 

2149 def __getstate__(self): 

2150 d = super().__getstate__() 

2151 

2152 # can't pickle this. See 

2153 # test_pickled.py -> test_lazyload_extra_criteria_not_supported 

2154 # where we should be emitting a warning for the usual case where this 

2155 # would be non-None 

2156 d["_extra_criteria"] = () 

2157 

2158 if self._path_with_polymorphic_path: 

2159 d["_path_with_polymorphic_path"] = ( 

2160 self._path_with_polymorphic_path.serialize() 

2161 ) 

2162 

2163 if self._of_type: 

2164 if self._of_type.is_aliased_class: 

2165 d["_of_type"] = None 

2166 elif self._of_type.is_mapper: 

2167 d["_of_type"] = self._of_type.class_ 

2168 else: 

2169 assert False, "unexpected object for _of_type" 

2170 

2171 return d 

2172 

2173 def __setstate__(self, state): 

2174 super().__setstate__(state) 

2175 

2176 if state.get("_path_with_polymorphic_path", None): 

2177 self._path_with_polymorphic_path = PathRegistry.deserialize( 

2178 state["_path_with_polymorphic_path"] 

2179 ) 

2180 else: 

2181 self._path_with_polymorphic_path = None 

2182 

2183 if state.get("_of_type", None): 

2184 self._of_type = inspect(state["_of_type"]) 

2185 else: 

2186 self._of_type = None 

2187 

2188 

2189class _TokenStrategyLoad(_LoadElement): 

2190 """Loader strategies against wildcard attributes 

2191 

2192 e.g.:: 

2193 

2194 raiseload("*") 

2195 Load(User).lazyload("*") 

2196 defer("*") 

2197 load_only(User.name, User.email) # will create a defer('*') 

2198 joinedload(User.addresses).raiseload("*") 

2199 

2200 """ 

2201 

2202 __visit_name__ = "token_strategy_load_element" 

2203 

2204 inherit_cache = True 

2205 is_class_strategy = False 

2206 is_token_strategy = True 

2207 

2208 def _init_path( 

2209 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

2210 ): 

2211 # assert isinstance(attr, str) or attr is None 

2212 if attr is not None: 

2213 # the only strings accepted here are the wildcard and default 

2214 # tokens, either bare or already prefixed with a wildcard key. 

2215 # anything else is a leftover from the string based loader 

2216 # option API removed in 2.0 and gets the same error as any 

2217 # other string. note that testing only for a trailing "*", 

2218 # as was formerly the case, lets a name like "addresses.*" 

2219 # through to build a loader path that matches nothing 

2220 if attr in _ACCEPTED_TOKENS: 

2221 if wildcard_key: 

2222 attr = f"{wildcard_key}:{attr}" 

2223 

2224 path = path.token(attr) 

2225 return path 

2226 else: 

2227 raise sa_exc.ArgumentError( 

2228 "Strings are not accepted for attribute names in loader " 

2229 "options; please use class-bound attributes directly." 

2230 ) 

2231 return path 

2232 

2233 def _prepare_for_compile_state( 

2234 self, 

2235 parent_loader, 

2236 compile_state, 

2237 mapper_entities, 

2238 reconciled_lead_entity, 

2239 raiseerr, 

2240 ): 

2241 # _TokenStrategyLoad 

2242 

2243 current_path = compile_state.current_path 

2244 is_refresh = compile_state.compile_options._for_refresh_state 

2245 

2246 assert self.path.is_token 

2247 

2248 if is_refresh and not self.propagate_to_loaders: 

2249 return [] 

2250 

2251 # omit setting attributes for a "defaultload" type of option 

2252 if not self.strategy and not self.local_opts: 

2253 return [] 

2254 

2255 effective_path = self.path 

2256 if reconciled_lead_entity: 

2257 effective_path = PathRegistry.coerce( 

2258 (reconciled_lead_entity,) + effective_path.path[1:] 

2259 ) 

2260 

2261 if current_path: 

2262 new_effective_path = self._adjust_effective_path_for_current_path( 

2263 effective_path, current_path 

2264 ) 

2265 if new_effective_path is None: 

2266 return [] 

2267 effective_path = new_effective_path 

2268 

2269 # for a wildcard token, expand out the path we set 

2270 # to encompass everything from the query entity on 

2271 # forward. not clear if this is necessary when current_path 

2272 # is set. 

2273 

2274 return [ 

2275 ("loader", natural_path) 

2276 for natural_path in ( 

2277 cast( 

2278 _TokenRegistry, effective_path 

2279 )._generate_natural_for_superclasses() 

2280 ) 

2281 ] 

2282 

2283 

2284class _ClassStrategyLoad(_LoadElement): 

2285 """Loader strategies that deals with a class as a target, not 

2286 an attribute path 

2287 

2288 e.g.:: 

2289 

2290 q = s.query(Person).options( 

2291 selectin_polymorphic(Person, [Engineer, Manager]) 

2292 ) 

2293 

2294 """ 

2295 

2296 inherit_cache = True 

2297 is_class_strategy = True 

2298 is_token_strategy = False 

2299 

2300 __visit_name__ = "class_strategy_load_element" 

2301 

2302 def _init_path( 

2303 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

2304 ): 

2305 return path 

2306 

2307 def _prepare_for_compile_state( 

2308 self, 

2309 parent_loader, 

2310 compile_state, 

2311 mapper_entities, 

2312 reconciled_lead_entity, 

2313 raiseerr, 

2314 ): 

2315 # _ClassStrategyLoad 

2316 

2317 current_path = compile_state.current_path 

2318 is_refresh = compile_state.compile_options._for_refresh_state 

2319 

2320 if is_refresh and not self.propagate_to_loaders: 

2321 return [] 

2322 

2323 # omit setting attributes for a "defaultload" type of option 

2324 if not self.strategy and not self.local_opts: 

2325 return [] 

2326 

2327 effective_path = self.path 

2328 

2329 if current_path: 

2330 new_effective_path = self._adjust_effective_path_for_current_path( 

2331 effective_path, current_path 

2332 ) 

2333 if new_effective_path is None: 

2334 return [] 

2335 effective_path = new_effective_path 

2336 

2337 return [("loader", effective_path.natural_path)] 

2338 

2339 

2340def _generate_from_keys( 

2341 meth: Callable[..., _AbstractLoad], 

2342 keys: Tuple[_AttrType, ...], 

2343 chained: bool, 

2344 kw: Any, 

2345) -> _AbstractLoad: 

2346 lead_element: Optional[_AbstractLoad] = None 

2347 

2348 attr: Any 

2349 for is_default, _keys in (True, keys[0:-1]), (False, keys[-1:]): 

2350 for attr in _keys: 

2351 if isinstance(attr, str): 

2352 if attr.startswith("." + _WILDCARD_TOKEN): 

2353 util.warn_deprecated( 

2354 "The undocumented `.{WILDCARD}` format is " 

2355 "deprecated " 

2356 "and will be removed in a future version as " 

2357 "it is " 

2358 "believed to be unused. " 

2359 "If you have been using this functionality, " 

2360 "please " 

2361 "comment on Issue #4390 on the SQLAlchemy project " 

2362 "tracker.", 

2363 version="1.4", 

2364 ) 

2365 attr = attr[1:] 

2366 

2367 if attr == _WILDCARD_TOKEN: 

2368 if is_default: 

2369 raise sa_exc.ArgumentError( 

2370 "Wildcard token cannot be followed by " 

2371 "another entity", 

2372 ) 

2373 

2374 if lead_element is None: 

2375 lead_element = _WildcardLoad() 

2376 

2377 lead_element = meth(lead_element, _DEFAULT_TOKEN, **kw) 

2378 

2379 else: 

2380 raise sa_exc.ArgumentError( 

2381 "Strings are not accepted for attribute names in " 

2382 "loader options; please use class-bound " 

2383 "attributes directly.", 

2384 ) 

2385 else: 

2386 if lead_element is None: 

2387 _, lead_entity, _ = _parse_attr_argument(attr) 

2388 lead_element = Load(lead_entity) 

2389 

2390 if is_default: 

2391 if not chained: 

2392 lead_element = lead_element.defaultload(attr) 

2393 else: 

2394 lead_element = meth( 

2395 lead_element, attr, _is_chain=True, **kw 

2396 ) 

2397 else: 

2398 lead_element = meth(lead_element, attr, **kw) 

2399 

2400 assert lead_element 

2401 return lead_element 

2402 

2403 

2404def _parse_attr_argument( 

2405 attr: _AttrType, 

2406) -> Tuple[InspectionAttr, _InternalEntityType[Any], MapperProperty[Any]]: 

2407 """parse an attribute or wildcard argument to produce an 

2408 :class:`._AbstractLoad` instance. 

2409 

2410 This is used by the standalone loader strategy functions like 

2411 ``joinedload()``, ``defer()``, etc. to produce :class:`_orm.Load` or 

2412 :class:`._WildcardLoad` objects. 

2413 

2414 """ 

2415 try: 

2416 insp: InspectionAttr = inspect(attr) 

2417 except sa_exc.NoInspectionAvailable as err: 

2418 raise sa_exc.ArgumentError( 

2419 "expected ORM mapped attribute for loader strategy argument" 

2420 ) from err 

2421 

2422 lead_entity: _InternalEntityType[Any] 

2423 

2424 if insp_is_mapper_property(insp): 

2425 lead_entity = insp.parent 

2426 prop = insp 

2427 elif insp_is_attribute(insp): 

2428 lead_entity = insp.parent 

2429 prop = insp.prop 

2430 else: 

2431 raise sa_exc.ArgumentError( 

2432 "expected ORM mapped attribute for loader strategy argument" 

2433 ) 

2434 

2435 return insp, lead_entity, prop 

2436 

2437 

2438def _strategy_labels( 

2439 *strategy_keys: "Any", 

2440 discriminating_opt: Optional[str] = None, 

2441) -> Callable[[_FN], _FN]: 

2442 """Decorator that registers strategy key(s) -> function name in 

2443 ``_STRATEGY_FN_LABELS``. Apply below ``@loader_unbound_fn`` so that 

2444 ``fn.__name__`` is still the original function name when the decorator 

2445 runs. 

2446 

2447 :param discriminating_opt: optional local_opts key that distinguishes 

2448 this function from another function with the same strategy key. 

2449 When set, the entry is stored under ``(strategy_key, opt_key)`` 

2450 instead of ``(strategy_key, None)``, and ``__str__`` will use this 

2451 function name when that opt is present in ``local_opts``. 

2452 """ 

2453 

2454 def decorator(fn: _FN) -> _FN: 

2455 for key in strategy_keys: 

2456 _STRATEGY_FN_LABELS[(key, discriminating_opt)] = fn.__name__ 

2457 return fn 

2458 

2459 return decorator 

2460 

2461 

2462def loader_unbound_fn(fn: _FN) -> _FN: 

2463 """decorator that applies docstrings between standalone loader functions 

2464 and the loader methods on :class:`._AbstractLoad`. 

2465 

2466 """ 

2467 bound_fn = getattr(_AbstractLoad, fn.__name__) 

2468 fn_doc = bound_fn.__doc__ 

2469 bound_fn.__doc__ = f"""Produce a new :class:`_orm.Load` object with the 

2470:func:`_orm.{fn.__name__}` option applied. 

2471 

2472See :func:`_orm.{fn.__name__}` for usage examples. 

2473 

2474""" 

2475 

2476 fn.__doc__ = fn_doc 

2477 return fn 

2478 

2479 

2480def _expand_column_strategy_attrs( 

2481 attrs: Tuple[_AttrType, ...], 

2482) -> Tuple[_AttrType, ...]: 

2483 return cast( 

2484 "Tuple[_AttrType, ...]", 

2485 tuple( 

2486 a 

2487 for attr in attrs 

2488 for a in ( 

2489 cast("QueryableAttribute[Any]", attr)._column_strategy_attrs() 

2490 if hasattr(attr, "_column_strategy_attrs") 

2491 else (attr,) 

2492 ) 

2493 ), 

2494 ) 

2495 

2496 

2497# standalone functions follow. docstrings are filled in 

2498# by the ``@loader_unbound_fn`` decorator. 

2499 

2500 

2501@loader_unbound_fn 

2502@_strategy_labels((("lazy", "joined"),), discriminating_opt="eager_from_alias") 

2503def contains_eager(*keys: _AttrType, **kw: Any) -> _AbstractLoad: 

2504 return _generate_from_keys(Load.contains_eager, keys, True, kw) 

2505 

2506 

2507@loader_unbound_fn 

2508def load_only(*attrs: _AttrType, raiseload: bool = False) -> _AbstractLoad: 

2509 # TODO: attrs against different classes. we likely have to 

2510 # add some extra state to Load of some kind 

2511 attrs = _expand_column_strategy_attrs(attrs) 

2512 _, lead_element, _ = _parse_attr_argument(attrs[0]) 

2513 return Load(lead_element).load_only(*attrs, raiseload=raiseload) 

2514 

2515 

2516@loader_unbound_fn 

2517@_strategy_labels((("lazy", "joined"),)) 

2518def joinedload(*keys: _AttrType, **kw: Any) -> _AbstractLoad: 

2519 return _generate_from_keys(Load.joinedload, keys, False, kw) 

2520 

2521 

2522@loader_unbound_fn 

2523@_strategy_labels((("lazy", "subquery"),)) 

2524def subqueryload(*keys: _AttrType) -> _AbstractLoad: 

2525 return _generate_from_keys(Load.subqueryload, keys, False, {}) 

2526 

2527 

2528@loader_unbound_fn 

2529@_strategy_labels((("lazy", "selectin"),)) 

2530def selectinload( 

2531 *keys: _AttrType, 

2532 recursion_depth: Optional[int] = None, 

2533 chunksize: Optional[int] = None, 

2534) -> _AbstractLoad: 

2535 return _generate_from_keys( 

2536 Load.selectinload, 

2537 keys, 

2538 False, 

2539 {"recursion_depth": recursion_depth, "chunksize": chunksize}, 

2540 ) 

2541 

2542 

2543@loader_unbound_fn 

2544@_strategy_labels((("lazy", "select"),)) 

2545def lazyload(*keys: _AttrType) -> _AbstractLoad: 

2546 return _generate_from_keys(Load.lazyload, keys, False, {}) 

2547 

2548 

2549@loader_unbound_fn 

2550@_strategy_labels((("lazy", "immediate"),)) 

2551def immediateload( 

2552 *keys: _AttrType, recursion_depth: Optional[int] = None 

2553) -> _AbstractLoad: 

2554 return _generate_from_keys( 

2555 Load.immediateload, keys, False, {"recursion_depth": recursion_depth} 

2556 ) 

2557 

2558 

2559@loader_unbound_fn 

2560@_strategy_labels((("lazy", "noload"),)) 

2561def noload(*keys: _AttrType) -> _AbstractLoad: 

2562 return _generate_from_keys(Load.noload, keys, False, {}) 

2563 

2564 

2565@loader_unbound_fn 

2566@_strategy_labels((("lazy", "raise"),), (("lazy", "raise_on_sql"),)) 

2567def raiseload(*keys: _AttrType, **kw: Any) -> _AbstractLoad: 

2568 return _generate_from_keys(Load.raiseload, keys, False, kw) 

2569 

2570 

2571@loader_unbound_fn 

2572def defaultload(*keys: _AttrType) -> _AbstractLoad: 

2573 return _generate_from_keys(Load.defaultload, keys, False, {}) 

2574 

2575 

2576@loader_unbound_fn 

2577@_strategy_labels( 

2578 (("deferred", True), ("instrument", True)), 

2579 (("deferred", True), ("instrument", True), ("raiseload", True)), 

2580) 

2581def defer(key: _AttrType, *, raiseload: bool = False) -> _AbstractLoad: 

2582 if raiseload: 

2583 kw = {"raiseload": raiseload} 

2584 else: 

2585 kw = {} 

2586 

2587 return _generate_from_keys(Load.defer, (key,), False, kw) 

2588 

2589 

2590@loader_unbound_fn 

2591@_strategy_labels((("deferred", False), ("instrument", True))) 

2592def undefer(key: _AttrType) -> _AbstractLoad: 

2593 return _generate_from_keys(Load.undefer, (key,), False, {}) 

2594 

2595 

2596@loader_unbound_fn 

2597def undefer_group(name: str) -> _AbstractLoad: 

2598 element = _WildcardLoad() 

2599 return element.undefer_group(name) 

2600 

2601 

2602@loader_unbound_fn 

2603@_strategy_labels((("query_expression", True),)) 

2604def with_expression( 

2605 key: _AttrType, expression: _ColumnExpressionArgument[Any] 

2606) -> _AbstractLoad: 

2607 return _generate_from_keys( 

2608 Load.with_expression, (key,), False, {"expression": expression} 

2609 ) 

2610 

2611 

2612@loader_unbound_fn 

2613@_strategy_labels((("selectinload_polymorphic", True),)) 

2614def selectin_polymorphic( 

2615 base_cls: _EntityType[Any], classes: Iterable[Type[Any]] 

2616) -> _AbstractLoad: 

2617 ul = Load(base_cls) 

2618 return ul.selectin_polymorphic(classes) 

2619 

2620 

2621def _raise_for_does_not_link(path, attrname, parent_entity): 

2622 if len(path) > 1: 

2623 path_is_of_type = path[-1].entity is not path[-2].mapper.class_ 

2624 

2625 raise sa_exc.ArgumentError( 

2626 f'ORM mapped entity or attribute "{attrname}" does not ' 

2627 f'link from relationship "{entity_str(path[-2])}%s".%s' 

2628 % ( 

2629 ( 

2630 f".of_type({entity_str(path[-1])})" 

2631 if path_is_of_type 

2632 else "" 

2633 ), 

2634 ( 

2635 " Did you mean to use " 

2636 f'"{entity_str(path[-2])}' 

2637 f'.of_type({entity_str(parent_entity)})" or ' 

2638 '"loadopt.options(' 

2639 f"selectin_polymorphic({path[-2].mapper.class_.__name__}, " 

2640 f'[{entity_str(parent_entity)}]), ...)" ?' 

2641 if not path_is_of_type 

2642 and not path[-1].is_aliased_class 

2643 and orm_util._entity_corresponds_to( 

2644 path.entity, inspect(parent_entity).mapper 

2645 ) 

2646 else "" 

2647 ), 

2648 ) 

2649 ) 

2650 else: 

2651 raise sa_exc.ArgumentError( 

2652 f'ORM mapped attribute "{attrname}" does not ' 

2653 f'link mapped class "{entity_str(path[-1])}"' 

2654 )