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

813 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 Final 

19from typing import Iterable 

20from typing import Literal 

21from typing import Optional 

22from typing import overload 

23from typing import Sequence 

24from typing import Tuple 

25from typing import Type 

26from typing import TypeVar 

27from typing import Union 

28 

29from . import util as orm_util 

30from ._typing import insp_is_attribute 

31from ._typing import insp_is_mapper 

32from ._typing import insp_is_mapper_property 

33from .attributes import QueryableAttribute 

34from .base import entity_str 

35from .base import InspectionAttr 

36from .interfaces import LoaderOption 

37from .path_registry import _AbstractEntityRegistry 

38from .path_registry import _DEFAULT_TOKEN 

39from .path_registry import _StrPathToken 

40from .path_registry import _TokenRegistry 

41from .path_registry import _WILDCARD_TOKEN 

42from .path_registry import path_is_property 

43from .path_registry import PathRegistry 

44from .util import _orm_full_deannotate 

45from .util import AliasedInsp 

46from .. import exc as sa_exc 

47from .. import inspect 

48from .. import util 

49from ..sql import and_ 

50from ..sql import cache_key 

51from ..sql import coercions 

52from ..sql import roles 

53from ..sql import traversals 

54from ..sql import visitors 

55from ..sql.base import _generative 

56from ..util.typing import Self 

57 

58_RELATIONSHIP_TOKEN: Final[Literal["relationship"]] = "relationship" 

59_COLUMN_TOKEN: Final[Literal["column"]] = "column" 

60 

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

62 

63if typing.TYPE_CHECKING: 

64 from ._typing import _EntityType 

65 from ._typing import _InternalEntityType 

66 from .context import _MapperEntity 

67 from .context import _ORMCompileState 

68 from .context import QueryContext 

69 from .interfaces import _StrategyKey 

70 from .interfaces import MapperProperty 

71 from .interfaces import ORMOption 

72 from .mapper import Mapper 

73 from .path_registry import _PathRepresentation 

74 from ..sql._typing import _ColumnExpressionArgument 

75 from ..sql._typing import _FromClauseArgument 

76 from ..sql.cache_key import _CacheKeyTraversalType 

77 from ..sql.cache_key import CacheKey 

78 

79 

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

81 

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

83_StrategySpec = Dict[str, Any] 

84_OptsType = Dict[str, Any] 

85_AttrGroupType = Tuple[_AttrType, ...] 

86 

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

88# populated by the @_strategy_labels() decorator below 

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

90 

91 

92class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption): 

93 __slots__ = ("propagate_to_loaders",) 

94 

95 _is_strategy_option = True 

96 propagate_to_loaders: bool 

97 

98 def contains_eager( 

99 self, 

100 attr: _AttrType, 

101 alias: Optional[_FromClauseArgument] = None, 

102 _is_chain: bool = False, 

103 _propagate_to_loaders: bool = False, 

104 ) -> Self: 

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

106 columns stated manually in the query. 

107 

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

109 both method-chained and standalone operation. 

110 

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

112 the desired rows, i.e.:: 

113 

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

115 

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

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

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

119 

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

121 collection; queries will normally want to use the 

122 :ref:`orm_queryguide_populate_existing` execution option assuming the 

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

124 

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

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

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

128 

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

130 

131 .. seealso:: 

132 

133 :ref:`loading_toplevel` 

134 

135 :ref:`contains_eager` 

136 

137 """ 

138 if alias is not None: 

139 if not isinstance(alias, str): 

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

141 else: 

142 util.warn_deprecated( 

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

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

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

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

147 version="1.4", 

148 ) 

149 coerced_alias = alias 

150 

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

152 assert isinstance(attr, QueryableAttribute) 

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

154 assert ot is not None 

155 coerced_alias = ot.selectable 

156 else: 

157 coerced_alias = None 

158 

159 cloned = self._set_relationship_strategy( 

160 attr, 

161 {"lazy": "joined"}, 

162 propagate_to_loaders=_propagate_to_loaders, 

163 opts={"eager_from_alias": coerced_alias}, 

164 _reconcile_to_other=True if _is_chain else None, 

165 ) 

166 return cloned 

167 

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

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

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

171 deferred. 

172 

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

174 both method-chained and standalone operation. 

175 

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

177 ``fullname`` attributes:: 

178 

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

180 

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

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

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

184 

185 session.query(User).options( 

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

187 ) 

188 

189 For a statement that has multiple entities, 

190 the lead entity can be 

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

192 

193 stmt = ( 

194 select(User, Address) 

195 .join(User.addresses) 

196 .options( 

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

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

199 ) 

200 ) 

201 

202 When used together with the 

203 :ref:`populate_existing <orm_queryguide_populate_existing>` 

204 execution option only the attributes listed will be refreshed. 

205 

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

207 

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

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

210 to prevent unwanted SQL from being emitted. 

211 

212 .. versionadded:: 2.0 

213 

214 .. seealso:: 

215 

216 :ref:`orm_queryguide_column_deferral` - in the 

217 :ref:`queryguide_toplevel` 

218 

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

220 

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

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

223 to prevent unwanted SQL from being emitted. 

224 

225 .. versionadded:: 2.0 

226 

227 """ 

228 cloned = self._set_column_strategy( 

229 _expand_column_strategy_attrs(attrs), 

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

231 ) 

232 

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

234 if raiseload: 

235 wildcard_strategy["raiseload"] = True 

236 

237 cloned = cloned._set_column_strategy( 

238 ("*",), 

239 wildcard_strategy, 

240 ) 

241 return cloned 

242 

243 def joinedload( 

244 self, 

245 attr: _AttrType, 

246 innerjoin: Optional[bool] = None, 

247 ) -> Self: 

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

249 eager loading. 

250 

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

252 both method-chained and standalone operation. 

253 

254 examples:: 

255 

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

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

258 

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

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

261 

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

263 # joined-load the keywords collection 

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

265 

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

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

268 

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

270 

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

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

273 

274 select(A).options( 

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

276 ) 

277 

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

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

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

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

282 directly supported. 

283 

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

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

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

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

288 is an outerjoin:: 

289 

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

291 

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

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

294 

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

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

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

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

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

300 

301 .. note:: 

302 

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

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

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

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

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

308 

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

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

311 explicit JOINs with eager loading of collections, use 

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

313 

314 .. seealso:: 

315 

316 :ref:`loading_toplevel` 

317 

318 :ref:`joined_eager_loading` 

319 

320 """ # noqa: E501 

321 loader = self._set_relationship_strategy( 

322 attr, 

323 {"lazy": "joined"}, 

324 opts=( 

325 {"innerjoin": innerjoin} 

326 if innerjoin is not None 

327 else util.EMPTY_DICT 

328 ), 

329 ) 

330 return loader 

331 

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

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

334 subquery eager loading. 

335 

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

337 both method-chained and standalone operation. 

338 

339 examples:: 

340 

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

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

343 

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

345 select(Order).options( 

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

347 ) 

348 

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

350 # subquery-load the keywords collection 

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

352 

353 .. seealso:: 

354 

355 :ref:`loading_toplevel` 

356 

357 :ref:`subquery_eager_loading` 

358 

359 """ 

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

361 

362 def selectinload( 

363 self, 

364 attr: _AttrType, 

365 recursion_depth: Optional[int] = None, 

366 chunksize: Optional[int] = None, 

367 ) -> Self: 

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

369 SELECT IN eager loading. 

370 

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

372 both method-chained and standalone operation. 

373 

374 examples:: 

375 

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

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

378 

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

380 select(Order).options( 

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

382 ) 

383 

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

385 # selectin-load the keywords collection 

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

387 

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

389 in conjunction with a self-referential relationship, 

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

391 automatically until no items are found. 

392 

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

394 currently supports only self-referential relationships. There 

395 is not yet an option to automatically traverse recursive structures 

396 with more than one relationship involved. 

397 

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

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

400 status for the 2.0 series. 

401 

402 .. versionadded:: 2.0 added 

403 :paramref:`_orm.selectinload.recursion_depth` 

404 

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

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

407 to the passed parameter 

408 

409 .. versionadded:: 2.1.0b3 

410 

411 .. seealso:: 

412 

413 :ref:`loading_toplevel` 

414 

415 :ref:`selectin_eager_loading` 

416 

417 """ 

418 return self._set_relationship_strategy( 

419 attr, 

420 {"lazy": "selectin"}, 

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

422 ) 

423 

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

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

426 loading. 

427 

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

429 both method-chained and standalone operation. 

430 

431 .. seealso:: 

432 

433 :ref:`loading_toplevel` 

434 

435 :ref:`lazy_loading` 

436 

437 """ 

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

439 

440 def immediateload( 

441 self, 

442 attr: _AttrType, 

443 recursion_depth: Optional[int] = None, 

444 ) -> Self: 

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

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

447 

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

449 fire off any additional eager loaders. 

450 

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

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

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

454 

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

456 both method-chained and standalone operation. 

457 

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

459 in conjunction with a self-referential relationship, 

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

461 automatically until no items are found. 

462 

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

464 currently supports only self-referential relationships. There 

465 is not yet an option to automatically traverse recursive structures 

466 with more than one relationship involved. 

467 

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

469 treated as "alpha" status 

470 

471 .. versionadded:: 2.0 added 

472 :paramref:`_orm.immediateload.recursion_depth` 

473 

474 

475 .. seealso:: 

476 

477 :ref:`loading_toplevel` 

478 

479 :ref:`selectin_eager_loading` 

480 

481 """ 

482 loader = self._set_relationship_strategy( 

483 attr, 

484 {"lazy": "immediate"}, 

485 opts={"recursion_depth": recursion_depth}, 

486 ) 

487 return loader 

488 

489 @util.deprecated( 

490 "2.1", 

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

492 "in a future release. This option " 

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

494 "items.", 

495 ) 

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

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

498 unloaded. 

499 

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

501 producing any loading effect. 

502 

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

504 only. 

505 

506 .. seealso:: 

507 

508 :ref:`loading_toplevel` 

509 

510 """ 

511 

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

513 

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

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

516 

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

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

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

520 ensure that all relationship attributes that are accessed in a 

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

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

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

524 

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

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

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

528 :func:`.defer` loader option. 

529 

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

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

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

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

534 

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

536 both method-chained and standalone operation. 

537 

538 .. seealso:: 

539 

540 :ref:`loading_toplevel` 

541 

542 :ref:`prevent_lazy_with_raiseload` 

543 

544 :ref:`orm_queryguide_deferred_raiseload` 

545 

546 """ 

547 

548 return self._set_relationship_strategy( 

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

550 ) 

551 

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

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

554 

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

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

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

558 loading will be used. 

559 

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

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

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

563 element of an element:: 

564 

565 session.query(MyClass).options( 

566 defaultload(MyClass.someattribute).joinedload( 

567 MyOtherClass.someotherattribute 

568 ) 

569 ) 

570 

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

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

573 

574 session.scalars( 

575 select(MyClass).options( 

576 defaultload(MyClass.someattribute) 

577 .defer("some_column") 

578 .undefer("some_other_column") 

579 ) 

580 ) 

581 

582 .. seealso:: 

583 

584 :ref:`orm_queryguide_relationship_sub_options` 

585 

586 :meth:`_orm.Load.options` 

587 

588 """ 

589 return self._set_relationship_strategy(attr, None) 

590 

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

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

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

594 

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

596 both method-chained and standalone operation. 

597 

598 e.g.:: 

599 

600 from sqlalchemy.orm import defer 

601 

602 session.query(MyClass).options( 

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

604 ) 

605 

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

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

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

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

610 

611 session.query(MyClass).options( 

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

613 ) 

614 

615 Multiple deferral options related to a relationship can be bundled 

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

617 

618 

619 select(MyClass).options( 

620 defaultload(MyClass.someattr).options( 

621 defer(RelatedClass.some_column), 

622 defer(RelatedClass.some_other_column), 

623 defer(RelatedClass.another_column), 

624 ) 

625 ) 

626 

627 :param key: Attribute to be deferred. 

628 

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

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

631 to prevent unwanted SQL from being emitted. 

632 

633 .. versionadded:: 1.4 

634 

635 .. seealso:: 

636 

637 :ref:`orm_queryguide_column_deferral` - in the 

638 :ref:`queryguide_toplevel` 

639 

640 :func:`_orm.load_only` 

641 

642 :func:`_orm.undefer` 

643 

644 """ 

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

646 if raiseload: 

647 strategy["raiseload"] = True 

648 return self._set_column_strategy( 

649 _expand_column_strategy_attrs((key,)), strategy 

650 ) 

651 

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

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

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

655 as a whole. 

656 

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

658 :func:`.deferred` attribute. 

659 

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

661 both method-chained and standalone operation. 

662 

663 Examples:: 

664 

665 # undefer two columns 

666 session.query(MyClass).options( 

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

668 ) 

669 

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

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

672 

673 # undefer a column on a related object 

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

675 

676 :param key: Attribute to be undeferred. 

677 

678 .. seealso:: 

679 

680 :ref:`orm_queryguide_column_deferral` - in the 

681 :ref:`queryguide_toplevel` 

682 

683 :func:`_orm.defer` 

684 

685 :func:`_orm.undefer_group` 

686 

687 """ # noqa: E501 

688 return self._set_column_strategy( 

689 _expand_column_strategy_attrs((key,)), 

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

691 ) 

692 

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

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

695 undeferred. 

696 

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

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

699 

700 E.g:: 

701 

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

703 

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

705 spelled out using relationship loader options, such as 

706 :func:`_orm.defaultload`:: 

707 

708 select(MyClass).options( 

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

710 ) 

711 

712 .. seealso:: 

713 

714 :ref:`orm_queryguide_column_deferral` - in the 

715 :ref:`queryguide_toplevel` 

716 

717 :func:`_orm.defer` 

718 

719 :func:`_orm.undefer` 

720 

721 """ 

722 return self._set_column_strategy( 

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

724 ) 

725 

726 def with_expression( 

727 self, 

728 key: _AttrType, 

729 expression: _ColumnExpressionArgument[Any], 

730 ) -> Self: 

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

732 attribute. 

733 

734 This option is used in conjunction with the 

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

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

737 

738 E.g.:: 

739 

740 stmt = select(SomeClass).options( 

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

742 ) 

743 

744 :param key: Attribute to be populated 

745 

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

747 

748 .. seealso:: 

749 

750 :ref:`orm_queryguide_with_expression` - background and usage 

751 examples 

752 

753 """ 

754 

755 expression = _orm_full_deannotate( 

756 coercions.expect(roles.LabeledColumnExprRole, expression) 

757 ) 

758 

759 return self._set_column_strategy( 

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

761 ) 

762 

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

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

765 specific to a subclass. 

766 

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

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

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

770 

771 .. seealso:: 

772 

773 :ref:`polymorphic_selectin` 

774 

775 """ 

776 self = self._set_class_strategy( 

777 {"selectinload_polymorphic": True}, 

778 opts={ 

779 "entities": tuple( 

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

781 ) 

782 }, 

783 ) 

784 return self 

785 

786 @overload 

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

788 

789 @overload 

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

791 

792 def _coerce_strat( 

793 self, strategy: Optional[_StrategySpec] 

794 ) -> Optional[_StrategyKey]: 

795 if strategy is not None: 

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

797 else: 

798 strategy_key = None 

799 return strategy_key 

800 

801 @_generative 

802 def _set_relationship_strategy( 

803 self, 

804 attr: _AttrType, 

805 strategy: Optional[_StrategySpec], 

806 propagate_to_loaders: bool = True, 

807 opts: Optional[_OptsType] = None, 

808 _reconcile_to_other: Optional[bool] = None, 

809 ) -> Self: 

810 strategy_key = self._coerce_strat(strategy) 

811 

812 self._clone_for_bind_strategy( 

813 (attr,), 

814 strategy_key, 

815 _RELATIONSHIP_TOKEN, 

816 opts=opts, 

817 propagate_to_loaders=propagate_to_loaders, 

818 reconcile_to_other=_reconcile_to_other, 

819 ) 

820 return self 

821 

822 @_generative 

823 def _set_column_strategy( 

824 self, 

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

826 strategy: Optional[_StrategySpec], 

827 opts: Optional[_OptsType] = None, 

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

829 ) -> Self: 

830 strategy_key = self._coerce_strat(strategy) 

831 

832 self._clone_for_bind_strategy( 

833 attrs, 

834 strategy_key, 

835 _COLUMN_TOKEN, 

836 opts=opts, 

837 attr_group=attrs, 

838 extra_criteria=extra_criteria, 

839 ) 

840 return self 

841 

842 @_generative 

843 def _set_generic_strategy( 

844 self, 

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

846 strategy: _StrategySpec, 

847 _reconcile_to_other: Optional[bool] = None, 

848 ) -> Self: 

849 strategy_key = self._coerce_strat(strategy) 

850 self._clone_for_bind_strategy( 

851 attrs, 

852 strategy_key, 

853 None, 

854 propagate_to_loaders=True, 

855 reconcile_to_other=_reconcile_to_other, 

856 ) 

857 return self 

858 

859 @_generative 

860 def _set_class_strategy( 

861 self, strategy: _StrategySpec, opts: _OptsType 

862 ) -> Self: 

863 strategy_key = self._coerce_strat(strategy) 

864 

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

866 return self 

867 

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

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

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

871 

872 Implementation is provided by subclasses. 

873 

874 """ 

875 raise NotImplementedError() 

876 

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

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

879 :class:`_orm._AbstractLoad` object. 

880 

881 Implementation is provided by subclasses. 

882 

883 """ 

884 raise NotImplementedError() 

885 

886 def _clone_for_bind_strategy( 

887 self, 

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

889 strategy: Optional[_StrategyKey], 

890 wildcard_key: Optional[_WildcardKeyType], 

891 opts: Optional[_OptsType] = None, 

892 attr_group: Optional[_AttrGroupType] = None, 

893 propagate_to_loaders: bool = True, 

894 reconcile_to_other: Optional[bool] = None, 

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

896 ) -> Self: 

897 raise NotImplementedError() 

898 

899 def process_compile_state_replaced_entities( 

900 self, 

901 compile_state: _ORMCompileState, 

902 mapper_entities: Sequence[_MapperEntity], 

903 ) -> None: 

904 if not compile_state.compile_options._enable_eagerloads: 

905 return 

906 

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

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

909 # for the entities having been replaced with equivalents 

910 self._process( 

911 compile_state, 

912 mapper_entities, 

913 not bool(compile_state.current_path), 

914 ) 

915 

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

917 if not compile_state.compile_options._enable_eagerloads: 

918 return 

919 

920 self._process( 

921 compile_state, 

922 compile_state._lead_mapper_entities, 

923 not bool(compile_state.current_path) 

924 and not compile_state.compile_options._for_refresh_state, 

925 ) 

926 

927 def _process( 

928 self, 

929 compile_state: _ORMCompileState, 

930 mapper_entities: Sequence[_MapperEntity], 

931 raiseerr: bool, 

932 ) -> None: 

933 """implemented by subclasses""" 

934 raise NotImplementedError() 

935 

936 @classmethod 

937 def _chop_path( 

938 cls, 

939 to_chop: _PathRepresentation, 

940 path: PathRegistry, 

941 debug: bool = False, 

942 ) -> Optional[_PathRepresentation]: 

943 i = -1 

944 

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

946 zip(to_chop, path.natural_path) 

947 ): 

948 if isinstance(c_token, str): 

949 if i == 0 and ( 

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

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

952 ): 

953 return to_chop 

954 elif ( 

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

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

957 ): 

958 return None 

959 

960 if c_token is p_token: 

961 continue 

962 elif ( 

963 isinstance(c_token, InspectionAttr) 

964 and insp_is_mapper(c_token) 

965 and insp_is_mapper(p_token) 

966 and c_token.isa(p_token) 

967 ): 

968 continue 

969 

970 else: 

971 return None 

972 return to_chop[i + 1 :] 

973 

974 

975class Load(_AbstractLoad): 

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

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

978 order to affect how various mapped attributes are loaded. 

979 

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

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

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

983 except for in some very specific cases. 

984 

985 .. seealso:: 

986 

987 :ref:`orm_queryguide_relationship_per_entity_wildcard` - illustrates an 

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

989 

990 """ 

991 

992 __slots__ = ( 

993 "path", 

994 "context", 

995 "additional_source_entities", 

996 ) 

997 

998 _traverse_internals = [ 

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

1000 ( 

1001 "context", 

1002 visitors.InternalTraversal.dp_has_cache_key_list, 

1003 ), 

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

1005 ( 

1006 "additional_source_entities", 

1007 visitors.InternalTraversal.dp_has_cache_key_list, 

1008 ), 

1009 ] 

1010 _cache_key_traversal = None 

1011 

1012 path: PathRegistry 

1013 context: Tuple[_LoadElement, ...] 

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

1015 

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

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

1018 insp._post_inspect 

1019 

1020 self.path = insp._path_registry 

1021 self.context = () 

1022 self.propagate_to_loaders = False 

1023 self.additional_source_entities = () 

1024 

1025 def __str__(self) -> str: 

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

1027 

1028 @classmethod 

1029 def _construct_for_existing_path( 

1030 cls, path: _AbstractEntityRegistry 

1031 ) -> Load: 

1032 load = cls.__new__(cls) 

1033 load.path = path 

1034 load.context = () 

1035 load.propagate_to_loaders = False 

1036 load.additional_source_entities = () 

1037 return load 

1038 

1039 def _adapt_cached_option_to_uncached_option( 

1040 self, context: QueryContext, uncached_opt: ORMOption 

1041 ) -> ORMOption: 

1042 if uncached_opt is self: 

1043 return self 

1044 return self._adjust_for_extra_criteria(context) 

1045 

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

1047 cloned = self._clone() 

1048 cloned.context = tuple( 

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

1050 ) 

1051 return cloned 

1052 

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

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

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

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

1057 

1058 """ 

1059 

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

1061 # actually have any extra_criteria options, which is the 

1062 # common case 

1063 for value in self.context: 

1064 if value._extra_criteria: 

1065 break 

1066 else: 

1067 return self 

1068 

1069 replacement_cache_key = context.user_passed_query._generate_cache_key() 

1070 

1071 if replacement_cache_key is None: 

1072 return self 

1073 

1074 orig_query = context.compile_state.select_statement 

1075 orig_cache_key = orig_query._generate_cache_key() 

1076 assert orig_cache_key is not None 

1077 

1078 def process( 

1079 opt: _LoadElement, 

1080 replacement_cache_key: CacheKey, 

1081 orig_cache_key: CacheKey, 

1082 ) -> _LoadElement: 

1083 cloned_opt = opt._clone() 

1084 

1085 cloned_opt._extra_criteria = tuple( 

1086 replacement_cache_key._apply_params_to_element( 

1087 orig_cache_key, crit 

1088 ) 

1089 for crit in cloned_opt._extra_criteria 

1090 ) 

1091 

1092 return cloned_opt 

1093 

1094 cloned = self._clone() 

1095 cloned.context = tuple( 

1096 ( 

1097 process(value, replacement_cache_key, orig_cache_key) 

1098 if value._extra_criteria 

1099 else value 

1100 ) 

1101 for value in self.context 

1102 ) 

1103 return cloned 

1104 

1105 def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr): 

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

1107 entity inside of _LoadElement objects. 

1108 

1109 """ 

1110 path = self.path 

1111 

1112 for ent in mapper_entities: 

1113 ezero = ent.entity_zero 

1114 if ezero and orm_util._entity_corresponds_to( 

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

1116 # safe to pass to _entity_corresponds_to() 

1117 ezero, 

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

1119 ): 

1120 return ezero 

1121 

1122 return None 

1123 

1124 def _process( 

1125 self, 

1126 compile_state: _ORMCompileState, 

1127 mapper_entities: Sequence[_MapperEntity], 

1128 raiseerr: bool, 

1129 ) -> None: 

1130 reconciled_lead_entity = self._reconcile_query_entities_with_us( 

1131 mapper_entities, raiseerr 

1132 ) 

1133 

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

1135 has_current_path = bool(compile_state.compile_options._current_path) 

1136 

1137 for loader in self.context: 

1138 # issue #11292 

1139 # historically, propagate_to_loaders was only considered at 

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

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

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

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

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

1145 # so we check again 

1146 if has_current_path and not loader.propagate_to_loaders: 

1147 continue 

1148 loader.process_compile_state( 

1149 self, 

1150 compile_state, 

1151 mapper_entities, 

1152 reconciled_lead_entity, 

1153 raiseerr, 

1154 ) 

1155 

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

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

1158 :class:`_orm.Load` object. 

1159 

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

1161 

1162 """ 

1163 cloned = self._generate() 

1164 

1165 assert cloned.propagate_to_loaders == self.propagate_to_loaders 

1166 

1167 if not any( 

1168 orm_util._entity_corresponds_to_use_path_impl( 

1169 elem, cloned.path.odd_element(0) 

1170 ) 

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

1172 + parent.additional_source_entities 

1173 ): 

1174 if len(cloned.path) > 1: 

1175 attrname = cloned.path[1] 

1176 parent_entity = cloned.path[0] 

1177 else: 

1178 attrname = cloned.path[0] 

1179 parent_entity = cloned.path[0] 

1180 _raise_for_does_not_link(parent.path, attrname, parent_entity) 

1181 

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

1183 

1184 if self.context: 

1185 cloned.context = tuple( 

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

1187 ) 

1188 

1189 if cloned.context: 

1190 parent.context += cloned.context 

1191 parent.additional_source_entities += ( 

1192 cloned.additional_source_entities 

1193 ) 

1194 

1195 @_generative 

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

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

1198 :class:`_orm.Load` 

1199 object. 

1200 

1201 E.g.:: 

1202 

1203 query = session.query(Author) 

1204 query = query.options( 

1205 joinedload(Author.book).options( 

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

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

1208 ) 

1209 ) 

1210 

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

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

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

1214 

1215 .. seealso:: 

1216 

1217 :func:`.defaultload` 

1218 

1219 :ref:`orm_queryguide_relationship_sub_options` 

1220 

1221 """ 

1222 for opt in opts: 

1223 try: 

1224 opt._apply_to_parent(self) 

1225 except AttributeError as ae: 

1226 if not isinstance(opt, _AbstractLoad): 

1227 raise sa_exc.ArgumentError( 

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

1229 "Load.options() method." 

1230 ) from ae 

1231 else: 

1232 raise 

1233 return self 

1234 

1235 def _clone_for_bind_strategy( 

1236 self, 

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

1238 strategy: Optional[_StrategyKey], 

1239 wildcard_key: Optional[_WildcardKeyType], 

1240 opts: Optional[_OptsType] = None, 

1241 attr_group: Optional[_AttrGroupType] = None, 

1242 propagate_to_loaders: bool = True, 

1243 reconcile_to_other: Optional[bool] = None, 

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

1245 ) -> Self: 

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

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

1248 # InstanceState.load_options 

1249 if propagate_to_loaders: 

1250 self.propagate_to_loaders = True 

1251 

1252 if self.path.is_token: 

1253 raise sa_exc.ArgumentError( 

1254 "Wildcard token cannot be followed by another entity" 

1255 ) 

1256 

1257 elif path_is_property(self.path): 

1258 # reuse the lookup which will raise a nicely formatted 

1259 # LoaderStrategyException 

1260 if strategy: 

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

1262 else: 

1263 raise sa_exc.ArgumentError( 

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

1265 "refer to a mapped entity" 

1266 ) 

1267 

1268 if attrs is None: 

1269 load_element = _ClassStrategyLoad.create( 

1270 self.path, 

1271 None, 

1272 strategy, 

1273 wildcard_key, 

1274 opts, 

1275 propagate_to_loaders, 

1276 attr_group=attr_group, 

1277 reconcile_to_other=reconcile_to_other, 

1278 extra_criteria=extra_criteria, 

1279 ) 

1280 if load_element: 

1281 self.context += (load_element,) 

1282 assert opts is not None 

1283 self.additional_source_entities += cast( 

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

1285 ) 

1286 

1287 else: 

1288 for attr in attrs: 

1289 if isinstance(attr, str): 

1290 load_element = _TokenStrategyLoad.create( 

1291 self.path, 

1292 attr, 

1293 strategy, 

1294 wildcard_key, 

1295 opts, 

1296 propagate_to_loaders, 

1297 attr_group=attr_group, 

1298 reconcile_to_other=reconcile_to_other, 

1299 extra_criteria=extra_criteria, 

1300 ) 

1301 else: 

1302 load_element = _AttributeStrategyLoad.create( 

1303 self.path, 

1304 attr, 

1305 strategy, 

1306 wildcard_key, 

1307 opts, 

1308 propagate_to_loaders, 

1309 attr_group=attr_group, 

1310 reconcile_to_other=reconcile_to_other, 

1311 extra_criteria=extra_criteria, 

1312 ) 

1313 

1314 if load_element: 

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

1316 # object with the latest path. 

1317 if wildcard_key is _RELATIONSHIP_TOKEN: 

1318 self.path = load_element.path 

1319 self.context += (load_element,) 

1320 

1321 # this seems to be effective for selectinloader, 

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

1323 # but does not work for immediateloader, which still 

1324 # must add additional options at load time 

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

1326 r1 = load_element._recurse() 

1327 self.context += (r1,) 

1328 

1329 return self 

1330 

1331 def __getstate__(self): 

1332 d = self._shallow_to_dict() 

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

1334 return d 

1335 

1336 def __setstate__(self, state): 

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

1338 self._shallow_from_dict(state) 

1339 

1340 

1341class _WildcardLoad(_AbstractLoad): 

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

1343 

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

1345 

1346 _traverse_internals = [ 

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

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

1349 ( 

1350 "local_opts", 

1351 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1352 ), 

1353 ] 

1354 cache_key_traversal: _CacheKeyTraversalType = None 

1355 

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

1357 local_opts: _OptsType 

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

1359 propagate_to_loaders = False 

1360 

1361 def __init__(self) -> None: 

1362 self.path = () 

1363 self.strategy = None 

1364 self.local_opts = util.EMPTY_DICT 

1365 

1366 def _clone_for_bind_strategy( 

1367 self, 

1368 attrs, 

1369 strategy, 

1370 wildcard_key, 

1371 opts=None, 

1372 attr_group=None, 

1373 propagate_to_loaders=True, 

1374 reconcile_to_other=None, 

1375 extra_criteria=None, 

1376 ): 

1377 assert attrs is not None 

1378 attr = attrs[0] 

1379 assert ( 

1380 wildcard_key 

1381 and isinstance(attr, str) 

1382 and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN) 

1383 ) 

1384 

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

1386 

1387 self.strategy = strategy 

1388 self.path = (attr,) 

1389 if opts: 

1390 self.local_opts = util.immutabledict(opts) 

1391 

1392 assert extra_criteria is None 

1393 

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

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

1396 

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

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

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

1400 

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

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

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

1404 

1405 """ 

1406 assert self.path 

1407 attr = self.path[0] 

1408 if attr.endswith(_DEFAULT_TOKEN): 

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

1410 

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

1412 

1413 assert effective_path.is_token 

1414 

1415 loader = _TokenStrategyLoad.create( 

1416 effective_path, 

1417 None, 

1418 self.strategy, 

1419 None, 

1420 self.local_opts, 

1421 self.propagate_to_loaders, 

1422 ) 

1423 

1424 parent.context += (loader,) 

1425 

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

1427 is_refresh = compile_state.compile_options._for_refresh_state 

1428 

1429 if is_refresh and not self.propagate_to_loaders: 

1430 return 

1431 

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

1433 current_path = compile_state.current_path 

1434 

1435 start_path: _PathRepresentation = self.path 

1436 

1437 if current_path: 

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

1439 # None back here 

1440 new_path = self._chop_path(start_path, current_path) 

1441 if new_path is None: 

1442 return 

1443 

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

1445 # just returns it 

1446 assert new_path == start_path 

1447 

1448 # start_path is a single-token tuple 

1449 assert start_path and len(start_path) == 1 

1450 

1451 token = start_path[0] 

1452 assert isinstance(token, str) 

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

1454 

1455 if not entity: 

1456 return 

1457 

1458 path_element = entity 

1459 

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

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

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

1463 # tokens and populate into the Load(). 

1464 

1465 assert isinstance(token, str) 

1466 loader = _TokenStrategyLoad.create( 

1467 path_element._path_registry, 

1468 token, 

1469 self.strategy, 

1470 None, 

1471 self.local_opts, 

1472 self.propagate_to_loaders, 

1473 raiseerr=raiseerr, 

1474 ) 

1475 if not loader: 

1476 return 

1477 

1478 assert loader.path.is_token 

1479 

1480 # don't pass a reconciled lead entity here 

1481 loader.process_compile_state( 

1482 self, compile_state, mapper_entities, None, raiseerr 

1483 ) 

1484 

1485 return loader 

1486 

1487 def _find_entity_basestring( 

1488 self, 

1489 entities: Iterable[_InternalEntityType[Any]], 

1490 token: str, 

1491 raiseerr: bool, 

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

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

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

1495 if raiseerr: 

1496 raise sa_exc.ArgumentError( 

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

1498 f"loader option to multiple entities " 

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

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

1501 f"""{ 

1502 ", ".join( 

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

1504 for ent in entities 

1505 ) 

1506 }.""" 

1507 ) 

1508 elif token.endswith(_DEFAULT_TOKEN): 

1509 raiseerr = False 

1510 

1511 for ent in entities: 

1512 # return only the first _MapperEntity when searching 

1513 # based on string prop name. Ideally object 

1514 # attributes are used to specify more exactly. 

1515 return ent 

1516 else: 

1517 if raiseerr: 

1518 raise sa_exc.ArgumentError( 

1519 "Query has only expression-based entities - " 

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

1521 ) 

1522 else: 

1523 return None 

1524 

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

1526 d = self._shallow_to_dict() 

1527 return d 

1528 

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

1530 self._shallow_from_dict(state) 

1531 

1532 

1533class _LoadElement( 

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

1535): 

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

1537 and pass options to it. 

1538 

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

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

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

1542 

1543 .. versionadded:: 2.0 

1544 

1545 """ 

1546 

1547 __slots__ = ( 

1548 "path", 

1549 "strategy", 

1550 "propagate_to_loaders", 

1551 "local_opts", 

1552 "_extra_criteria", 

1553 "_reconcile_to_other", 

1554 ) 

1555 __visit_name__ = "load_element" 

1556 

1557 _traverse_internals = [ 

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

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

1560 ( 

1561 "local_opts", 

1562 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1563 ), 

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

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

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

1567 ] 

1568 _cache_key_traversal = None 

1569 

1570 _extra_criteria: Tuple[Any, ...] 

1571 

1572 _reconcile_to_other: Optional[bool] 

1573 strategy: Optional[_StrategyKey] 

1574 path: PathRegistry 

1575 propagate_to_loaders: bool 

1576 

1577 local_opts: util.immutabledict[str, Any] 

1578 

1579 is_token_strategy: bool 

1580 is_class_strategy: bool 

1581 

1582 def __hash__(self) -> int: 

1583 return id(self) 

1584 

1585 def __eq__(self, other): 

1586 return traversals.compare(self, other) 

1587 

1588 @property 

1589 def is_opts_only(self) -> bool: 

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

1591 

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

1593 cls = self.__class__ 

1594 s = cls.__new__(cls) 

1595 

1596 self._shallow_copy_to(s) 

1597 return s 

1598 

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

1600 new = self._clone() 

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

1602 return new 

1603 

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

1605 d = self._shallow_to_dict() 

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

1607 return d 

1608 

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

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

1611 self._shallow_from_dict(state) 

1612 

1613 def _raise_for_no_match(self, parent_loader, mapper_entities): 

1614 path = parent_loader.path 

1615 

1616 found_entities = False 

1617 for ent in mapper_entities: 

1618 ezero = ent.entity_zero 

1619 if ezero: 

1620 found_entities = True 

1621 break 

1622 

1623 if not found_entities: 

1624 raise sa_exc.ArgumentError( 

1625 "Query has only expression-based entities; " 

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

1627 "can't be applied here." 

1628 ) 

1629 else: 

1630 raise sa_exc.ArgumentError( 

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

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

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

1634 f"""{ 

1635 ", ".join( 

1636 entity_str(x.entity_zero) 

1637 for x in mapper_entities if x.entity_zero 

1638 )}. Please """ 

1639 "specify the full path " 

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

1641 "attribute. " 

1642 ) 

1643 

1644 def _adjust_effective_path_for_current_path( 

1645 self, effective_path: PathRegistry, current_path: PathRegistry 

1646 ) -> Optional[PathRegistry]: 

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

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

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

1650 current_path. 

1651 

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

1653 

1654 .. sourcecode:: text 

1655 

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

1657 

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

1659 

1660 The adjusted path would be: 

1661 

1662 .. sourcecode:: text 

1663 

1664 Item -> keywords -> Keyword 

1665 

1666 

1667 """ 

1668 chopped_start_path = Load._chop_path( 

1669 effective_path.natural_path, current_path 

1670 ) 

1671 if not chopped_start_path: 

1672 return None 

1673 

1674 tokens_removed_from_start_path = len(effective_path) - len( 

1675 chopped_start_path 

1676 ) 

1677 

1678 loader_lead_path_element = self.path[tokens_removed_from_start_path] 

1679 

1680 effective_path = PathRegistry.coerce( 

1681 (loader_lead_path_element,) + chopped_start_path[1:] 

1682 ) 

1683 

1684 return effective_path 

1685 

1686 def _init_path( 

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

1688 ): 

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

1690 a new path. 

1691 

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

1693 a :class:`._LoadElement` object. 

1694 

1695 """ 

1696 raise NotImplementedError() 

1697 

1698 def _prepare_for_compile_state( 

1699 self, 

1700 parent_loader, 

1701 compile_state, 

1702 mapper_entities, 

1703 reconciled_lead_entity, 

1704 raiseerr, 

1705 ): 

1706 """implemented by subclasses.""" 

1707 raise NotImplementedError() 

1708 

1709 def process_compile_state( 

1710 self, 

1711 parent_loader, 

1712 compile_state, 

1713 mapper_entities, 

1714 reconciled_lead_entity, 

1715 raiseerr, 

1716 ): 

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

1718 _LoadElement. 

1719 

1720 """ 

1721 keys = self._prepare_for_compile_state( 

1722 parent_loader, 

1723 compile_state, 

1724 mapper_entities, 

1725 reconciled_lead_entity, 

1726 raiseerr, 

1727 ) 

1728 for key in keys: 

1729 if key in compile_state.attributes: 

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

1731 self, compile_state.attributes[key] 

1732 ) 

1733 else: 

1734 compile_state.attributes[key] = self 

1735 

1736 @classmethod 

1737 def create( 

1738 cls, 

1739 path: PathRegistry, 

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

1741 strategy: Optional[_StrategyKey], 

1742 wildcard_key: Optional[_WildcardKeyType], 

1743 local_opts: Optional[_OptsType], 

1744 propagate_to_loaders: bool, 

1745 raiseerr: bool = True, 

1746 attr_group: Optional[_AttrGroupType] = None, 

1747 reconcile_to_other: Optional[bool] = None, 

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

1749 ) -> _LoadElement: 

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

1751 

1752 opt = cls.__new__(cls) 

1753 opt.path = path 

1754 opt.strategy = strategy 

1755 opt.propagate_to_loaders = propagate_to_loaders 

1756 opt.local_opts = ( 

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

1758 ) 

1759 opt._extra_criteria = () 

1760 

1761 if reconcile_to_other is not None: 

1762 opt._reconcile_to_other = reconcile_to_other 

1763 elif strategy is None and not local_opts: 

1764 opt._reconcile_to_other = True 

1765 else: 

1766 opt._reconcile_to_other = None 

1767 

1768 path = opt._init_path( 

1769 path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

1770 ) 

1771 

1772 if not path: 

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

1774 

1775 assert opt.is_token_strategy == path.is_token 

1776 

1777 opt.path = path 

1778 return opt 

1779 

1780 def __init__(self) -> None: 

1781 raise NotImplementedError() 

1782 

1783 def _recurse(self) -> _LoadElement: 

1784 cloned = self._clone() 

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

1786 

1787 return cloned 

1788 

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

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

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

1792 path. 

1793 

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

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

1796 

1797 """ 

1798 

1799 if not any( 

1800 orm_util._entity_corresponds_to_use_path_impl( 

1801 elem, 

1802 self.path.odd_element(0), 

1803 ) 

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

1805 + parent.additional_source_entities 

1806 ): 

1807 raise sa_exc.ArgumentError( 

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

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

1810 ) 

1811 

1812 return self._prepend_path(parent.path) 

1813 

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

1815 cloned = self._clone() 

1816 

1817 assert cloned.strategy == self.strategy 

1818 assert cloned.local_opts == self.local_opts 

1819 assert cloned.is_class_strategy == self.is_class_strategy 

1820 

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

1822 

1823 return cloned 

1824 

1825 @staticmethod 

1826 def _reconcile( 

1827 replacement: _LoadElement, existing: _LoadElement 

1828 ) -> _LoadElement: 

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

1830 the context.attributes under the same key. 

1831 

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

1833 existing one 

1834 

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

1836 

1837 """ 

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

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

1840 # see test_poly_loading.py 

1841 

1842 if replacement._reconcile_to_other: 

1843 return existing 

1844 elif replacement._reconcile_to_other is False: 

1845 return replacement 

1846 elif existing._reconcile_to_other: 

1847 return replacement 

1848 elif existing._reconcile_to_other is False: 

1849 return existing 

1850 

1851 if existing is replacement: 

1852 return replacement 

1853 elif ( 

1854 existing.strategy == replacement.strategy 

1855 and existing.local_opts == replacement.local_opts 

1856 ): 

1857 return replacement 

1858 elif replacement.is_opts_only: 

1859 existing = existing._clone() 

1860 existing.local_opts = existing.local_opts.union( 

1861 replacement.local_opts 

1862 ) 

1863 existing._extra_criteria += replacement._extra_criteria 

1864 return existing 

1865 elif existing.is_opts_only: 

1866 replacement = replacement._clone() 

1867 replacement.local_opts = replacement.local_opts.union( 

1868 existing.local_opts 

1869 ) 

1870 replacement._extra_criteria += existing._extra_criteria 

1871 return replacement 

1872 elif replacement.path.is_token: 

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

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

1875 # will raise as below 

1876 return replacement 

1877 

1878 raise sa_exc.InvalidRequestError( 

1879 f"Loader strategy replacement " 

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

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

1882 ) 

1883 

1884 def _to_option_method_string(self) -> str: 

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

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

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

1888 

1889 """ 

1890 assert ( 

1891 self.strategy is not None 

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

1893 

1894 for opt_key in self.local_opts: 

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

1896 if fn: 

1897 break 

1898 else: 

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

1900 

1901 assert ( 

1902 fn is not None 

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

1904 

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

1906 

1907 

1908class _AttributeStrategyLoad(_LoadElement): 

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

1910 

1911 e.g.:: 

1912 

1913 joinedload(User.addresses) 

1914 defer(Order.name) 

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

1916 

1917 """ 

1918 

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

1920 

1921 __visit_name__ = "attribute_strategy_load_element" 

1922 

1923 _traverse_internals = _LoadElement._traverse_internals + [ 

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

1925 ( 

1926 "_path_with_polymorphic_path", 

1927 visitors.ExtendedInternalTraversal.dp_has_cache_key, 

1928 ), 

1929 ] 

1930 

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

1932 _path_with_polymorphic_path: Optional[PathRegistry] 

1933 

1934 is_class_strategy = False 

1935 is_token_strategy = False 

1936 

1937 def _init_path( 

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

1939 ): 

1940 assert attr is not None 

1941 self._of_type = None 

1942 self._path_with_polymorphic_path = None 

1943 insp, _, prop = _parse_attr_argument(attr) 

1944 

1945 if insp.is_property: 

1946 # direct property can be sent from internal strategy logic 

1947 # that sets up specific loaders, such as 

1948 # emit_lazyload->_lazyload_reverse 

1949 # prop = found_property = attr 

1950 prop = attr 

1951 path = path[prop] 

1952 

1953 if path.has_entity: 

1954 path = path.entity_path 

1955 return path 

1956 

1957 elif not insp.is_attribute: 

1958 # should not reach here; 

1959 assert False 

1960 

1961 # here we assume we have user-passed InstrumentedAttribute 

1962 if not orm_util._entity_corresponds_to_use_path_impl( 

1963 path[-1], attr.parent 

1964 ): 

1965 if raiseerr: 

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

1967 raise sa_exc.ArgumentError( 

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

1969 "loader option to multiple entities in the " 

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

1971 ) 

1972 else: 

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

1974 else: 

1975 return None 

1976 

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

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

1979 # test_relationship_criteria.py::RelationshipCriteriaTest:: 

1980 # test_selectinload_nested_criteria[True] if an existing 

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

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

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

1984 # poorly defined. 

1985 if extra_criteria: 

1986 assert not attr._extra_criteria 

1987 self._extra_criteria = extra_criteria 

1988 else: 

1989 self._extra_criteria = attr._extra_criteria 

1990 

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

1992 ac = attr._of_type 

1993 ext_info = inspect(ac) 

1994 self._of_type = ext_info 

1995 

1996 self._path_with_polymorphic_path = path.entity_path[prop] 

1997 

1998 path = path[prop][ext_info] 

1999 

2000 else: 

2001 path = path[prop] 

2002 

2003 if path.has_entity: 

2004 path = path.entity_path 

2005 

2006 return path 

2007 

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

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

2010 

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

2012 ensures that the polymorphic path information is correctly updated 

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

2014 """ 

2015 cloned = super()._prepend_path(path) 

2016 

2017 # Also prepend the parent path to _path_with_polymorphic_path if 

2018 # present 

2019 if self._path_with_polymorphic_path is not None: 

2020 cloned._path_with_polymorphic_path = PathRegistry.coerce( 

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

2022 ) 

2023 

2024 return cloned 

2025 

2026 def _generate_extra_criteria(self, context): 

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

2028 immediate "extra_criteria" stored with this Load object. 

2029 

2030 Load objects are typically pulled from the cached version of 

2031 the statement from a QueryContext. The statement currently being 

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

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

2034 they handle this criteria for a result set. 

2035 

2036 """ 

2037 

2038 assert ( 

2039 self._extra_criteria 

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

2041 

2042 orig_query = context.compile_state.select_statement 

2043 current_query = context.query 

2044 

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

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

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

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

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

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

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

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

2053 

2054 # if orig_query is current_query: 

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

2056 # return and_(*self._extra_criteria) 

2057 

2058 k1 = orig_query._generate_cache_key() 

2059 k2 = current_query._generate_cache_key() 

2060 

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

2062 

2063 def _set_of_type_info(self, context, current_path): 

2064 assert self._path_with_polymorphic_path 

2065 

2066 pwpi = self._of_type 

2067 assert pwpi 

2068 if not pwpi.is_aliased_class: 

2069 pwpi = inspect( 

2070 orm_util.AliasedInsp._with_polymorphic_factory( 

2071 pwpi.mapper.base_mapper, 

2072 (pwpi.mapper,), 

2073 aliased=True, 

2074 _use_mapper_path=True, 

2075 ) 

2076 ) 

2077 start_path = self._path_with_polymorphic_path 

2078 if current_path: 

2079 new_path = self._adjust_effective_path_for_current_path( 

2080 start_path, current_path 

2081 ) 

2082 if new_path is None: 

2083 return 

2084 start_path = new_path 

2085 

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

2087 if key in context: 

2088 existing_aliased_insp = context[key] 

2089 this_aliased_insp = pwpi 

2090 new_aliased_insp = existing_aliased_insp._merge_with( 

2091 this_aliased_insp 

2092 ) 

2093 context[key] = new_aliased_insp 

2094 else: 

2095 context[key] = pwpi 

2096 

2097 def _prepare_for_compile_state( 

2098 self, 

2099 parent_loader, 

2100 compile_state, 

2101 mapper_entities, 

2102 reconciled_lead_entity, 

2103 raiseerr, 

2104 ): 

2105 # _AttributeStrategyLoad 

2106 

2107 current_path = compile_state.current_path 

2108 is_refresh = compile_state.compile_options._for_refresh_state 

2109 assert not self.path.is_token 

2110 

2111 if is_refresh and not self.propagate_to_loaders: 

2112 return [] 

2113 

2114 if self._of_type: 

2115 # apply additional with_polymorphic alias that may have been 

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

2117 self._set_of_type_info(compile_state.attributes, current_path) 

2118 

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

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

2121 return [] 

2122 

2123 if raiseerr and not reconciled_lead_entity: 

2124 self._raise_for_no_match(parent_loader, mapper_entities) 

2125 

2126 if self.path.has_entity: 

2127 effective_path = self.path.parent 

2128 else: 

2129 effective_path = self.path 

2130 

2131 if current_path: 

2132 assert effective_path is not None 

2133 effective_path = self._adjust_effective_path_for_current_path( 

2134 effective_path, current_path 

2135 ) 

2136 if effective_path is None: 

2137 return [] 

2138 

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

2140 

2141 def __getstate__(self): 

2142 d = super().__getstate__() 

2143 

2144 # can't pickle this. See 

2145 # test_pickled.py -> test_lazyload_extra_criteria_not_supported 

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

2147 # would be non-None 

2148 d["_extra_criteria"] = () 

2149 

2150 if self._path_with_polymorphic_path: 

2151 d["_path_with_polymorphic_path"] = ( 

2152 self._path_with_polymorphic_path.serialize() 

2153 ) 

2154 

2155 if self._of_type: 

2156 if self._of_type.is_aliased_class: 

2157 d["_of_type"] = None 

2158 elif self._of_type.is_mapper: 

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

2160 else: 

2161 assert False, "unexpected object for _of_type" 

2162 

2163 return d 

2164 

2165 def __setstate__(self, state): 

2166 super().__setstate__(state) 

2167 

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

2169 self._path_with_polymorphic_path = PathRegistry.deserialize( 

2170 state["_path_with_polymorphic_path"] 

2171 ) 

2172 else: 

2173 self._path_with_polymorphic_path = None 

2174 

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

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

2177 else: 

2178 self._of_type = None 

2179 

2180 

2181class _TokenStrategyLoad(_LoadElement): 

2182 """Loader strategies against wildcard attributes 

2183 

2184 e.g.:: 

2185 

2186 raiseload("*") 

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

2188 defer("*") 

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

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

2191 

2192 """ 

2193 

2194 __visit_name__ = "token_strategy_load_element" 

2195 

2196 inherit_cache = True 

2197 is_class_strategy = False 

2198 is_token_strategy = True 

2199 

2200 def _init_path( 

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

2202 ): 

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

2204 if attr is not None: 

2205 default_token = attr.endswith(_DEFAULT_TOKEN) 

2206 if attr.endswith(_WILDCARD_TOKEN) or default_token: 

2207 if wildcard_key: 

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

2209 

2210 path = path.token(attr) 

2211 return path 

2212 else: 

2213 raise sa_exc.ArgumentError( 

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

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

2216 ) 

2217 return path 

2218 

2219 def _prepare_for_compile_state( 

2220 self, 

2221 parent_loader, 

2222 compile_state, 

2223 mapper_entities, 

2224 reconciled_lead_entity, 

2225 raiseerr, 

2226 ): 

2227 # _TokenStrategyLoad 

2228 

2229 current_path = compile_state.current_path 

2230 is_refresh = compile_state.compile_options._for_refresh_state 

2231 

2232 assert self.path.is_token 

2233 

2234 if is_refresh and not self.propagate_to_loaders: 

2235 return [] 

2236 

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

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

2239 return [] 

2240 

2241 effective_path = self.path 

2242 if reconciled_lead_entity: 

2243 effective_path = PathRegistry.coerce( 

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

2245 ) 

2246 

2247 if current_path: 

2248 new_effective_path = self._adjust_effective_path_for_current_path( 

2249 effective_path, current_path 

2250 ) 

2251 if new_effective_path is None: 

2252 return [] 

2253 effective_path = new_effective_path 

2254 

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

2256 # to encompass everything from the query entity on 

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

2258 # is set. 

2259 

2260 return [ 

2261 ("loader", natural_path) 

2262 for natural_path in ( 

2263 cast( 

2264 _TokenRegistry, effective_path 

2265 )._generate_natural_for_superclasses() 

2266 ) 

2267 ] 

2268 

2269 

2270class _ClassStrategyLoad(_LoadElement): 

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

2272 an attribute path 

2273 

2274 e.g.:: 

2275 

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

2277 selectin_polymorphic(Person, [Engineer, Manager]) 

2278 ) 

2279 

2280 """ 

2281 

2282 inherit_cache = True 

2283 is_class_strategy = True 

2284 is_token_strategy = False 

2285 

2286 __visit_name__ = "class_strategy_load_element" 

2287 

2288 def _init_path( 

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

2290 ): 

2291 return path 

2292 

2293 def _prepare_for_compile_state( 

2294 self, 

2295 parent_loader, 

2296 compile_state, 

2297 mapper_entities, 

2298 reconciled_lead_entity, 

2299 raiseerr, 

2300 ): 

2301 # _ClassStrategyLoad 

2302 

2303 current_path = compile_state.current_path 

2304 is_refresh = compile_state.compile_options._for_refresh_state 

2305 

2306 if is_refresh and not self.propagate_to_loaders: 

2307 return [] 

2308 

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

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

2311 return [] 

2312 

2313 effective_path = self.path 

2314 

2315 if current_path: 

2316 new_effective_path = self._adjust_effective_path_for_current_path( 

2317 effective_path, current_path 

2318 ) 

2319 if new_effective_path is None: 

2320 return [] 

2321 effective_path = new_effective_path 

2322 

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

2324 

2325 

2326def _generate_from_keys( 

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

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

2329 chained: bool, 

2330 kw: Any, 

2331) -> _AbstractLoad: 

2332 lead_element: Optional[_AbstractLoad] = None 

2333 

2334 attr: Any 

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

2336 for attr in _keys: 

2337 if isinstance(attr, str): 

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

2339 util.warn_deprecated( 

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

2341 "deprecated " 

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

2343 "it is " 

2344 "believed to be unused. " 

2345 "If you have been using this functionality, " 

2346 "please " 

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

2348 "tracker.", 

2349 version="1.4", 

2350 ) 

2351 attr = attr[1:] 

2352 

2353 if attr == _WILDCARD_TOKEN: 

2354 if is_default: 

2355 raise sa_exc.ArgumentError( 

2356 "Wildcard token cannot be followed by " 

2357 "another entity", 

2358 ) 

2359 

2360 if lead_element is None: 

2361 lead_element = _WildcardLoad() 

2362 

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

2364 

2365 else: 

2366 raise sa_exc.ArgumentError( 

2367 "Strings are not accepted for attribute names in " 

2368 "loader options; please use class-bound " 

2369 "attributes directly.", 

2370 ) 

2371 else: 

2372 if lead_element is None: 

2373 _, lead_entity, _ = _parse_attr_argument(attr) 

2374 lead_element = Load(lead_entity) 

2375 

2376 if is_default: 

2377 if not chained: 

2378 lead_element = lead_element.defaultload(attr) 

2379 else: 

2380 lead_element = meth( 

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

2382 ) 

2383 else: 

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

2385 

2386 assert lead_element 

2387 return lead_element 

2388 

2389 

2390def _parse_attr_argument( 

2391 attr: _AttrType, 

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

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

2394 :class:`._AbstractLoad` instance. 

2395 

2396 This is used by the standalone loader strategy functions like 

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

2398 :class:`._WildcardLoad` objects. 

2399 

2400 """ 

2401 try: 

2402 # TODO: need to figure out this None thing being returned by 

2403 # inspect(), it should not have None as an option in most cases 

2404 # if at all 

2405 insp: InspectionAttr = inspect(attr) # type: ignore[assignment] 

2406 except sa_exc.NoInspectionAvailable as err: 

2407 raise sa_exc.ArgumentError( 

2408 "expected ORM mapped attribute for loader strategy argument" 

2409 ) from err 

2410 

2411 lead_entity: _InternalEntityType[Any] 

2412 

2413 if insp_is_mapper_property(insp): 

2414 lead_entity = insp.parent 

2415 prop = insp 

2416 elif insp_is_attribute(insp): 

2417 lead_entity = insp.parent 

2418 prop = insp.prop 

2419 else: 

2420 raise sa_exc.ArgumentError( 

2421 "expected ORM mapped attribute for loader strategy argument" 

2422 ) 

2423 

2424 return insp, lead_entity, prop 

2425 

2426 

2427def _strategy_labels( 

2428 *strategy_keys: "Any", 

2429 discriminating_opt: Optional[str] = None, 

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

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

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

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

2434 runs. 

2435 

2436 :param discriminating_opt: optional local_opts key that distinguishes 

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

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

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

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

2441 """ 

2442 

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

2444 for key in strategy_keys: 

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

2446 return fn 

2447 

2448 return decorator 

2449 

2450 

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

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

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

2454 

2455 """ 

2456 bound_fn = getattr(_AbstractLoad, fn.__name__) 

2457 fn_doc = bound_fn.__doc__ 

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

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

2460 

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

2462 

2463""" 

2464 

2465 fn.__doc__ = fn_doc 

2466 return fn 

2467 

2468 

2469def _expand_column_strategy_attrs( 

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

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

2472 return cast( 

2473 "Tuple[_AttrType, ...]", 

2474 tuple( 

2475 a 

2476 for attr in attrs 

2477 for a in ( 

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

2479 if hasattr(attr, "_column_strategy_attrs") 

2480 else (attr,) 

2481 ) 

2482 ), 

2483 ) 

2484 

2485 

2486# standalone functions follow. docstrings are filled in 

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

2488 

2489 

2490@loader_unbound_fn 

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

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

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

2494 

2495 

2496@loader_unbound_fn 

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

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

2499 # add some extra state to Load of some kind 

2500 attrs = _expand_column_strategy_attrs(attrs) 

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

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

2503 

2504 

2505@loader_unbound_fn 

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

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

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

2509 

2510 

2511@loader_unbound_fn 

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

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

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

2515 

2516 

2517@loader_unbound_fn 

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

2519def selectinload( 

2520 *keys: _AttrType, 

2521 recursion_depth: Optional[int] = None, 

2522 chunksize: Optional[int] = None, 

2523) -> _AbstractLoad: 

2524 return _generate_from_keys( 

2525 Load.selectinload, 

2526 keys, 

2527 False, 

2528 {"recursion_depth": recursion_depth, "chunksize": chunksize}, 

2529 ) 

2530 

2531 

2532@loader_unbound_fn 

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

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

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

2536 

2537 

2538@loader_unbound_fn 

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

2540def immediateload( 

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

2542) -> _AbstractLoad: 

2543 return _generate_from_keys( 

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

2545 ) 

2546 

2547 

2548@loader_unbound_fn 

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

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

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

2552 

2553 

2554@loader_unbound_fn 

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

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

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

2558 

2559 

2560@loader_unbound_fn 

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

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

2563 

2564 

2565@loader_unbound_fn 

2566@_strategy_labels( 

2567 (("deferred", True), ("instrument", True)), 

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

2569) 

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

2571 if raiseload: 

2572 kw = {"raiseload": raiseload} 

2573 else: 

2574 kw = {} 

2575 

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

2577 

2578 

2579@loader_unbound_fn 

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

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

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

2583 

2584 

2585@loader_unbound_fn 

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

2587 element = _WildcardLoad() 

2588 return element.undefer_group(name) 

2589 

2590 

2591@loader_unbound_fn 

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

2593def with_expression( 

2594 key: _AttrType, expression: _ColumnExpressionArgument[Any] 

2595) -> _AbstractLoad: 

2596 return _generate_from_keys( 

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

2598 ) 

2599 

2600 

2601@loader_unbound_fn 

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

2603def selectin_polymorphic( 

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

2605) -> _AbstractLoad: 

2606 ul = Load(base_cls) 

2607 return ul.selectin_polymorphic(classes) 

2608 

2609 

2610def _raise_for_does_not_link(path, attrname, parent_entity): 

2611 if len(path) > 1: 

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

2613 

2614 raise sa_exc.ArgumentError( 

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

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

2617 % ( 

2618 ( 

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

2620 if path_is_of_type 

2621 else "" 

2622 ), 

2623 ( 

2624 " Did you mean to use " 

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

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

2627 '"loadopt.options(' 

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

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

2630 if not path_is_of_type 

2631 and not path[-1].is_aliased_class 

2632 and orm_util._entity_corresponds_to( 

2633 path.entity, inspect(parent_entity).mapper 

2634 ) 

2635 else "" 

2636 ), 

2637 ) 

2638 ) 

2639 else: 

2640 raise sa_exc.ArgumentError( 

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

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

2643 )