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

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

783 statements  

1# orm/strategy_options.py 

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

3# <see AUTHORS file> 

4# 

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

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

7# mypy: 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 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_aliased_class 

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 InspectionAttr 

35from .interfaces import LoaderOption 

36from .path_registry import _AbstractEntityRegistry 

37from .path_registry import _DEFAULT_TOKEN 

38from .path_registry import _StrPathToken 

39from .path_registry import _TokenRegistry 

40from .path_registry import _WILDCARD_TOKEN 

41from .path_registry import path_is_property 

42from .path_registry import PathRegistry 

43from .util import _orm_full_deannotate 

44from .util import AliasedInsp 

45from .. import exc as sa_exc 

46from .. import inspect 

47from .. import util 

48from ..sql import and_ 

49from ..sql import cache_key 

50from ..sql import coercions 

51from ..sql import roles 

52from ..sql import traversals 

53from ..sql import visitors 

54from ..sql.base import _generative 

55from ..util.typing import Literal 

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 

88class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption): 

89 __slots__ = ("propagate_to_loaders",) 

90 

91 _is_strategy_option = True 

92 propagate_to_loaders: bool 

93 

94 def contains_eager( 

95 self, 

96 attr: _AttrType, 

97 alias: Optional[_FromClauseArgument] = None, 

98 _is_chain: bool = False, 

99 _propagate_to_loaders: bool = False, 

100 ) -> Self: 

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

102 columns stated manually in the query. 

103 

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

105 both method-chained and standalone operation. 

106 

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

108 the desired rows, i.e.:: 

109 

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

111 

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

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

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

115 

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

117 collection; queries will normally want to use the 

118 :ref:`orm_queryguide_populate_existing` execution option assuming the 

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

120 

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

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

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

124 

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

126 

127 .. seealso:: 

128 

129 :ref:`loading_toplevel` 

130 

131 :ref:`contains_eager` 

132 

133 """ 

134 if alias is not None: 

135 if not isinstance(alias, str): 

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

137 else: 

138 util.warn_deprecated( 

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

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

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

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

143 version="1.4", 

144 ) 

145 coerced_alias = alias 

146 

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

148 assert isinstance(attr, QueryableAttribute) 

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

150 assert ot is not None 

151 coerced_alias = ot.selectable 

152 else: 

153 coerced_alias = None 

154 

155 cloned = self._set_relationship_strategy( 

156 attr, 

157 {"lazy": "joined"}, 

158 propagate_to_loaders=_propagate_to_loaders, 

159 opts={"eager_from_alias": coerced_alias}, 

160 _reconcile_to_other=True if _is_chain else None, 

161 ) 

162 return cloned 

163 

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

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

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

167 deferred. 

168 

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

170 both method-chained and standalone operation. 

171 

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

173 ``fullname`` attributes:: 

174 

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

176 

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

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

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

180 

181 session.query(User).options( 

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

183 ) 

184 

185 For a statement that has multiple entities, 

186 the lead entity can be 

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

188 

189 stmt = ( 

190 select(User, Address) 

191 .join(User.addresses) 

192 .options( 

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

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

195 ) 

196 ) 

197 

198 When used together with the 

199 :ref:`populate_existing <orm_queryguide_populate_existing>` 

200 execution option only the attributes listed will be refreshed. 

201 

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

203 

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

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

206 to prevent unwanted SQL from being emitted. 

207 

208 .. versionadded:: 2.0 

209 

210 .. seealso:: 

211 

212 :ref:`orm_queryguide_column_deferral` - in the 

213 :ref:`queryguide_toplevel` 

214 

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

216 

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

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

219 to prevent unwanted SQL from being emitted. 

220 

221 .. versionadded:: 2.0 

222 

223 """ 

224 cloned = self._set_column_strategy( 

225 _expand_column_strategy_attrs(attrs), 

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

227 ) 

228 

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

230 if raiseload: 

231 wildcard_strategy["raiseload"] = True 

232 

233 cloned = cloned._set_column_strategy( 

234 ("*",), 

235 wildcard_strategy, 

236 ) 

237 return cloned 

238 

239 def joinedload( 

240 self, 

241 attr: _AttrType, 

242 innerjoin: Optional[bool] = None, 

243 ) -> Self: 

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

245 eager loading. 

246 

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

248 both method-chained and standalone operation. 

249 

250 examples:: 

251 

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

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

254 

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

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

257 

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

259 # joined-load the keywords collection 

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

261 

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

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

264 

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

266 

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

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

269 

270 select(A).options( 

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

272 ) 

273 

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

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

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

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

278 directly supported. 

279 

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

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

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

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

284 is an outerjoin:: 

285 

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

287 

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

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

290 

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

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

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

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

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

296 

297 .. note:: 

298 

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

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

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

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

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

304 

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

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

307 explicit JOINs with eager loading of collections, use 

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

309 

310 .. seealso:: 

311 

312 :ref:`loading_toplevel` 

313 

314 :ref:`joined_eager_loading` 

315 

316 """ # noqa: E501 

317 loader = self._set_relationship_strategy( 

318 attr, 

319 {"lazy": "joined"}, 

320 opts=( 

321 {"innerjoin": innerjoin} 

322 if innerjoin is not None 

323 else util.EMPTY_DICT 

324 ), 

325 ) 

326 return loader 

327 

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

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

330 subquery eager loading. 

331 

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

333 both method-chained and standalone operation. 

334 

335 examples:: 

336 

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

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

339 

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

341 select(Order).options( 

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

343 ) 

344 

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

346 # subquery-load the keywords collection 

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

348 

349 .. seealso:: 

350 

351 :ref:`loading_toplevel` 

352 

353 :ref:`subquery_eager_loading` 

354 

355 """ 

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

357 

358 def selectinload( 

359 self, 

360 attr: _AttrType, 

361 recursion_depth: Optional[int] = None, 

362 ) -> Self: 

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

364 SELECT IN eager loading. 

365 

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

367 both method-chained and standalone operation. 

368 

369 examples:: 

370 

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

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

373 

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

375 select(Order).options( 

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

377 ) 

378 

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

380 # selectin-load the keywords collection 

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

382 

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

384 in conjunction with a self-referential relationship, 

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

386 automatically until no items are found. 

387 

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

389 currently supports only self-referential relationships. There 

390 is not yet an option to automatically traverse recursive structures 

391 with more than one relationship involved. 

392 

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

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

395 status for the 2.0 series. 

396 

397 .. versionadded:: 2.0 added 

398 :paramref:`_orm.selectinload.recursion_depth` 

399 

400 

401 .. seealso:: 

402 

403 :ref:`loading_toplevel` 

404 

405 :ref:`selectin_eager_loading` 

406 

407 """ 

408 return self._set_relationship_strategy( 

409 attr, 

410 {"lazy": "selectin"}, 

411 opts={"recursion_depth": recursion_depth}, 

412 ) 

413 

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

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

416 loading. 

417 

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

419 both method-chained and standalone operation. 

420 

421 .. seealso:: 

422 

423 :ref:`loading_toplevel` 

424 

425 :ref:`lazy_loading` 

426 

427 """ 

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

429 

430 def immediateload( 

431 self, 

432 attr: _AttrType, 

433 recursion_depth: Optional[int] = None, 

434 ) -> Self: 

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

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

437 

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

439 fire off any additional eager loaders. 

440 

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

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

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

444 

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

446 both method-chained and standalone operation. 

447 

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

449 in conjunction with a self-referential relationship, 

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

451 automatically until no items are found. 

452 

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

454 currently supports only self-referential relationships. There 

455 is not yet an option to automatically traverse recursive structures 

456 with more than one relationship involved. 

457 

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

459 treated as "alpha" status 

460 

461 .. versionadded:: 2.0 added 

462 :paramref:`_orm.immediateload.recursion_depth` 

463 

464 

465 .. seealso:: 

466 

467 :ref:`loading_toplevel` 

468 

469 :ref:`selectin_eager_loading` 

470 

471 """ 

472 loader = self._set_relationship_strategy( 

473 attr, 

474 {"lazy": "immediate"}, 

475 opts={"recursion_depth": recursion_depth}, 

476 ) 

477 return loader 

478 

479 @util.deprecated( 

480 "2.1", 

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

482 "in a future release. This option " 

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

484 "items.", 

485 ) 

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

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

488 unloaded. 

489 

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

491 producing any loading effect. 

492 

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

494 only. 

495 

496 .. seealso:: 

497 

498 :ref:`loading_toplevel` 

499 

500 """ 

501 

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

503 

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

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

506 

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

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

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

510 ensure that all relationship attributes that are accessed in a 

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

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

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

514 

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

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

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

518 :func:`.defer` loader option. 

519 

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

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

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

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

524 

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

526 both method-chained and standalone operation. 

527 

528 .. seealso:: 

529 

530 :ref:`loading_toplevel` 

531 

532 :ref:`prevent_lazy_with_raiseload` 

533 

534 :ref:`orm_queryguide_deferred_raiseload` 

535 

536 """ 

537 

538 return self._set_relationship_strategy( 

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

540 ) 

541 

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

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

544 

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

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

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

548 loading will be used. 

549 

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

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

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

553 element of an element:: 

554 

555 session.query(MyClass).options( 

556 defaultload(MyClass.someattribute).joinedload( 

557 MyOtherClass.someotherattribute 

558 ) 

559 ) 

560 

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

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

563 

564 session.scalars( 

565 select(MyClass).options( 

566 defaultload(MyClass.someattribute) 

567 .defer("some_column") 

568 .undefer("some_other_column") 

569 ) 

570 ) 

571 

572 .. seealso:: 

573 

574 :ref:`orm_queryguide_relationship_sub_options` 

575 

576 :meth:`_orm.Load.options` 

577 

578 """ 

579 return self._set_relationship_strategy(attr, None) 

580 

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

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

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

584 

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

586 both method-chained and standalone operation. 

587 

588 e.g.:: 

589 

590 from sqlalchemy.orm import defer 

591 

592 session.query(MyClass).options( 

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

594 ) 

595 

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

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

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

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

600 

601 session.query(MyClass).options( 

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

603 ) 

604 

605 Multiple deferral options related to a relationship can be bundled 

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

607 

608 

609 select(MyClass).options( 

610 defaultload(MyClass.someattr).options( 

611 defer(RelatedClass.some_column), 

612 defer(RelatedClass.some_other_column), 

613 defer(RelatedClass.another_column), 

614 ) 

615 ) 

616 

617 :param key: Attribute to be deferred. 

618 

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

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

621 to prevent unwanted SQL from being emitted. 

622 

623 .. versionadded:: 1.4 

624 

625 .. seealso:: 

626 

627 :ref:`orm_queryguide_column_deferral` - in the 

628 :ref:`queryguide_toplevel` 

629 

630 :func:`_orm.load_only` 

631 

632 :func:`_orm.undefer` 

633 

634 """ 

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

636 if raiseload: 

637 strategy["raiseload"] = True 

638 return self._set_column_strategy( 

639 _expand_column_strategy_attrs((key,)), strategy 

640 ) 

641 

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

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

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

645 as a whole. 

646 

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

648 :func:`.deferred` attribute. 

649 

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

651 both method-chained and standalone operation. 

652 

653 Examples:: 

654 

655 # undefer two columns 

656 session.query(MyClass).options( 

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

658 ) 

659 

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

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

662 

663 # undefer a column on a related object 

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

665 

666 :param key: Attribute to be undeferred. 

667 

668 .. seealso:: 

669 

670 :ref:`orm_queryguide_column_deferral` - in the 

671 :ref:`queryguide_toplevel` 

672 

673 :func:`_orm.defer` 

674 

675 :func:`_orm.undefer_group` 

676 

677 """ # noqa: E501 

678 return self._set_column_strategy( 

679 _expand_column_strategy_attrs((key,)), 

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

681 ) 

682 

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

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

685 undeferred. 

686 

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

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

689 

690 E.g:: 

691 

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

693 

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

695 spelled out using relationship loader options, such as 

696 :func:`_orm.defaultload`:: 

697 

698 select(MyClass).options( 

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

700 ) 

701 

702 .. seealso:: 

703 

704 :ref:`orm_queryguide_column_deferral` - in the 

705 :ref:`queryguide_toplevel` 

706 

707 :func:`_orm.defer` 

708 

709 :func:`_orm.undefer` 

710 

711 """ 

712 return self._set_column_strategy( 

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

714 ) 

715 

716 def with_expression( 

717 self, 

718 key: _AttrType, 

719 expression: _ColumnExpressionArgument[Any], 

720 ) -> Self: 

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

722 attribute. 

723 

724 This option is used in conjunction with the 

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

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

727 

728 E.g.:: 

729 

730 stmt = select(SomeClass).options( 

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

732 ) 

733 

734 :param key: Attribute to be populated 

735 

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

737 

738 .. seealso:: 

739 

740 :ref:`orm_queryguide_with_expression` - background and usage 

741 examples 

742 

743 """ 

744 

745 expression = _orm_full_deannotate( 

746 coercions.expect(roles.LabeledColumnExprRole, expression) 

747 ) 

748 

749 return self._set_column_strategy( 

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

751 ) 

752 

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

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

755 specific to a subclass. 

756 

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

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

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

760 

761 .. seealso:: 

762 

763 :ref:`polymorphic_selectin` 

764 

765 """ 

766 self = self._set_class_strategy( 

767 {"selectinload_polymorphic": True}, 

768 opts={ 

769 "entities": tuple( 

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

771 ) 

772 }, 

773 ) 

774 return self 

775 

776 @overload 

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

778 

779 @overload 

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

781 

782 def _coerce_strat( 

783 self, strategy: Optional[_StrategySpec] 

784 ) -> Optional[_StrategyKey]: 

785 if strategy is not None: 

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

787 else: 

788 strategy_key = None 

789 return strategy_key 

790 

791 @_generative 

792 def _set_relationship_strategy( 

793 self, 

794 attr: _AttrType, 

795 strategy: Optional[_StrategySpec], 

796 propagate_to_loaders: bool = True, 

797 opts: Optional[_OptsType] = None, 

798 _reconcile_to_other: Optional[bool] = None, 

799 ) -> Self: 

800 strategy_key = self._coerce_strat(strategy) 

801 

802 self._clone_for_bind_strategy( 

803 (attr,), 

804 strategy_key, 

805 _RELATIONSHIP_TOKEN, 

806 opts=opts, 

807 propagate_to_loaders=propagate_to_loaders, 

808 reconcile_to_other=_reconcile_to_other, 

809 ) 

810 return self 

811 

812 @_generative 

813 def _set_column_strategy( 

814 self, 

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

816 strategy: Optional[_StrategySpec], 

817 opts: Optional[_OptsType] = None, 

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

819 ) -> Self: 

820 strategy_key = self._coerce_strat(strategy) 

821 

822 self._clone_for_bind_strategy( 

823 attrs, 

824 strategy_key, 

825 _COLUMN_TOKEN, 

826 opts=opts, 

827 attr_group=attrs, 

828 extra_criteria=extra_criteria, 

829 ) 

830 return self 

831 

832 @_generative 

833 def _set_generic_strategy( 

834 self, 

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

836 strategy: _StrategySpec, 

837 _reconcile_to_other: Optional[bool] = None, 

838 ) -> Self: 

839 strategy_key = self._coerce_strat(strategy) 

840 self._clone_for_bind_strategy( 

841 attrs, 

842 strategy_key, 

843 None, 

844 propagate_to_loaders=True, 

845 reconcile_to_other=_reconcile_to_other, 

846 ) 

847 return self 

848 

849 @_generative 

850 def _set_class_strategy( 

851 self, strategy: _StrategySpec, opts: _OptsType 

852 ) -> Self: 

853 strategy_key = self._coerce_strat(strategy) 

854 

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

856 return self 

857 

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

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

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

861 

862 Implementation is provided by subclasses. 

863 

864 """ 

865 raise NotImplementedError() 

866 

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

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

869 :class:`_orm._AbstractLoad` object. 

870 

871 Implementation is provided by subclasses. 

872 

873 """ 

874 raise NotImplementedError() 

875 

876 def _clone_for_bind_strategy( 

877 self, 

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

879 strategy: Optional[_StrategyKey], 

880 wildcard_key: Optional[_WildcardKeyType], 

881 opts: Optional[_OptsType] = None, 

882 attr_group: Optional[_AttrGroupType] = None, 

883 propagate_to_loaders: bool = True, 

884 reconcile_to_other: Optional[bool] = None, 

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

886 ) -> Self: 

887 raise NotImplementedError() 

888 

889 def process_compile_state_replaced_entities( 

890 self, 

891 compile_state: _ORMCompileState, 

892 mapper_entities: Sequence[_MapperEntity], 

893 ) -> None: 

894 if not compile_state.compile_options._enable_eagerloads: 

895 return 

896 

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

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

899 # for the entities having been replaced with equivalents 

900 self._process( 

901 compile_state, 

902 mapper_entities, 

903 not bool(compile_state.current_path), 

904 ) 

905 

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

907 if not compile_state.compile_options._enable_eagerloads: 

908 return 

909 

910 self._process( 

911 compile_state, 

912 compile_state._lead_mapper_entities, 

913 not bool(compile_state.current_path) 

914 and not compile_state.compile_options._for_refresh_state, 

915 ) 

916 

917 def _process( 

918 self, 

919 compile_state: _ORMCompileState, 

920 mapper_entities: Sequence[_MapperEntity], 

921 raiseerr: bool, 

922 ) -> None: 

923 """implemented by subclasses""" 

924 raise NotImplementedError() 

925 

926 @classmethod 

927 def _chop_path( 

928 cls, 

929 to_chop: _PathRepresentation, 

930 path: PathRegistry, 

931 debug: bool = False, 

932 ) -> Optional[_PathRepresentation]: 

933 i = -1 

934 

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

936 zip(to_chop, path.natural_path) 

937 ): 

938 if isinstance(c_token, str): 

939 if i == 0 and ( 

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

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

942 ): 

943 return to_chop 

944 elif ( 

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

946 and c_token != p_token.key # type: ignore 

947 ): 

948 return None 

949 

950 if c_token is p_token: 

951 continue 

952 elif ( 

953 isinstance(c_token, InspectionAttr) 

954 and insp_is_mapper(c_token) 

955 and insp_is_mapper(p_token) 

956 and c_token.isa(p_token) 

957 ): 

958 continue 

959 

960 else: 

961 return None 

962 return to_chop[i + 1 :] 

963 

964 

965class Load(_AbstractLoad): 

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

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

968 order to affect how various mapped attributes are loaded. 

969 

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

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

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

973 except for in some very specific cases. 

974 

975 .. seealso:: 

976 

977 :ref:`orm_queryguide_relationship_per_entity_wildcard` - illustrates an 

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

979 

980 """ 

981 

982 __slots__ = ( 

983 "path", 

984 "context", 

985 "additional_source_entities", 

986 ) 

987 

988 _traverse_internals = [ 

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

990 ( 

991 "context", 

992 visitors.InternalTraversal.dp_has_cache_key_list, 

993 ), 

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

995 ( 

996 "additional_source_entities", 

997 visitors.InternalTraversal.dp_has_cache_key_list, 

998 ), 

999 ] 

1000 _cache_key_traversal = None 

1001 

1002 path: PathRegistry 

1003 context: Tuple[_LoadElement, ...] 

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

1005 

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

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

1008 insp._post_inspect 

1009 

1010 self.path = insp._path_registry 

1011 self.context = () 

1012 self.propagate_to_loaders = False 

1013 self.additional_source_entities = () 

1014 

1015 def __str__(self) -> str: 

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

1017 

1018 @classmethod 

1019 def _construct_for_existing_path( 

1020 cls, path: _AbstractEntityRegistry 

1021 ) -> Load: 

1022 load = cls.__new__(cls) 

1023 load.path = path 

1024 load.context = () 

1025 load.propagate_to_loaders = False 

1026 load.additional_source_entities = () 

1027 return load 

1028 

1029 def _adapt_cached_option_to_uncached_option( 

1030 self, context: QueryContext, uncached_opt: ORMOption 

1031 ) -> ORMOption: 

1032 if uncached_opt is self: 

1033 return self 

1034 return self._adjust_for_extra_criteria(context) 

1035 

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

1037 cloned = self._clone() 

1038 cloned.context = tuple( 

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

1040 ) 

1041 return cloned 

1042 

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

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

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

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

1047 

1048 """ 

1049 

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

1051 # actually have any extra_criteria options, which is the 

1052 # common case 

1053 for value in self.context: 

1054 if value._extra_criteria: 

1055 break 

1056 else: 

1057 return self 

1058 

1059 replacement_cache_key = context.user_passed_query._generate_cache_key() 

1060 

1061 if replacement_cache_key is None: 

1062 return self 

1063 

1064 orig_query = context.compile_state.select_statement 

1065 orig_cache_key = orig_query._generate_cache_key() 

1066 assert orig_cache_key is not None 

1067 

1068 def process( 

1069 opt: _LoadElement, 

1070 replacement_cache_key: CacheKey, 

1071 orig_cache_key: CacheKey, 

1072 ) -> _LoadElement: 

1073 cloned_opt = opt._clone() 

1074 

1075 cloned_opt._extra_criteria = tuple( 

1076 replacement_cache_key._apply_params_to_element( 

1077 orig_cache_key, crit 

1078 ) 

1079 for crit in cloned_opt._extra_criteria 

1080 ) 

1081 

1082 return cloned_opt 

1083 

1084 cloned = self._clone() 

1085 cloned.context = tuple( 

1086 ( 

1087 process(value, replacement_cache_key, orig_cache_key) 

1088 if value._extra_criteria 

1089 else value 

1090 ) 

1091 for value in self.context 

1092 ) 

1093 return cloned 

1094 

1095 def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr): 

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

1097 entity inside of _LoadElement objects. 

1098 

1099 """ 

1100 path = self.path 

1101 

1102 for ent in mapper_entities: 

1103 ezero = ent.entity_zero 

1104 if ezero and orm_util._entity_corresponds_to( 

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

1106 # safe to pass to _entity_corresponds_to() 

1107 ezero, 

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

1109 ): 

1110 return ezero 

1111 

1112 return None 

1113 

1114 def _process( 

1115 self, 

1116 compile_state: _ORMCompileState, 

1117 mapper_entities: Sequence[_MapperEntity], 

1118 raiseerr: bool, 

1119 ) -> None: 

1120 reconciled_lead_entity = self._reconcile_query_entities_with_us( 

1121 mapper_entities, raiseerr 

1122 ) 

1123 

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

1125 has_current_path = bool(compile_state.compile_options._current_path) 

1126 

1127 for loader in self.context: 

1128 # issue #11292 

1129 # historically, propagate_to_loaders was only considered at 

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

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

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

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

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

1135 # so we check again 

1136 if has_current_path and not loader.propagate_to_loaders: 

1137 continue 

1138 loader.process_compile_state( 

1139 self, 

1140 compile_state, 

1141 mapper_entities, 

1142 reconciled_lead_entity, 

1143 raiseerr, 

1144 ) 

1145 

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

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

1148 :class:`_orm.Load` object. 

1149 

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

1151 

1152 """ 

1153 cloned = self._generate() 

1154 

1155 assert cloned.propagate_to_loaders == self.propagate_to_loaders 

1156 

1157 if not any( 

1158 orm_util._entity_corresponds_to_use_path_impl( 

1159 elem, cloned.path.odd_element(0) 

1160 ) 

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

1162 + parent.additional_source_entities 

1163 ): 

1164 if len(cloned.path) > 1: 

1165 attrname = cloned.path[1] 

1166 parent_entity = cloned.path[0] 

1167 else: 

1168 attrname = cloned.path[0] 

1169 parent_entity = cloned.path[0] 

1170 _raise_for_does_not_link(parent.path, attrname, parent_entity) 

1171 

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

1173 

1174 if self.context: 

1175 cloned.context = tuple( 

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

1177 ) 

1178 

1179 if cloned.context: 

1180 parent.context += cloned.context 

1181 parent.additional_source_entities += ( 

1182 cloned.additional_source_entities 

1183 ) 

1184 

1185 @_generative 

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

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

1188 :class:`_orm.Load` 

1189 object. 

1190 

1191 E.g.:: 

1192 

1193 query = session.query(Author) 

1194 query = query.options( 

1195 joinedload(Author.book).options( 

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

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

1198 ) 

1199 ) 

1200 

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

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

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

1204 

1205 .. seealso:: 

1206 

1207 :func:`.defaultload` 

1208 

1209 :ref:`orm_queryguide_relationship_sub_options` 

1210 

1211 """ 

1212 for opt in opts: 

1213 try: 

1214 opt._apply_to_parent(self) 

1215 except AttributeError as ae: 

1216 if not isinstance(opt, _AbstractLoad): 

1217 raise sa_exc.ArgumentError( 

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

1219 "Load.options() method." 

1220 ) from ae 

1221 else: 

1222 raise 

1223 return self 

1224 

1225 def _clone_for_bind_strategy( 

1226 self, 

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

1228 strategy: Optional[_StrategyKey], 

1229 wildcard_key: Optional[_WildcardKeyType], 

1230 opts: Optional[_OptsType] = None, 

1231 attr_group: Optional[_AttrGroupType] = None, 

1232 propagate_to_loaders: bool = True, 

1233 reconcile_to_other: Optional[bool] = None, 

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

1235 ) -> Self: 

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

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

1238 # InstanceState.load_options 

1239 if propagate_to_loaders: 

1240 self.propagate_to_loaders = True 

1241 

1242 if self.path.is_token: 

1243 raise sa_exc.ArgumentError( 

1244 "Wildcard token cannot be followed by another entity" 

1245 ) 

1246 

1247 elif path_is_property(self.path): 

1248 # re-use the lookup which will raise a nicely formatted 

1249 # LoaderStrategyException 

1250 if strategy: 

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

1252 else: 

1253 raise sa_exc.ArgumentError( 

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

1255 "refer to a mapped entity" 

1256 ) 

1257 

1258 if attrs is None: 

1259 load_element = _ClassStrategyLoad.create( 

1260 self.path, 

1261 None, 

1262 strategy, 

1263 wildcard_key, 

1264 opts, 

1265 propagate_to_loaders, 

1266 attr_group=attr_group, 

1267 reconcile_to_other=reconcile_to_other, 

1268 extra_criteria=extra_criteria, 

1269 ) 

1270 if load_element: 

1271 self.context += (load_element,) 

1272 assert opts is not None 

1273 self.additional_source_entities += cast( 

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

1275 ) 

1276 

1277 else: 

1278 for attr in attrs: 

1279 if isinstance(attr, str): 

1280 load_element = _TokenStrategyLoad.create( 

1281 self.path, 

1282 attr, 

1283 strategy, 

1284 wildcard_key, 

1285 opts, 

1286 propagate_to_loaders, 

1287 attr_group=attr_group, 

1288 reconcile_to_other=reconcile_to_other, 

1289 extra_criteria=extra_criteria, 

1290 ) 

1291 else: 

1292 load_element = _AttributeStrategyLoad.create( 

1293 self.path, 

1294 attr, 

1295 strategy, 

1296 wildcard_key, 

1297 opts, 

1298 propagate_to_loaders, 

1299 attr_group=attr_group, 

1300 reconcile_to_other=reconcile_to_other, 

1301 extra_criteria=extra_criteria, 

1302 ) 

1303 

1304 if load_element: 

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

1306 # object with the latest path. 

1307 if wildcard_key is _RELATIONSHIP_TOKEN: 

1308 self.path = load_element.path 

1309 self.context += (load_element,) 

1310 

1311 # this seems to be effective for selectinloader, 

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

1313 # but does not work for immediateloader, which still 

1314 # must add additional options at load time 

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

1316 r1 = load_element._recurse() 

1317 self.context += (r1,) 

1318 

1319 return self 

1320 

1321 def __getstate__(self): 

1322 d = self._shallow_to_dict() 

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

1324 return d 

1325 

1326 def __setstate__(self, state): 

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

1328 self._shallow_from_dict(state) 

1329 

1330 

1331class _WildcardLoad(_AbstractLoad): 

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

1333 

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

1335 

1336 _traverse_internals = [ 

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

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

1339 ( 

1340 "local_opts", 

1341 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1342 ), 

1343 ] 

1344 cache_key_traversal: _CacheKeyTraversalType = None 

1345 

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

1347 local_opts: _OptsType 

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

1349 propagate_to_loaders = False 

1350 

1351 def __init__(self) -> None: 

1352 self.path = () 

1353 self.strategy = None 

1354 self.local_opts = util.EMPTY_DICT 

1355 

1356 def _clone_for_bind_strategy( 

1357 self, 

1358 attrs, 

1359 strategy, 

1360 wildcard_key, 

1361 opts=None, 

1362 attr_group=None, 

1363 propagate_to_loaders=True, 

1364 reconcile_to_other=None, 

1365 extra_criteria=None, 

1366 ): 

1367 assert attrs is not None 

1368 attr = attrs[0] 

1369 assert ( 

1370 wildcard_key 

1371 and isinstance(attr, str) 

1372 and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN) 

1373 ) 

1374 

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

1376 

1377 self.strategy = strategy 

1378 self.path = (attr,) 

1379 if opts: 

1380 self.local_opts = util.immutabledict(opts) 

1381 

1382 assert extra_criteria is None 

1383 

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

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

1386 

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

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

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

1390 

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

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

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

1394 

1395 """ 

1396 assert self.path 

1397 attr = self.path[0] 

1398 if attr.endswith(_DEFAULT_TOKEN): 

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

1400 

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

1402 

1403 assert effective_path.is_token 

1404 

1405 loader = _TokenStrategyLoad.create( 

1406 effective_path, 

1407 None, 

1408 self.strategy, 

1409 None, 

1410 self.local_opts, 

1411 self.propagate_to_loaders, 

1412 ) 

1413 

1414 parent.context += (loader,) 

1415 

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

1417 is_refresh = compile_state.compile_options._for_refresh_state 

1418 

1419 if is_refresh and not self.propagate_to_loaders: 

1420 return 

1421 

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

1423 current_path = compile_state.current_path 

1424 

1425 start_path: _PathRepresentation = self.path 

1426 

1427 if current_path: 

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

1429 # None back here 

1430 new_path = self._chop_path(start_path, current_path) 

1431 if new_path is None: 

1432 return 

1433 

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

1435 # just returns it 

1436 assert new_path == start_path 

1437 

1438 # start_path is a single-token tuple 

1439 assert start_path and len(start_path) == 1 

1440 

1441 token = start_path[0] 

1442 assert isinstance(token, str) 

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

1444 

1445 if not entity: 

1446 return 

1447 

1448 path_element = entity 

1449 

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

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

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

1453 # tokens and populate into the Load(). 

1454 

1455 assert isinstance(token, str) 

1456 loader = _TokenStrategyLoad.create( 

1457 path_element._path_registry, 

1458 token, 

1459 self.strategy, 

1460 None, 

1461 self.local_opts, 

1462 self.propagate_to_loaders, 

1463 raiseerr=raiseerr, 

1464 ) 

1465 if not loader: 

1466 return 

1467 

1468 assert loader.path.is_token 

1469 

1470 # don't pass a reconciled lead entity here 

1471 loader.process_compile_state( 

1472 self, compile_state, mapper_entities, None, raiseerr 

1473 ) 

1474 

1475 return loader 

1476 

1477 def _find_entity_basestring( 

1478 self, 

1479 entities: Iterable[_InternalEntityType[Any]], 

1480 token: str, 

1481 raiseerr: bool, 

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

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

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

1485 if raiseerr: 

1486 raise sa_exc.ArgumentError( 

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

1488 f"loader option to multiple entities " 

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

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

1491 f"""{ 

1492 ", ".join( 

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

1494 for ent in entities 

1495 ) 

1496 }.""" 

1497 ) 

1498 elif token.endswith(_DEFAULT_TOKEN): 

1499 raiseerr = False 

1500 

1501 for ent in entities: 

1502 # return only the first _MapperEntity when searching 

1503 # based on string prop name. Ideally object 

1504 # attributes are used to specify more exactly. 

1505 return ent 

1506 else: 

1507 if raiseerr: 

1508 raise sa_exc.ArgumentError( 

1509 "Query has only expression-based entities - " 

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

1511 ) 

1512 else: 

1513 return None 

1514 

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

1516 d = self._shallow_to_dict() 

1517 return d 

1518 

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

1520 self._shallow_from_dict(state) 

1521 

1522 

1523class _LoadElement( 

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

1525): 

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

1527 and pass options to it. 

1528 

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

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

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

1532 

1533 .. versionadded:: 2.0 

1534 

1535 """ 

1536 

1537 __slots__ = ( 

1538 "path", 

1539 "strategy", 

1540 "propagate_to_loaders", 

1541 "local_opts", 

1542 "_extra_criteria", 

1543 "_reconcile_to_other", 

1544 ) 

1545 __visit_name__ = "load_element" 

1546 

1547 _traverse_internals = [ 

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

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

1550 ( 

1551 "local_opts", 

1552 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1553 ), 

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

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

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

1557 ] 

1558 _cache_key_traversal = None 

1559 

1560 _extra_criteria: Tuple[Any, ...] 

1561 

1562 _reconcile_to_other: Optional[bool] 

1563 strategy: Optional[_StrategyKey] 

1564 path: PathRegistry 

1565 propagate_to_loaders: bool 

1566 

1567 local_opts: util.immutabledict[str, Any] 

1568 

1569 is_token_strategy: bool 

1570 is_class_strategy: bool 

1571 

1572 def __hash__(self) -> int: 

1573 return id(self) 

1574 

1575 def __eq__(self, other): 

1576 return traversals.compare(self, other) 

1577 

1578 @property 

1579 def is_opts_only(self) -> bool: 

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

1581 

1582 def _clone(self, **kw: Any) -> _LoadElement: 

1583 cls = self.__class__ 

1584 s = cls.__new__(cls) 

1585 

1586 self._shallow_copy_to(s) 

1587 return s 

1588 

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

1590 new = self._clone() 

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

1592 return new 

1593 

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

1595 d = self._shallow_to_dict() 

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

1597 return d 

1598 

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

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

1601 self._shallow_from_dict(state) 

1602 

1603 def _raise_for_no_match(self, parent_loader, mapper_entities): 

1604 path = parent_loader.path 

1605 

1606 found_entities = False 

1607 for ent in mapper_entities: 

1608 ezero = ent.entity_zero 

1609 if ezero: 

1610 found_entities = True 

1611 break 

1612 

1613 if not found_entities: 

1614 raise sa_exc.ArgumentError( 

1615 "Query has only expression-based entities; " 

1616 f"attribute loader options for {path[0]} can't " 

1617 "be applied here." 

1618 ) 

1619 else: 

1620 raise sa_exc.ArgumentError( 

1621 f"Mapped class {path[0]} does not apply to any of the " 

1622 f"root entities in this query, e.g. " 

1623 f"""{ 

1624 ", ".join( 

1625 str(x.entity_zero) 

1626 for x in mapper_entities if x.entity_zero 

1627 )}. Please """ 

1628 "specify the full path " 

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

1630 "attribute. " 

1631 ) 

1632 

1633 def _adjust_effective_path_for_current_path( 

1634 self, effective_path: PathRegistry, current_path: PathRegistry 

1635 ) -> Optional[PathRegistry]: 

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

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

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

1639 current_path. 

1640 

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

1642 

1643 .. sourcecode:: text 

1644 

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

1646 

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

1648 

1649 The adjusted path would be: 

1650 

1651 .. sourcecode:: text 

1652 

1653 Item -> keywords -> Keyword 

1654 

1655 

1656 """ 

1657 chopped_start_path = Load._chop_path( 

1658 effective_path.natural_path, current_path 

1659 ) 

1660 if not chopped_start_path: 

1661 return None 

1662 

1663 tokens_removed_from_start_path = len(effective_path) - len( 

1664 chopped_start_path 

1665 ) 

1666 

1667 loader_lead_path_element = self.path[tokens_removed_from_start_path] 

1668 

1669 effective_path = PathRegistry.coerce( 

1670 (loader_lead_path_element,) + chopped_start_path[1:] 

1671 ) 

1672 

1673 return effective_path 

1674 

1675 def _init_path( 

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

1677 ): 

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

1679 a new path. 

1680 

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

1682 a :class:`._LoadElement` object. 

1683 

1684 """ 

1685 raise NotImplementedError() 

1686 

1687 def _prepare_for_compile_state( 

1688 self, 

1689 parent_loader, 

1690 compile_state, 

1691 mapper_entities, 

1692 reconciled_lead_entity, 

1693 raiseerr, 

1694 ): 

1695 """implemented by subclasses.""" 

1696 raise NotImplementedError() 

1697 

1698 def process_compile_state( 

1699 self, 

1700 parent_loader, 

1701 compile_state, 

1702 mapper_entities, 

1703 reconciled_lead_entity, 

1704 raiseerr, 

1705 ): 

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

1707 _LoadElement. 

1708 

1709 """ 

1710 keys = self._prepare_for_compile_state( 

1711 parent_loader, 

1712 compile_state, 

1713 mapper_entities, 

1714 reconciled_lead_entity, 

1715 raiseerr, 

1716 ) 

1717 for key in keys: 

1718 if key in compile_state.attributes: 

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

1720 self, compile_state.attributes[key] 

1721 ) 

1722 else: 

1723 compile_state.attributes[key] = self 

1724 

1725 @classmethod 

1726 def create( 

1727 cls, 

1728 path: PathRegistry, 

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

1730 strategy: Optional[_StrategyKey], 

1731 wildcard_key: Optional[_WildcardKeyType], 

1732 local_opts: Optional[_OptsType], 

1733 propagate_to_loaders: bool, 

1734 raiseerr: bool = True, 

1735 attr_group: Optional[_AttrGroupType] = None, 

1736 reconcile_to_other: Optional[bool] = None, 

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

1738 ) -> _LoadElement: 

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

1740 

1741 opt = cls.__new__(cls) 

1742 opt.path = path 

1743 opt.strategy = strategy 

1744 opt.propagate_to_loaders = propagate_to_loaders 

1745 opt.local_opts = ( 

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

1747 ) 

1748 opt._extra_criteria = () 

1749 

1750 if reconcile_to_other is not None: 

1751 opt._reconcile_to_other = reconcile_to_other 

1752 elif strategy is None and not local_opts: 

1753 opt._reconcile_to_other = True 

1754 else: 

1755 opt._reconcile_to_other = None 

1756 

1757 path = opt._init_path( 

1758 path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

1759 ) 

1760 

1761 if not path: 

1762 return None # type: ignore 

1763 

1764 assert opt.is_token_strategy == path.is_token 

1765 

1766 opt.path = path 

1767 return opt 

1768 

1769 def __init__(self) -> None: 

1770 raise NotImplementedError() 

1771 

1772 def _recurse(self) -> _LoadElement: 

1773 cloned = self._clone() 

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

1775 

1776 return cloned 

1777 

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

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

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

1781 path. 

1782 

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

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

1785 

1786 """ 

1787 

1788 if not any( 

1789 orm_util._entity_corresponds_to_use_path_impl( 

1790 elem, 

1791 self.path.odd_element(0), 

1792 ) 

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

1794 + parent.additional_source_entities 

1795 ): 

1796 raise sa_exc.ArgumentError( 

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

1798 f'from element "{parent.path[-1]}".' 

1799 ) 

1800 

1801 return self._prepend_path(parent.path) 

1802 

1803 def _prepend_path(self, path: PathRegistry) -> _LoadElement: 

1804 cloned = self._clone() 

1805 

1806 assert cloned.strategy == self.strategy 

1807 assert cloned.local_opts == self.local_opts 

1808 assert cloned.is_class_strategy == self.is_class_strategy 

1809 

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

1811 

1812 return cloned 

1813 

1814 @staticmethod 

1815 def _reconcile( 

1816 replacement: _LoadElement, existing: _LoadElement 

1817 ) -> _LoadElement: 

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

1819 the context.attributes under the same key. 

1820 

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

1822 existing one 

1823 

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

1825 

1826 """ 

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

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

1829 # see test_poly_loading.py 

1830 

1831 if replacement._reconcile_to_other: 

1832 return existing 

1833 elif replacement._reconcile_to_other is False: 

1834 return replacement 

1835 elif existing._reconcile_to_other: 

1836 return replacement 

1837 elif existing._reconcile_to_other is False: 

1838 return existing 

1839 

1840 if existing is replacement: 

1841 return replacement 

1842 elif ( 

1843 existing.strategy == replacement.strategy 

1844 and existing.local_opts == replacement.local_opts 

1845 ): 

1846 return replacement 

1847 elif replacement.is_opts_only: 

1848 existing = existing._clone() 

1849 existing.local_opts = existing.local_opts.union( 

1850 replacement.local_opts 

1851 ) 

1852 existing._extra_criteria += replacement._extra_criteria 

1853 return existing 

1854 elif existing.is_opts_only: 

1855 replacement = replacement._clone() 

1856 replacement.local_opts = replacement.local_opts.union( 

1857 existing.local_opts 

1858 ) 

1859 replacement._extra_criteria += existing._extra_criteria 

1860 return replacement 

1861 elif replacement.path.is_token: 

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

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

1864 # will raise as below 

1865 return replacement 

1866 

1867 raise sa_exc.InvalidRequestError( 

1868 f"Loader strategies for {replacement.path} conflict" 

1869 ) 

1870 

1871 

1872class _AttributeStrategyLoad(_LoadElement): 

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

1874 

1875 e.g.:: 

1876 

1877 joinedload(User.addresses) 

1878 defer(Order.name) 

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

1880 

1881 """ 

1882 

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

1884 

1885 __visit_name__ = "attribute_strategy_load_element" 

1886 

1887 _traverse_internals = _LoadElement._traverse_internals + [ 

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

1889 ( 

1890 "_path_with_polymorphic_path", 

1891 visitors.ExtendedInternalTraversal.dp_has_cache_key, 

1892 ), 

1893 ] 

1894 

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

1896 _path_with_polymorphic_path: Optional[PathRegistry] 

1897 

1898 is_class_strategy = False 

1899 is_token_strategy = False 

1900 

1901 def _init_path( 

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

1903 ): 

1904 assert attr is not None 

1905 self._of_type = None 

1906 self._path_with_polymorphic_path = None 

1907 insp, _, prop = _parse_attr_argument(attr) 

1908 

1909 if insp.is_property: 

1910 # direct property can be sent from internal strategy logic 

1911 # that sets up specific loaders, such as 

1912 # emit_lazyload->_lazyload_reverse 

1913 # prop = found_property = attr 

1914 prop = attr 

1915 path = path[prop] 

1916 

1917 if path.has_entity: 

1918 path = path.entity_path 

1919 return path 

1920 

1921 elif not insp.is_attribute: 

1922 # should not reach here; 

1923 assert False 

1924 

1925 # here we assume we have user-passed InstrumentedAttribute 

1926 if not orm_util._entity_corresponds_to_use_path_impl( 

1927 path[-1], attr.parent 

1928 ): 

1929 if raiseerr: 

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

1931 raise sa_exc.ArgumentError( 

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

1933 "loader option to multiple entities in the " 

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

1935 ) 

1936 else: 

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

1938 else: 

1939 return None 

1940 

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

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

1943 # test_relationship_criteria.py::RelationshipCriteriaTest:: 

1944 # test_selectinload_nested_criteria[True] if an existing 

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

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

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

1948 # poorly defined. 

1949 if extra_criteria: 

1950 assert not attr._extra_criteria 

1951 self._extra_criteria = extra_criteria 

1952 else: 

1953 self._extra_criteria = attr._extra_criteria 

1954 

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

1956 ac = attr._of_type 

1957 ext_info = inspect(ac) 

1958 self._of_type = ext_info 

1959 

1960 self._path_with_polymorphic_path = path.entity_path[prop] 

1961 

1962 path = path[prop][ext_info] 

1963 

1964 else: 

1965 path = path[prop] 

1966 

1967 if path.has_entity: 

1968 path = path.entity_path 

1969 

1970 return path 

1971 

1972 def _generate_extra_criteria(self, context): 

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

1974 immediate "extra_criteria" stored with this Load object. 

1975 

1976 Load objects are typically pulled from the cached version of 

1977 the statement from a QueryContext. The statement currently being 

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

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

1980 they handle this criteria for a result set. 

1981 

1982 """ 

1983 

1984 assert ( 

1985 self._extra_criteria 

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

1987 

1988 orig_query = context.compile_state.select_statement 

1989 current_query = context.query 

1990 

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

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

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

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

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

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

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

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

1999 

2000 # if orig_query is current_query: 

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

2002 # return and_(*self._extra_criteria) 

2003 

2004 k1 = orig_query._generate_cache_key() 

2005 k2 = current_query._generate_cache_key() 

2006 

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

2008 

2009 def _set_of_type_info(self, context, current_path): 

2010 assert self._path_with_polymorphic_path 

2011 

2012 pwpi = self._of_type 

2013 assert pwpi 

2014 if not pwpi.is_aliased_class: 

2015 pwpi = inspect( 

2016 orm_util.AliasedInsp._with_polymorphic_factory( 

2017 pwpi.mapper.base_mapper, 

2018 (pwpi.mapper,), 

2019 aliased=True, 

2020 _use_mapper_path=True, 

2021 ) 

2022 ) 

2023 start_path = self._path_with_polymorphic_path 

2024 if current_path: 

2025 new_path = self._adjust_effective_path_for_current_path( 

2026 start_path, current_path 

2027 ) 

2028 if new_path is None: 

2029 return 

2030 start_path = new_path 

2031 

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

2033 if key in context: 

2034 existing_aliased_insp = context[key] 

2035 this_aliased_insp = pwpi 

2036 new_aliased_insp = existing_aliased_insp._merge_with( 

2037 this_aliased_insp 

2038 ) 

2039 context[key] = new_aliased_insp 

2040 else: 

2041 context[key] = pwpi 

2042 

2043 def _prepare_for_compile_state( 

2044 self, 

2045 parent_loader, 

2046 compile_state, 

2047 mapper_entities, 

2048 reconciled_lead_entity, 

2049 raiseerr, 

2050 ): 

2051 # _AttributeStrategyLoad 

2052 

2053 current_path = compile_state.current_path 

2054 is_refresh = compile_state.compile_options._for_refresh_state 

2055 assert not self.path.is_token 

2056 

2057 if is_refresh and not self.propagate_to_loaders: 

2058 return [] 

2059 

2060 if self._of_type: 

2061 # apply additional with_polymorphic alias that may have been 

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

2063 self._set_of_type_info(compile_state.attributes, current_path) 

2064 

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

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

2067 return [] 

2068 

2069 if raiseerr and not reconciled_lead_entity: 

2070 self._raise_for_no_match(parent_loader, mapper_entities) 

2071 

2072 if self.path.has_entity: 

2073 effective_path = self.path.parent 

2074 else: 

2075 effective_path = self.path 

2076 

2077 if current_path: 

2078 assert effective_path is not None 

2079 effective_path = self._adjust_effective_path_for_current_path( 

2080 effective_path, current_path 

2081 ) 

2082 if effective_path is None: 

2083 return [] 

2084 

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

2086 

2087 def __getstate__(self): 

2088 d = super().__getstate__() 

2089 

2090 # can't pickle this. See 

2091 # test_pickled.py -> test_lazyload_extra_criteria_not_supported 

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

2093 # would be non-None 

2094 d["_extra_criteria"] = () 

2095 

2096 if self._path_with_polymorphic_path: 

2097 d["_path_with_polymorphic_path"] = ( 

2098 self._path_with_polymorphic_path.serialize() 

2099 ) 

2100 

2101 if self._of_type: 

2102 if self._of_type.is_aliased_class: 

2103 d["_of_type"] = None 

2104 elif self._of_type.is_mapper: 

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

2106 else: 

2107 assert False, "unexpected object for _of_type" 

2108 

2109 return d 

2110 

2111 def __setstate__(self, state): 

2112 super().__setstate__(state) 

2113 

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

2115 self._path_with_polymorphic_path = PathRegistry.deserialize( 

2116 state["_path_with_polymorphic_path"] 

2117 ) 

2118 else: 

2119 self._path_with_polymorphic_path = None 

2120 

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

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

2123 else: 

2124 self._of_type = None 

2125 

2126 

2127class _TokenStrategyLoad(_LoadElement): 

2128 """Loader strategies against wildcard attributes 

2129 

2130 e.g.:: 

2131 

2132 raiseload("*") 

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

2134 defer("*") 

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

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

2137 

2138 """ 

2139 

2140 __visit_name__ = "token_strategy_load_element" 

2141 

2142 inherit_cache = True 

2143 is_class_strategy = False 

2144 is_token_strategy = True 

2145 

2146 def _init_path( 

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

2148 ): 

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

2150 if attr is not None: 

2151 default_token = attr.endswith(_DEFAULT_TOKEN) 

2152 if attr.endswith(_WILDCARD_TOKEN) or default_token: 

2153 if wildcard_key: 

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

2155 

2156 path = path.token(attr) 

2157 return path 

2158 else: 

2159 raise sa_exc.ArgumentError( 

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

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

2162 ) 

2163 return path 

2164 

2165 def _prepare_for_compile_state( 

2166 self, 

2167 parent_loader, 

2168 compile_state, 

2169 mapper_entities, 

2170 reconciled_lead_entity, 

2171 raiseerr, 

2172 ): 

2173 # _TokenStrategyLoad 

2174 

2175 current_path = compile_state.current_path 

2176 is_refresh = compile_state.compile_options._for_refresh_state 

2177 

2178 assert self.path.is_token 

2179 

2180 if is_refresh and not self.propagate_to_loaders: 

2181 return [] 

2182 

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

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

2185 return [] 

2186 

2187 effective_path = self.path 

2188 if reconciled_lead_entity: 

2189 effective_path = PathRegistry.coerce( 

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

2191 ) 

2192 

2193 if current_path: 

2194 new_effective_path = self._adjust_effective_path_for_current_path( 

2195 effective_path, current_path 

2196 ) 

2197 if new_effective_path is None: 

2198 return [] 

2199 effective_path = new_effective_path 

2200 

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

2202 # to encompass everything from the query entity on 

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

2204 # is set. 

2205 

2206 return [ 

2207 ("loader", natural_path) 

2208 for natural_path in ( 

2209 cast( 

2210 _TokenRegistry, effective_path 

2211 )._generate_natural_for_superclasses() 

2212 ) 

2213 ] 

2214 

2215 

2216class _ClassStrategyLoad(_LoadElement): 

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

2218 an attribute path 

2219 

2220 e.g.:: 

2221 

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

2223 selectin_polymorphic(Person, [Engineer, Manager]) 

2224 ) 

2225 

2226 """ 

2227 

2228 inherit_cache = True 

2229 is_class_strategy = True 

2230 is_token_strategy = False 

2231 

2232 __visit_name__ = "class_strategy_load_element" 

2233 

2234 def _init_path( 

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

2236 ): 

2237 return path 

2238 

2239 def _prepare_for_compile_state( 

2240 self, 

2241 parent_loader, 

2242 compile_state, 

2243 mapper_entities, 

2244 reconciled_lead_entity, 

2245 raiseerr, 

2246 ): 

2247 # _ClassStrategyLoad 

2248 

2249 current_path = compile_state.current_path 

2250 is_refresh = compile_state.compile_options._for_refresh_state 

2251 

2252 if is_refresh and not self.propagate_to_loaders: 

2253 return [] 

2254 

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

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

2257 return [] 

2258 

2259 effective_path = self.path 

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 return [("loader", effective_path.natural_path)] 

2270 

2271 

2272def _generate_from_keys( 

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

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

2275 chained: bool, 

2276 kw: Any, 

2277) -> _AbstractLoad: 

2278 lead_element: Optional[_AbstractLoad] = None 

2279 

2280 attr: Any 

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

2282 for attr in _keys: 

2283 if isinstance(attr, str): 

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

2285 util.warn_deprecated( 

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

2287 "deprecated " 

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

2289 "it is " 

2290 "believed to be unused. " 

2291 "If you have been using this functionality, " 

2292 "please " 

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

2294 "tracker.", 

2295 version="1.4", 

2296 ) 

2297 attr = attr[1:] 

2298 

2299 if attr == _WILDCARD_TOKEN: 

2300 if is_default: 

2301 raise sa_exc.ArgumentError( 

2302 "Wildcard token cannot be followed by " 

2303 "another entity", 

2304 ) 

2305 

2306 if lead_element is None: 

2307 lead_element = _WildcardLoad() 

2308 

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

2310 

2311 else: 

2312 raise sa_exc.ArgumentError( 

2313 "Strings are not accepted for attribute names in " 

2314 "loader options; please use class-bound " 

2315 "attributes directly.", 

2316 ) 

2317 else: 

2318 if lead_element is None: 

2319 _, lead_entity, _ = _parse_attr_argument(attr) 

2320 lead_element = Load(lead_entity) 

2321 

2322 if is_default: 

2323 if not chained: 

2324 lead_element = lead_element.defaultload(attr) 

2325 else: 

2326 lead_element = meth( 

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

2328 ) 

2329 else: 

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

2331 

2332 assert lead_element 

2333 return lead_element 

2334 

2335 

2336def _parse_attr_argument( 

2337 attr: _AttrType, 

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

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

2340 :class:`._AbstractLoad` instance. 

2341 

2342 This is used by the standalone loader strategy functions like 

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

2344 :class:`._WildcardLoad` objects. 

2345 

2346 """ 

2347 try: 

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

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

2350 # if at all 

2351 insp: InspectionAttr = inspect(attr) # type: ignore 

2352 except sa_exc.NoInspectionAvailable as err: 

2353 raise sa_exc.ArgumentError( 

2354 "expected ORM mapped attribute for loader strategy argument" 

2355 ) from err 

2356 

2357 lead_entity: _InternalEntityType[Any] 

2358 

2359 if insp_is_mapper_property(insp): 

2360 lead_entity = insp.parent 

2361 prop = insp 

2362 elif insp_is_attribute(insp): 

2363 lead_entity = insp.parent 

2364 prop = insp.prop 

2365 else: 

2366 raise sa_exc.ArgumentError( 

2367 "expected ORM mapped attribute for loader strategy argument" 

2368 ) 

2369 

2370 return insp, lead_entity, prop 

2371 

2372 

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

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

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

2376 

2377 """ 

2378 bound_fn = getattr(_AbstractLoad, fn.__name__) 

2379 fn_doc = bound_fn.__doc__ 

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

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

2382 

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

2384 

2385""" 

2386 

2387 fn.__doc__ = fn_doc 

2388 return fn 

2389 

2390 

2391def _expand_column_strategy_attrs( 

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

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

2394 return cast( 

2395 "Tuple[_AttrType, ...]", 

2396 tuple( 

2397 a 

2398 for attr in attrs 

2399 for a in ( 

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

2401 if hasattr(attr, "_column_strategy_attrs") 

2402 else (attr,) 

2403 ) 

2404 ), 

2405 ) 

2406 

2407 

2408# standalone functions follow. docstrings are filled in 

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

2410 

2411 

2412@loader_unbound_fn 

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

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

2415 

2416 

2417@loader_unbound_fn 

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

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

2420 # add some extra state to Load of some kind 

2421 attrs = _expand_column_strategy_attrs(attrs) 

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

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

2424 

2425 

2426@loader_unbound_fn 

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

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

2429 

2430 

2431@loader_unbound_fn 

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

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

2434 

2435 

2436@loader_unbound_fn 

2437def selectinload( 

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

2439) -> _AbstractLoad: 

2440 return _generate_from_keys( 

2441 Load.selectinload, keys, False, {"recursion_depth": recursion_depth} 

2442 ) 

2443 

2444 

2445@loader_unbound_fn 

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

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

2448 

2449 

2450@loader_unbound_fn 

2451def immediateload( 

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

2453) -> _AbstractLoad: 

2454 return _generate_from_keys( 

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

2456 ) 

2457 

2458 

2459@loader_unbound_fn 

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

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

2462 

2463 

2464@loader_unbound_fn 

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

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

2467 

2468 

2469@loader_unbound_fn 

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

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

2472 

2473 

2474@loader_unbound_fn 

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

2476 if raiseload: 

2477 kw = {"raiseload": raiseload} 

2478 else: 

2479 kw = {} 

2480 

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

2482 

2483 

2484@loader_unbound_fn 

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

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

2487 

2488 

2489@loader_unbound_fn 

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

2491 element = _WildcardLoad() 

2492 return element.undefer_group(name) 

2493 

2494 

2495@loader_unbound_fn 

2496def with_expression( 

2497 key: _AttrType, expression: _ColumnExpressionArgument[Any] 

2498) -> _AbstractLoad: 

2499 return _generate_from_keys( 

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

2501 ) 

2502 

2503 

2504@loader_unbound_fn 

2505def selectin_polymorphic( 

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

2507) -> _AbstractLoad: 

2508 ul = Load(base_cls) 

2509 return ul.selectin_polymorphic(classes) 

2510 

2511 

2512def _raise_for_does_not_link(path, attrname, parent_entity): 

2513 if len(path) > 1: 

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

2515 if insp_is_aliased_class(parent_entity): 

2516 parent_entity_str = str(parent_entity) 

2517 else: 

2518 parent_entity_str = parent_entity.class_.__name__ 

2519 

2520 raise sa_exc.ArgumentError( 

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

2522 f'link from relationship "{path[-2]}%s".%s' 

2523 % ( 

2524 f".of_type({path[-1]})" if path_is_of_type else "", 

2525 ( 

2526 " Did you mean to use " 

2527 f'"{path[-2]}' 

2528 f'.of_type({parent_entity_str})" or "loadopt.options(' 

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

2530 f'[{parent_entity_str}]), ...)" ?' 

2531 if not path_is_of_type 

2532 and not path[-1].is_aliased_class 

2533 and orm_util._entity_corresponds_to( 

2534 path.entity, inspect(parent_entity).mapper 

2535 ) 

2536 else "" 

2537 ), 

2538 ) 

2539 ) 

2540 else: 

2541 raise sa_exc.ArgumentError( 

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

2543 f'link mapped class "{path[-1]}"' 

2544 )