Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/ext/hybrid.py: 51%

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

257 statements  

1# ext/hybrid.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 

8r"""Define attributes on ORM-mapped classes that have "hybrid" behavior. 

9 

10"hybrid" means the attribute has distinct behaviors defined at the 

11class level and at the instance level. 

12 

13The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of 

14method decorator and has minimal dependencies on the rest of SQLAlchemy. 

15Its basic theory of operation can work with any descriptor-based expression 

16system. 

17 

18Consider a mapping ``Interval``, representing integer ``start`` and ``end`` 

19values. We can define higher level functions on mapped classes that produce SQL 

20expressions at the class level, and Python expression evaluation at the 

21instance level. Below, each function decorated with :class:`.hybrid_method` or 

22:class:`.hybrid_property` may receive ``self`` as an instance of the class, or 

23may receive the class directly, depending on context:: 

24 

25 from __future__ import annotations 

26 

27 from sqlalchemy.ext.hybrid import hybrid_method 

28 from sqlalchemy.ext.hybrid import hybrid_property 

29 from sqlalchemy.orm import DeclarativeBase 

30 from sqlalchemy.orm import Mapped 

31 from sqlalchemy.orm import mapped_column 

32 

33 

34 class Base(DeclarativeBase): 

35 pass 

36 

37 

38 class Interval(Base): 

39 __tablename__ = "interval" 

40 

41 id: Mapped[int] = mapped_column(primary_key=True) 

42 start: Mapped[int] 

43 end: Mapped[int] 

44 

45 def __init__(self, start: int, end: int): 

46 self.start = start 

47 self.end = end 

48 

49 @hybrid_property 

50 def length(self) -> int: 

51 return self.end - self.start 

52 

53 @hybrid_method 

54 def contains(self, point: int) -> bool: 

55 return (self.start <= point) & (point <= self.end) 

56 

57 @hybrid_method 

58 def intersects(self, other: Interval) -> bool: 

59 return self.contains(other.start) | self.contains(other.end) 

60 

61Above, the ``length`` property returns the difference between the 

62``end`` and ``start`` attributes. With an instance of ``Interval``, 

63this subtraction occurs in Python, using normal Python descriptor 

64mechanics:: 

65 

66 >>> i1 = Interval(5, 10) 

67 >>> i1.length 

68 5 

69 

70When dealing with the ``Interval`` class itself, the :class:`.hybrid_property` 

71descriptor evaluates the function body given the ``Interval`` class as 

72the argument, which when evaluated with SQLAlchemy expression mechanics 

73returns a new SQL expression: 

74 

75.. sourcecode:: pycon+sql 

76 

77 >>> from sqlalchemy import select 

78 >>> print(select(Interval.length)) 

79 {printsql}SELECT interval."end" - interval.start AS length 

80 FROM interval{stop} 

81 

82 

83 >>> print(select(Interval).filter(Interval.length > 10)) 

84 {printsql}SELECT interval.id, interval.start, interval."end" 

85 FROM interval 

86 WHERE interval."end" - interval.start > :param_1 

87 

88Filtering methods such as :meth:`.Select.filter_by` are supported 

89with hybrid attributes as well: 

90 

91.. sourcecode:: pycon+sql 

92 

93 >>> print(select(Interval).filter_by(length=5)) 

94 {printsql}SELECT interval.id, interval.start, interval."end" 

95 FROM interval 

96 WHERE interval."end" - interval.start = :param_1 

97 

98The ``Interval`` class example also illustrates two methods, 

99``contains()`` and ``intersects()``, decorated with 

100:class:`.hybrid_method`. This decorator applies the same idea to 

101methods that :class:`.hybrid_property` applies to attributes. The 

102methods return boolean values, and take advantage of the Python ``|`` 

103and ``&`` bitwise operators to produce equivalent instance-level and 

104SQL expression-level boolean behavior: 

105 

106.. sourcecode:: pycon+sql 

107 

108 >>> i1.contains(6) 

109 True 

110 >>> i1.contains(15) 

111 False 

112 >>> i1.intersects(Interval(7, 18)) 

113 True 

114 >>> i1.intersects(Interval(25, 29)) 

115 False 

116 

117 >>> print(select(Interval).filter(Interval.contains(15))) 

118 {printsql}SELECT interval.id, interval.start, interval."end" 

119 FROM interval 

120 WHERE interval.start <= :start_1 AND interval."end" > :end_1{stop} 

121 

122 >>> ia = aliased(Interval) 

123 >>> print(select(Interval, ia).filter(Interval.intersects(ia))) 

124 {printsql}SELECT interval.id, interval.start, 

125 interval."end", interval_1.id AS interval_1_id, 

126 interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end 

127 FROM interval, interval AS interval_1 

128 WHERE interval.start <= interval_1.start 

129 AND interval."end" > interval_1.start 

130 OR interval.start <= interval_1."end" 

131 AND interval."end" > interval_1."end"{stop} 

132 

133.. _hybrid_distinct_expression: 

134 

135Defining Expression Behavior Distinct from Attribute Behavior 

136-------------------------------------------------------------- 

137 

138In the previous section, our usage of the ``&`` and ``|`` bitwise operators 

139within the ``Interval.contains`` and ``Interval.intersects`` methods was 

140fortunate, considering our functions operated on two boolean values to return a 

141new one. In many cases, the construction of an in-Python function and a 

142SQLAlchemy SQL expression have enough differences that two separate Python 

143expressions should be defined. The :mod:`~sqlalchemy.ext.hybrid` decorator 

144defines a **modifier** :meth:`.hybrid_property.expression` for this purpose. As an 

145example we'll define the radius of the interval, which requires the usage of 

146the absolute value function:: 

147 

148 from sqlalchemy import ColumnElement 

149 from sqlalchemy import Float 

150 from sqlalchemy import func 

151 from sqlalchemy import type_coerce 

152 

153 

154 class Interval(Base): 

155 # ... 

156 

157 @hybrid_property 

158 def radius(self) -> float: 

159 return abs(self.length) / 2 

160 

161 @radius.inplace.expression 

162 @classmethod 

163 def _radius_expression(cls) -> ColumnElement[float]: 

164 return type_coerce(func.abs(cls.length) / 2, Float) 

165 

166In the above example, the :class:`.hybrid_property` first assigned to the 

167name ``Interval.radius`` is amended by a subsequent method called 

168``Interval._radius_expression``, using the decorator 

169``@radius.inplace.expression``, which chains together two modifiers 

170:attr:`.hybrid_property.inplace` and :attr:`.hybrid_property.expression`. 

171The use of :attr:`.hybrid_property.inplace` indicates that the 

172:meth:`.hybrid_property.expression` modifier should mutate the 

173existing hybrid object at ``Interval.radius`` in place, without creating a 

174new object. Notes on this modifier and its 

175rationale are discussed in the next section :ref:`hybrid_pep484_naming`. 

176The use of ``@classmethod`` is optional, and is strictly to give typing 

177tools a hint that ``cls`` in this case is expected to be the ``Interval`` 

178class, and not an instance of ``Interval``. 

179 

180.. note:: :attr:`.hybrid_property.inplace` as well as the use of ``@classmethod`` 

181 for proper typing support are available as of SQLAlchemy 2.0.4, and will 

182 not work in earlier versions. 

183 

184With ``Interval.radius`` now including an expression element, the SQL 

185function ``ABS()`` is returned when accessing ``Interval.radius`` 

186at the class level: 

187 

188.. sourcecode:: pycon+sql 

189 

190 >>> from sqlalchemy import select 

191 >>> print(select(Interval).filter(Interval.radius > 5)) 

192 {printsql}SELECT interval.id, interval.start, interval."end" 

193 FROM interval 

194 WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1 

195 

196 

197.. _hybrid_pep484_naming: 

198 

199Using ``inplace`` to create pep-484 compliant hybrid properties 

200--------------------------------------------------------------- 

201 

202In the previous section, a :class:`.hybrid_property` decorator is illustrated 

203which includes two separate method-level functions being decorated, both 

204to produce a single object attribute referenced as ``Interval.radius``. 

205There are actually several different modifiers we can use for 

206:class:`.hybrid_property` including :meth:`.hybrid_property.expression`, 

207:meth:`.hybrid_property.setter` and :meth:`.hybrid_property.update_expression`. 

208 

209SQLAlchemy's :class:`.hybrid_property` decorator intends that adding on these 

210methods may be done in the identical manner as Python's built-in 

211``@property`` decorator, where idiomatic use is to continue to redefine the 

212attribute repeatedly, using the **same attribute name** each time, as in the 

213example below that illustrates the use of :meth:`.hybrid_property.setter` and 

214:meth:`.hybrid_property.expression` for the ``Interval.radius`` descriptor:: 

215 

216 # correct use, however is not accepted by pep-484 tooling 

217 

218 

219 class Interval(Base): 

220 # ... 

221 

222 @hybrid_property 

223 def radius(self): 

224 return abs(self.length) / 2 

225 

226 @radius.setter 

227 def radius(self, value): 

228 self.length = value * 2 

229 

230 @radius.expression 

231 def radius(cls): 

232 return type_coerce(func.abs(cls.length) / 2, Float) 

233 

234Above, there are three ``Interval.radius`` methods, but as each are decorated, 

235first by the :class:`.hybrid_property` decorator and then by the 

236``@radius`` name itself, the end effect is that ``Interval.radius`` is 

237a single attribute with three different functions contained within it. 

238This style of use is taken from `Python's documented use of @property 

239<https://docs.python.org/3/library/functions.html#property>`_. 

240It is important to note that the way both ``@property`` as well as 

241:class:`.hybrid_property` work, a **copy of the descriptor is made each time**. 

242That is, each call to ``@radius.expression``, ``@radius.setter`` etc. 

243make a new object entirely. This allows the attribute to be re-defined in 

244subclasses without issue (see :ref:`hybrid_reuse_subclass` later in this 

245section for how this is used). 

246 

247However, the above approach is not compatible with typing tools such as 

248mypy and pyright. Python's own ``@property`` decorator does not have this 

249limitation only because 

250`these tools hardcode the behavior of @property 

251<https://github.com/python/typing/discussions/1102>`_, meaning this syntax 

252is not available to SQLAlchemy under :pep:`484` compliance. 

253 

254In order to produce a reasonable syntax while remaining typing compliant, 

255the :attr:`.hybrid_property.inplace` decorator allows the same 

256decorator to be reused with different method names, while still producing 

257a single decorator under one name:: 

258 

259 # correct use which is also accepted by pep-484 tooling 

260 

261 

262 class Interval(Base): 

263 # ... 

264 

265 @hybrid_property 

266 def radius(self) -> float: 

267 return abs(self.length) / 2 

268 

269 @radius.inplace.setter 

270 def _radius_setter(self, value: float) -> None: 

271 # for example only 

272 self.length = value * 2 

273 

274 @radius.inplace.expression 

275 @classmethod 

276 def _radius_expression(cls) -> ColumnElement[float]: 

277 return type_coerce(func.abs(cls.length) / 2, Float) 

278 

279Using :attr:`.hybrid_property.inplace` further qualifies the use of the 

280decorator that a new copy should not be made, thereby maintaining the 

281``Interval.radius`` name while allowing additional methods 

282``Interval._radius_setter`` and ``Interval._radius_expression`` to be 

283differently named. 

284 

285 

286.. versionadded:: 2.0.4 Added :attr:`.hybrid_property.inplace` to allow 

287 less verbose construction of composite :class:`.hybrid_property` objects 

288 while not having to use repeated method names. Additionally allowed the 

289 use of ``@classmethod`` within :attr:`.hybrid_property.expression`, 

290 :attr:`.hybrid_property.update_expression`, and 

291 :attr:`.hybrid_property.comparator` to allow typing tools to identify 

292 ``cls`` as a class and not an instance in the method signature. 

293 

294 

295Defining Setters 

296---------------- 

297 

298The :meth:`.hybrid_property.setter` modifier allows the construction of a 

299custom setter method, that can modify values on the object:: 

300 

301 class Interval(Base): 

302 # ... 

303 

304 @hybrid_property 

305 def length(self) -> int: 

306 return self.end - self.start 

307 

308 @length.inplace.setter 

309 def _length_setter(self, value: int) -> None: 

310 self.end = self.start + value 

311 

312The ``length(self, value)`` method is now called upon set:: 

313 

314 >>> i1 = Interval(5, 10) 

315 >>> i1.length 

316 5 

317 >>> i1.length = 12 

318 >>> i1.end 

319 17 

320 

321.. _hybrid_bulk_update: 

322 

323Supporting ORM Bulk INSERT and UPDATE 

324------------------------------------- 

325 

326Hybrids have support for use in ORM Bulk INSERT/UPDATE operations described 

327at :ref:`orm_expression_update_delete`. There are two distinct hooks 

328that may be used supply a hybrid value within a DML operation: 

329 

3301. The :meth:`.hybrid_property.update_expression` hook indicates a method that 

331 can provide one or more expressions to render in the SET clause of an 

332 UPDATE or INSERT statement, in response to when a hybrid attribute is referenced 

333 directly in the :meth:`.UpdateBase.values` method; i.e. the use shown 

334 in :ref:`orm_queryguide_update_delete_where` and :ref:`orm_queryguide_insert_values` 

335 

3362. The :meth:`.hybrid_property.bulk_dml` hook indicates a method that 

337 can intercept individual parameter dictionaries sent to :meth:`_orm.Session.execute`, 

338 i.e. the use shown at :ref:`orm_queryguide_bulk_insert` as well 

339 as :ref:`orm_queryguide_bulk_update`. 

340 

341Using update_expression with update.values() and insert.values() 

342^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

343 

344The :meth:`.hybrid_property.update_expression` decorator indicates a method 

345that is invoked when a hybrid is used in the :meth:`.ValuesBase.values` clause 

346of an :func:`_sql.update` or :func:`_sql.insert` statement. It returns a list 

347of tuple pairs ``[(x1, y1), (x2, y2), ...]`` which will expand into the SET 

348clause of an UPDATE statement as ``SET x1=y1, x2=y2, ...``. 

349 

350The :func:`_sql.from_dml_column` construct is often useful as it can create a 

351SQL expression that refers to another column that may also present in the same 

352INSERT or UPDATE statement, alternatively falling back to referring to the 

353original column if such an expression is not present. 

354 

355In the example below, the ``total_price`` hybrid will derive the ``price`` 

356column, by taking the given "total price" value and dividing it by a 

357``tax_rate`` value that is also present in the :meth:`.ValuesBase.values` call:: 

358 

359 from sqlalchemy import from_dml_column 

360 

361 

362 class Product(Base): 

363 __tablename__ = "product" 

364 

365 id: Mapped[int] = mapped_column(primary_key=True) 

366 price: Mapped[float] 

367 tax_rate: Mapped[float] 

368 

369 @hybrid_property 

370 def total_price(self) -> float: 

371 return self.price * (1 + self.tax_rate) 

372 

373 @total_price.inplace.update_expression 

374 @classmethod 

375 def _total_price_update_expression( 

376 cls, value: Any 

377 ) -> List[Tuple[Any, Any]]: 

378 return [(cls.price, value / (1 + from_dml_column(cls.tax_rate)))] 

379 

380When used in an UPDATE statement, :func:`_sql.from_dml_column` creates a 

381reference to the ``tax_rate`` column that will use the value passed to 

382the :meth:`.ValuesBase.values` method, rather than the existing value on the column 

383in the database. This allows the hybrid to access other values being 

384updated in the same statement: 

385 

386.. sourcecode:: pycon+sql 

387 

388 >>> from sqlalchemy import update 

389 >>> print( 

390 ... update(Product).values( 

391 ... {Product.tax_rate: 0.08, Product.total_price: 125.00} 

392 ... ) 

393 ... ) 

394 {printsql}UPDATE product SET tax_rate=:tax_rate, price=(:total_price / (:tax_rate + :param_1)) 

395 

396When the column referenced by :func:`_sql.from_dml_column` (in this case ``product.tax_rate``) 

397is omitted from :meth:`.ValuesBase.values`, the rendered expression falls back to 

398using the original column: 

399 

400.. sourcecode:: pycon+sql 

401 

402 >>> from sqlalchemy import update 

403 >>> print(update(Product).values({Product.total_price: 125.00})) 

404 {printsql}UPDATE product SET price=(:total_price / (tax_rate + :param_1)) 

405 

406 

407 

408Using bulk_dml to intercept bulk parameter dictionaries 

409^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

410 

411.. versionadded:: 2.1 

412 

413For bulk operations that pass a list of parameter dictionaries to 

414methods like :meth:`.Session.execute`, the 

415:meth:`.hybrid_property.bulk_dml` decorator provides a hook that can 

416receive each dictionary and populate it with new values. 

417 

418The implementation for the :meth:`.hybrid_property.bulk_dml` hook can retrieve 

419other column values from the parameter dictionary:: 

420 

421 from typing import MutableMapping 

422 

423 

424 class Product(Base): 

425 __tablename__ = "product" 

426 

427 id: Mapped[int] = mapped_column(primary_key=True) 

428 price: Mapped[float] 

429 tax_rate: Mapped[float] 

430 

431 @hybrid_property 

432 def total_price(self) -> float: 

433 return self.price * (1 + self.tax_rate) 

434 

435 @total_price.inplace.bulk_dml 

436 @classmethod 

437 def _total_price_bulk_dml( 

438 cls, mapping: MutableMapping[str, Any], value: float 

439 ) -> None: 

440 mapping["price"] = value / (1 + mapping["tax_rate"]) 

441 

442This allows for bulk INSERT/UPDATE with derived values:: 

443 

444 # Bulk INSERT 

445 session.execute( 

446 insert(Product), 

447 [ 

448 {"tax_rate": 0.08, "total_price": 125.00}, 

449 {"tax_rate": 0.05, "total_price": 110.00}, 

450 ], 

451 ) 

452 

453Note that the method decorated by :meth:`.hybrid_property.bulk_dml` is invoked 

454only with parameter dictionaries and does not have the ability to use 

455SQL expressions in the given dictionaries, only literal Python values that will 

456be passed to parameters in the INSERT or UPDATE statement. 

457 

458.. seealso:: 

459 

460 :ref:`orm_expression_update_delete` - includes background on ORM-enabled 

461 UPDATE statements 

462 

463 

464Working with Relationships 

465-------------------------- 

466 

467There's no essential difference when creating hybrids that work with 

468related objects as opposed to column-based data. The need for distinct 

469expressions tends to be greater. The two variants we'll illustrate 

470are the "join-dependent" hybrid, and the "correlated subquery" hybrid. 

471 

472Join-Dependent Relationship Hybrid 

473^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

474 

475Consider the following declarative 

476mapping which relates a ``User`` to a ``SavingsAccount``:: 

477 

478 from __future__ import annotations 

479 

480 from decimal import Decimal 

481 from typing import cast 

482 from typing import List 

483 from typing import Optional 

484 

485 from sqlalchemy import ForeignKey 

486 from sqlalchemy import Numeric 

487 from sqlalchemy import String 

488 from sqlalchemy import SQLColumnExpression 

489 from sqlalchemy.ext.hybrid import hybrid_property 

490 from sqlalchemy.orm import DeclarativeBase 

491 from sqlalchemy.orm import Mapped 

492 from sqlalchemy.orm import mapped_column 

493 from sqlalchemy.orm import relationship 

494 

495 

496 class Base(DeclarativeBase): 

497 pass 

498 

499 

500 class SavingsAccount(Base): 

501 __tablename__ = "account" 

502 id: Mapped[int] = mapped_column(primary_key=True) 

503 user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) 

504 balance: Mapped[Decimal] = mapped_column(Numeric(15, 5)) 

505 

506 owner: Mapped[User] = relationship(back_populates="accounts") 

507 

508 

509 class User(Base): 

510 __tablename__ = "user" 

511 id: Mapped[int] = mapped_column(primary_key=True) 

512 name: Mapped[str] = mapped_column(String(100)) 

513 

514 accounts: Mapped[List[SavingsAccount]] = relationship( 

515 back_populates="owner", lazy="selectin" 

516 ) 

517 

518 @hybrid_property 

519 def balance(self) -> Optional[Decimal]: 

520 if self.accounts: 

521 return self.accounts[0].balance 

522 else: 

523 return None 

524 

525 @balance.inplace.setter 

526 def _balance_setter(self, value: Optional[Decimal]) -> None: 

527 assert value is not None 

528 

529 if not self.accounts: 

530 account = SavingsAccount(owner=self) 

531 else: 

532 account = self.accounts[0] 

533 account.balance = value 

534 

535 @balance.inplace.expression 

536 @classmethod 

537 def _balance_expression(cls) -> SQLColumnExpression[Optional[Decimal]]: 

538 return cast( 

539 "SQLColumnExpression[Optional[Decimal]]", 

540 SavingsAccount.balance, 

541 ) 

542 

543The above hybrid property ``balance`` works with the first 

544``SavingsAccount`` entry in the list of accounts for this user. The 

545in-Python getter/setter methods can treat ``accounts`` as a Python 

546list available on ``self``. 

547 

548.. tip:: The ``User.balance`` getter in the above example accesses the 

549 ``self.accounts`` collection, which will normally be loaded via the 

550 :func:`.selectinload` loader strategy configured on the ``User.balance`` 

551 :func:`_orm.relationship`. The default loader strategy when not otherwise 

552 stated on :func:`_orm.relationship` is :func:`.lazyload`, which emits SQL on 

553 demand. When using asyncio, on-demand loaders such as :func:`.lazyload` are 

554 not supported, so care should be taken to ensure the ``self.accounts`` 

555 collection is accessible to this hybrid accessor when using asyncio. 

556 

557At the expression level, it's expected that the ``User`` class will 

558be used in an appropriate context such that an appropriate join to 

559``SavingsAccount`` will be present: 

560 

561.. sourcecode:: pycon+sql 

562 

563 >>> from sqlalchemy import select 

564 >>> print( 

565 ... select(User, User.balance) 

566 ... .join(User.accounts) 

567 ... .filter(User.balance > 5000) 

568 ... ) 

569 {printsql}SELECT "user".id AS user_id, "user".name AS user_name, 

570 account.balance AS account_balance 

571 FROM "user" JOIN account ON "user".id = account.user_id 

572 WHERE account.balance > :balance_1 

573 

574Note however, that while the instance level accessors need to worry 

575about whether ``self.accounts`` is even present, this issue expresses 

576itself differently at the SQL expression level, where we basically 

577would use an outer join: 

578 

579.. sourcecode:: pycon+sql 

580 

581 >>> from sqlalchemy import select 

582 >>> from sqlalchemy import or_ 

583 >>> print( 

584 ... select(User, User.balance) 

585 ... .outerjoin(User.accounts) 

586 ... .filter(or_(User.balance < 5000, User.balance == None)) 

587 ... ) 

588 {printsql}SELECT "user".id AS user_id, "user".name AS user_name, 

589 account.balance AS account_balance 

590 FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id 

591 WHERE account.balance < :balance_1 OR account.balance IS NULL 

592 

593Correlated Subquery Relationship Hybrid 

594^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

595 

596We can, of course, forego being dependent on the enclosing query's usage 

597of joins in favor of the correlated subquery, which can portably be packed 

598into a single column expression. A correlated subquery is more portable, but 

599often performs more poorly at the SQL level. Using the same technique 

600illustrated at :ref:`mapper_column_property_sql_expressions`, 

601we can adjust our ``SavingsAccount`` example to aggregate the balances for 

602*all* accounts, and use a correlated subquery for the column expression:: 

603 

604 from __future__ import annotations 

605 

606 from decimal import Decimal 

607 from typing import List 

608 

609 from sqlalchemy import ForeignKey 

610 from sqlalchemy import func 

611 from sqlalchemy import Numeric 

612 from sqlalchemy import select 

613 from sqlalchemy import SQLColumnExpression 

614 from sqlalchemy import String 

615 from sqlalchemy.ext.hybrid import hybrid_property 

616 from sqlalchemy.orm import DeclarativeBase 

617 from sqlalchemy.orm import Mapped 

618 from sqlalchemy.orm import mapped_column 

619 from sqlalchemy.orm import relationship 

620 

621 

622 class Base(DeclarativeBase): 

623 pass 

624 

625 

626 class SavingsAccount(Base): 

627 __tablename__ = "account" 

628 id: Mapped[int] = mapped_column(primary_key=True) 

629 user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) 

630 balance: Mapped[Decimal] = mapped_column(Numeric(15, 5)) 

631 

632 owner: Mapped[User] = relationship(back_populates="accounts") 

633 

634 

635 class User(Base): 

636 __tablename__ = "user" 

637 id: Mapped[int] = mapped_column(primary_key=True) 

638 name: Mapped[str] = mapped_column(String(100)) 

639 

640 accounts: Mapped[List[SavingsAccount]] = relationship( 

641 back_populates="owner", lazy="selectin" 

642 ) 

643 

644 @hybrid_property 

645 def balance(self) -> Decimal: 

646 return sum( 

647 (acc.balance for acc in self.accounts), start=Decimal("0") 

648 ) 

649 

650 @balance.inplace.expression 

651 @classmethod 

652 def _balance_expression(cls) -> SQLColumnExpression[Decimal]: 

653 return ( 

654 select(func.sum(SavingsAccount.balance)) 

655 .where(SavingsAccount.user_id == cls.id) 

656 .label("total_balance") 

657 ) 

658 

659The above recipe will give us the ``balance`` column which renders 

660a correlated SELECT: 

661 

662.. sourcecode:: pycon+sql 

663 

664 >>> from sqlalchemy import select 

665 >>> print(select(User).filter(User.balance > 400)) 

666 {printsql}SELECT "user".id, "user".name 

667 FROM "user" 

668 WHERE ( 

669 SELECT sum(account.balance) AS sum_1 FROM account 

670 WHERE account.user_id = "user".id 

671 ) > :param_1 

672 

673 

674.. _hybrid_custom_comparators: 

675 

676Building Custom Comparators 

677--------------------------- 

678 

679The hybrid property also includes a helper that allows construction of 

680custom comparators. A comparator object allows one to customize the 

681behavior of each SQLAlchemy expression operator individually. They 

682are useful when creating custom types that have some highly 

683idiosyncratic behavior on the SQL side. 

684 

685.. note:: The :meth:`.hybrid_property.comparator` decorator introduced 

686 in this section **replaces** the use of the 

687 :meth:`.hybrid_property.expression` decorator. 

688 They cannot be used together. 

689 

690The example class below allows case-insensitive comparisons on the attribute 

691named ``word_insensitive``:: 

692 

693 from __future__ import annotations 

694 

695 from typing import Any 

696 

697 from sqlalchemy import ColumnElement 

698 from sqlalchemy import func 

699 from sqlalchemy.ext.hybrid import Comparator 

700 from sqlalchemy.ext.hybrid import hybrid_property 

701 from sqlalchemy.orm import DeclarativeBase 

702 from sqlalchemy.orm import Mapped 

703 from sqlalchemy.orm import mapped_column 

704 

705 

706 class Base(DeclarativeBase): 

707 pass 

708 

709 

710 class CaseInsensitiveComparator(Comparator[str]): 

711 def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501 

712 return func.lower(self.__clause_element__()) == func.lower(other) 

713 

714 

715 class SearchWord(Base): 

716 __tablename__ = "searchword" 

717 

718 id: Mapped[int] = mapped_column(primary_key=True) 

719 word: Mapped[str] 

720 

721 @hybrid_property 

722 def word_insensitive(self) -> str: 

723 return self.word.lower() 

724 

725 @word_insensitive.inplace.comparator 

726 @classmethod 

727 def _word_insensitive_comparator(cls) -> CaseInsensitiveComparator: 

728 return CaseInsensitiveComparator(cls.word) 

729 

730Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()`` 

731SQL function to both sides: 

732 

733.. sourcecode:: pycon+sql 

734 

735 >>> from sqlalchemy import select 

736 >>> print(select(SearchWord).filter_by(word_insensitive="Trucks")) 

737 {printsql}SELECT searchword.id, searchword.word 

738 FROM searchword 

739 WHERE lower(searchword.word) = lower(:lower_1) 

740 

741 

742The ``CaseInsensitiveComparator`` above implements part of the 

743:class:`.ColumnOperators` interface. A "coercion" operation like 

744lowercasing can be applied to all comparison operations (i.e. ``eq``, 

745``lt``, ``gt``, etc.) using :meth:`.Operators.operate`:: 

746 

747 class CaseInsensitiveComparator(Comparator): 

748 def operate(self, op, other, **kwargs): 

749 return op( 

750 func.lower(self.__clause_element__()), 

751 func.lower(other), 

752 **kwargs, 

753 ) 

754 

755.. _hybrid_reuse_subclass: 

756 

757Reusing Hybrid Properties across Subclasses 

758------------------------------------------- 

759 

760A hybrid can be referred to from a superclass, to allow modifying 

761methods like :meth:`.hybrid_property.getter`, :meth:`.hybrid_property.setter` 

762to be used to redefine those methods on a subclass. This is similar to 

763how the standard Python ``@property`` object works:: 

764 

765 class FirstNameOnly(Base): 

766 # ... 

767 

768 first_name: Mapped[str] 

769 

770 @hybrid_property 

771 def name(self) -> str: 

772 return self.first_name 

773 

774 @name.inplace.setter 

775 def _name_setter(self, value: str) -> None: 

776 self.first_name = value 

777 

778 

779 class FirstNameLastName(FirstNameOnly): 

780 # ... 

781 

782 last_name: Mapped[str] 

783 

784 # 'inplace' is not used here; calling getter creates a copy 

785 # of FirstNameOnly.name that is local to FirstNameLastName 

786 @FirstNameOnly.name.getter 

787 def name(self) -> str: 

788 return self.first_name + " " + self.last_name 

789 

790 @name.inplace.setter 

791 def _name_setter(self, value: str) -> None: 

792 self.first_name, self.last_name = value.split(" ", 1) 

793 

794Above, the ``FirstNameLastName`` class refers to the hybrid from 

795``FirstNameOnly.name`` to repurpose its getter and setter for the subclass. 

796 

797When overriding :meth:`.hybrid_property.expression` and 

798:meth:`.hybrid_property.comparator` alone as the first reference to the 

799superclass, these names conflict with the same-named accessors on the class- 

800level :class:`.QueryableAttribute` object returned at the class level. To 

801override these methods when referring directly to the parent class descriptor, 

802add the special qualifier :attr:`.hybrid_property.overrides`, which will de- 

803reference the instrumented attribute back to the hybrid object:: 

804 

805 class FirstNameLastName(FirstNameOnly): 

806 # ... 

807 

808 last_name: Mapped[str] 

809 

810 @FirstNameOnly.name.overrides.expression 

811 @classmethod 

812 def name(cls): 

813 return func.concat(cls.first_name, " ", cls.last_name) 

814 

815.. _hybrid_value_objects: 

816 

817Hybrid Value Objects 

818-------------------- 

819 

820In the example shown previously at :ref:`hybrid_custom_comparators`, 

821if we were to compare the ``word_insensitive`` 

822attribute of a ``SearchWord`` instance to a plain Python string, the plain 

823Python string would not be coerced to lower case - the 

824``CaseInsensitiveComparator`` we built, being returned by 

825``@word_insensitive.comparator``, only applies to the SQL side. 

826 

827A more comprehensive form of the custom comparator is to construct a **Hybrid 

828Value Object**. This technique applies the target value or expression to a value 

829object which is then returned by the accessor in all cases. The value object 

830allows control of all operations upon the value as well as how compared values 

831are treated, both on the SQL expression side as well as the Python value side. 

832Replacing the previous ``CaseInsensitiveComparator`` class with a new 

833``CaseInsensitiveWord`` class:: 

834 

835 from sqlalchemy import func 

836 from sqlalchemy.ext.hybrid import Comparator 

837 

838 

839 class CaseInsensitiveWord(Comparator): 

840 "Hybrid value representing a lower case representation of a word." 

841 

842 def __init__(self, word): 

843 if isinstance(word, str): 

844 self.word = word.lower() 

845 else: 

846 self.word = func.lower(word) 

847 

848 def operate(self, op, other, **kwargs): 

849 if not isinstance(other, CaseInsensitiveWord): 

850 other = CaseInsensitiveWord(other) 

851 return op(self.word, other.word, **kwargs) 

852 

853 def __clause_element__(self): 

854 return self.word 

855 

856 def __str__(self): 

857 return self.word 

858 

859 key = "word" 

860 "Label to apply to Query tuple results" 

861 

862Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may 

863be a SQL function, or may be a Python native string. The hybrid value object should 

864implement ``__clause_element__()``, which allows the object to be coerced into 

865a SQL-capable value when used in SQL expression constructs, as well as Python 

866comparison methods such as ``__eq__()``, which is accomplished in the above 

867example by subclassing :class:`.hybrid.Comparator` and overriding the 

868``operate()`` method. 

869 

870.. topic:: Building the Value object with dataclasses 

871 

872 Hybrid value objects may also be implemented as Python dataclasses. If 

873 modification to values upon construction is needed, use the 

874 ``__post_init__()`` dataclasses method. Instance variables that work in 

875 a "hybrid" fashion may be instance of a plain Python value, or an instance 

876 of :class:`.SQLColumnExpression` genericized against that type. Also make sure to disable 

877 dataclass comparison features, as the :class:`.hybrid.Comparator` class 

878 provides these:: 

879 

880 from sqlalchemy import func 

881 from sqlalchemy.ext.hybrid import Comparator 

882 from dataclasses import dataclass 

883 

884 

885 @dataclass(eq=False) 

886 class CaseInsensitiveWord(Comparator): 

887 word: str | SQLColumnExpression[str] 

888 

889 def __post_init__(self): 

890 if isinstance(self.word, str): 

891 self.word = self.word.lower() 

892 else: 

893 self.word = func.lower(self.word) 

894 

895 def operate(self, op, other, **kwargs): 

896 if not isinstance(other, CaseInsensitiveWord): 

897 other = CaseInsensitiveWord(other) 

898 return op(self.word, other.word, **kwargs) 

899 

900 def __clause_element__(self): 

901 return self.word 

902 

903With ``__clause_element__()`` provided, our ``SearchWord`` class 

904can now deliver the ``CaseInsensitiveWord`` object unconditionally from a 

905single hybrid method, returning an object that behaves appropriately 

906in both value-based and SQL contexts:: 

907 

908 class SearchWord(Base): 

909 __tablename__ = "searchword" 

910 id: Mapped[int] = mapped_column(primary_key=True) 

911 word: Mapped[str] 

912 

913 @hybrid_property 

914 def word_insensitive(self) -> CaseInsensitiveWord: 

915 return CaseInsensitiveWord(self.word) 

916 

917The class-level version of ``CaseInsensitiveWord`` will work in SQL 

918constructs: 

919 

920.. sourcecode:: pycon+sql 

921 

922 >>> print(select(SearchWord).filter(SearchWord.word_insensitive == "Trucks")) 

923 {printsql}SELECT searchword.id AS searchword_id, searchword.word AS searchword_word 

924 FROM searchword 

925 WHERE lower(searchword.word) = :lower_1 

926 

927By also subclassing :class:`.hybrid.Comparator` and providing an implementation 

928for ``operate()``, the ``word_insensitive`` attribute also has case-insensitive 

929comparison behavior universally, including SQL expression and Python expression 

930(note the Python value is converted to lower case on the Python side here): 

931 

932.. sourcecode:: pycon+sql 

933 

934 >>> from sqlalchemy.orm import aliased 

935 >>> sw1 = aliased(SearchWord) 

936 >>> sw2 = aliased(SearchWord) 

937 >>> print( 

938 ... select(sw1.word_insensitive, sw2.word_insensitive).filter( 

939 ... sw1.word_insensitive > sw2.word_insensitive 

940 ... ) 

941 ... ) 

942 {printsql}SELECT lower(searchword_1.word) AS lower_1, 

943 lower(searchword_2.word) AS lower_2 

944 FROM searchword AS searchword_1, searchword AS searchword_2 

945 WHERE lower(searchword_1.word) > lower(searchword_2.word) 

946 

947Python only expression:: 

948 

949 >>> ws1 = SearchWord(word="SomeWord") 

950 >>> ws1.word_insensitive == "sOmEwOrD" 

951 True 

952 >>> ws1.word_insensitive == "XOmEwOrX" 

953 False 

954 >>> print(ws1.word_insensitive) 

955 someword 

956 

957The Hybrid Value pattern is very useful for any kind of value that may have 

958multiple representations, such as timestamps, time deltas, units of 

959measurement, currencies and encrypted passwords. 

960 

961.. seealso:: 

962 

963 `Hybrids and Value Agnostic Types 

964 <https://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_ 

965 - on the techspot.zzzeek.org blog 

966 

967 `Value Agnostic Types, Part II 

968 <https://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ - 

969 on the techspot.zzzeek.org blog 

970 

971.. _composite_hybrid_value_objects: 

972 

973Composite Hybrid Value Objects 

974^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

975 

976The functionality of :ref:`hybrid_value_objects` may also be expanded to 

977support "composite" forms; in this pattern, SQLAlchemy hybrids begin to 

978approximate most (though not all) the same functionality that is available from 

979the ORM natively via the :ref:`mapper_composite` feature. We can imitate the 

980example of ``Point`` and ``Vertex`` from that section using hybrids, where 

981``Point`` is modified to become a Hybrid Value Object:: 

982 

983 from dataclasses import dataclass 

984 

985 from sqlalchemy import tuple_ 

986 from sqlalchemy.ext.hybrid import Comparator 

987 from sqlalchemy import SQLColumnExpression 

988 

989 

990 @dataclass(eq=False) 

991 class Point(Comparator): 

992 x: int | SQLColumnExpression[int] 

993 y: int | SQLColumnExpression[int] 

994 

995 def operate(self, op, other, **kwargs): 

996 return op(self.x, other.x) & op(self.y, other.y) 

997 

998 def __clause_element__(self): 

999 return tuple_(self.x, self.y) 

1000 

1001Above, the ``operate()`` method is where the most "hybrid" behavior takes 

1002place, making use of ``op()`` (the Python operator function in use) along 

1003with the the bitwise ``&`` operator provides us with the SQL AND operator 

1004in a SQL context, and boolean "and" in a Python boolean context. 

1005 

1006Following from there, the owning ``Vertex`` class now uses hybrids to 

1007represent ``start`` and ``end``:: 

1008 

1009 from sqlalchemy.orm import DeclarativeBase, Mapped 

1010 from sqlalchemy.orm import mapped_column 

1011 from sqlalchemy.ext.hybrid import hybrid_property 

1012 

1013 

1014 class Base(DeclarativeBase): 

1015 pass 

1016 

1017 

1018 class Vertex(Base): 

1019 __tablename__ = "vertices" 

1020 

1021 id: Mapped[int] = mapped_column(primary_key=True) 

1022 

1023 x1: Mapped[int] 

1024 y1: Mapped[int] 

1025 x2: Mapped[int] 

1026 y2: Mapped[int] 

1027 

1028 @hybrid_property 

1029 def start(self) -> Point: 

1030 return Point(self.x1, self.y1) 

1031 

1032 @start.inplace.setter 

1033 def _set_start(self, value: Point) -> None: 

1034 self.x1 = value.x 

1035 self.y1 = value.y 

1036 

1037 @hybrid_property 

1038 def end(self) -> Point: 

1039 return Point(self.x2, self.y2) 

1040 

1041 @end.inplace.setter 

1042 def _set_end(self, value: Point) -> None: 

1043 self.x2 = value.x 

1044 self.y2 = value.y 

1045 

1046 def __repr__(self) -> str: 

1047 return f"Vertex(start={self.start}, end={self.end})" 

1048 

1049Using the above mapping, we can use expressions at the Python or SQL level 

1050using ``Vertex.start`` and ``Vertex.end``:: 

1051 

1052 >>> v1 = Vertex(start=Point(3, 4), end=Point(15, 10)) 

1053 >>> v1.end == Point(15, 10) 

1054 True 

1055 >>> stmt = ( 

1056 ... select(Vertex) 

1057 ... .where(Vertex.start == Point(3, 4)) 

1058 ... .where(Vertex.end < Point(7, 8)) 

1059 ... ) 

1060 >>> print(stmt) 

1061 SELECT vertices.id, vertices.x1, vertices.y1, vertices.x2, vertices.y2 

1062 FROM vertices 

1063 WHERE vertices.x1 = :x1_1 AND vertices.y1 = :y1_1 AND vertices.x2 < :x2_1 AND vertices.y2 < :y2_1 

1064 

1065DML Support for Composite Value Objects 

1066~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

1067 

1068Composite value objects like ``Point`` can also be used with the ORM's 

1069DML features. The :meth:`.hybrid_property.update_expression` decorator allows 

1070the hybrid to expand a composite value into multiple column assignments 

1071in UPDATE and INSERT statements:: 

1072 

1073 class Location(Base): 

1074 __tablename__ = "location" 

1075 

1076 id: Mapped[int] = mapped_column(primary_key=True) 

1077 x: Mapped[int] 

1078 y: Mapped[int] 

1079 

1080 @hybrid_property 

1081 def coordinates(self) -> Point: 

1082 return Point(self.x, self.y) 

1083 

1084 @coordinates.inplace.update_expression 

1085 @classmethod 

1086 def _coordinates_update_expression( 

1087 cls, value: Any 

1088 ) -> List[Tuple[Any, Any]]: 

1089 assert isinstance(value, Point) 

1090 return [(cls.x, value.x), (cls.y, value.y)] 

1091 

1092This allows UPDATE statements to work with the composite value: 

1093 

1094.. sourcecode:: pycon+sql 

1095 

1096 >>> from sqlalchemy import update 

1097 >>> print( 

1098 ... update(Location) 

1099 ... .where(Location.id == 5) 

1100 ... .values({Location.coordinates: Point(25, 17)}) 

1101 ... ) 

1102 {printsql}UPDATE location SET x=:x, y=:y WHERE location.id = :id_1 

1103 

1104For bulk operations that use parameter dictionaries, the 

1105:meth:`.hybrid_property.bulk_dml` decorator provides a hook to 

1106convert composite values into individual column values:: 

1107 

1108 from typing import MutableMapping 

1109 

1110 

1111 class Location(Base): 

1112 # ... (same as above) 

1113 

1114 @coordinates.inplace.bulk_dml 

1115 @classmethod 

1116 def _coordinates_bulk_dml( 

1117 cls, mapping: MutableMapping[str, Any], value: Point 

1118 ) -> None: 

1119 mapping["x"] = value.x 

1120 mapping["y"] = value.y 

1121 

1122This enables bulk operations with composite values:: 

1123 

1124 # Bulk INSERT 

1125 session.execute( 

1126 insert(Location), 

1127 [ 

1128 {"id": 1, "coordinates": Point(10, 20)}, 

1129 {"id": 2, "coordinates": Point(30, 40)}, 

1130 ], 

1131 ) 

1132 

1133 # Bulk UPDATE 

1134 session.execute( 

1135 update(Location), 

1136 [ 

1137 {"id": 1, "coordinates": Point(15, 25)}, 

1138 {"id": 2, "coordinates": Point(35, 45)}, 

1139 ], 

1140 ) 

1141 

1142""" # noqa 

1143 

1144from __future__ import annotations 

1145 

1146from typing import Any 

1147from typing import Callable 

1148from typing import cast 

1149from typing import Concatenate 

1150from typing import Generic 

1151from typing import List 

1152from typing import Literal 

1153from typing import MutableMapping 

1154from typing import Optional 

1155from typing import overload 

1156from typing import ParamSpec 

1157from typing import Protocol 

1158from typing import Sequence 

1159from typing import Tuple 

1160from typing import Type 

1161from typing import TYPE_CHECKING 

1162from typing import TypeVar 

1163from typing import Union 

1164 

1165from .. import exc 

1166from .. import util 

1167from ..orm import attributes 

1168from ..orm import InspectionAttrExtensionType 

1169from ..orm import interfaces 

1170from ..orm import ORMDescriptor 

1171from ..orm.attributes import QueryableAttribute 

1172from ..sql import roles 

1173from ..sql._typing import is_has_clause_element 

1174from ..sql.elements import ColumnElement 

1175from ..sql.elements import SQLCoreOperations 

1176from ..util.typing import Self 

1177 

1178if TYPE_CHECKING: 

1179 from ..orm.interfaces import MapperProperty 

1180 from ..orm.util import AliasedInsp 

1181 from ..sql import SQLColumnExpression 

1182 from ..sql._typing import _ColumnExpressionArgument 

1183 from ..sql._typing import _DMLColumnArgument 

1184 from ..sql._typing import _HasClauseElement 

1185 from ..sql._typing import _InfoType 

1186 from ..sql.operators import OperatorType 

1187 

1188_P = ParamSpec("_P") 

1189_R = TypeVar("_R") 

1190_T = TypeVar("_T", bound=Any) 

1191_TE = TypeVar("_TE", bound=Any) 

1192_T_co = TypeVar("_T_co", bound=Any, covariant=True) 

1193_T_con = TypeVar("_T_con", bound=Any, contravariant=True) 

1194 

1195 

1196class HybridExtensionType(InspectionAttrExtensionType): 

1197 HYBRID_METHOD = "HYBRID_METHOD" 

1198 """Symbol indicating an :class:`InspectionAttr` that's 

1199 of type :class:`.hybrid_method`. 

1200 

1201 Is assigned to the :attr:`.InspectionAttr.extension_type` 

1202 attribute. 

1203 

1204 .. seealso:: 

1205 

1206 :attr:`_orm.Mapper.all_orm_attributes` 

1207 

1208 """ 

1209 

1210 HYBRID_PROPERTY = "HYBRID_PROPERTY" 

1211 """Symbol indicating an :class:`InspectionAttr` that's 

1212 of type :class:`.hybrid_method`. 

1213 

1214 Is assigned to the :attr:`.InspectionAttr.extension_type` 

1215 attribute. 

1216 

1217 .. seealso:: 

1218 

1219 :attr:`_orm.Mapper.all_orm_attributes` 

1220 

1221 """ 

1222 

1223 

1224class _HybridGetterType(Protocol[_T_co]): 

1225 def __call__(s, self: Any, /) -> _T_co: ... 

1226 

1227 

1228class _HybridSetterType(Protocol[_T_con]): 

1229 def __call__(s, self: Any, value: _T_con, /) -> None: ... 

1230 

1231 

1232class _HybridUpdaterType(Protocol[_T_con]): 

1233 def __call__( 

1234 s, 

1235 cls: Any, 

1236 value: Union[_T_con, _ColumnExpressionArgument[_T_con]], 

1237 ) -> List[Tuple[_DMLColumnArgument, Any]]: ... 

1238 

1239 

1240class _HybridBulkDMLType(Protocol[_T_co]): 

1241 def __call__( 

1242 s, 

1243 cls: Any, 

1244 mapping: MutableMapping[str, Any], 

1245 value: Any, 

1246 ) -> Any: ... 

1247 

1248 

1249class _HybridDeleterType(Protocol[_T_co]): 

1250 def __call__(s, self: Any, /) -> None: ... 

1251 

1252 

1253class _HybridExprCallableType(Protocol[_T_co]): 

1254 def __call__( 

1255 s, cls: Any, / 

1256 ) -> Union[_HasClauseElement[_T_co], SQLColumnExpression[_T_co]]: ... 

1257 

1258 

1259class _HybridComparatorCallableType(Protocol[_T]): 

1260 def __call__(self, cls: Any) -> Comparator[_T]: ... 

1261 

1262 

1263class _HybridClassLevelAccessor(QueryableAttribute[_T]): 

1264 """Describe the object returned by a hybrid_property() when 

1265 called as a class-level descriptor. 

1266 

1267 """ 

1268 

1269 if TYPE_CHECKING: 

1270 

1271 def getter( 

1272 self, fget: _HybridGetterType[_T] 

1273 ) -> hybrid_property[_T]: ... 

1274 

1275 def setter( 

1276 self, fset: _HybridSetterType[_T] 

1277 ) -> hybrid_property[_T]: ... 

1278 

1279 def deleter( 

1280 self, fdel: _HybridDeleterType[_T] 

1281 ) -> hybrid_property[_T]: ... 

1282 

1283 @property 

1284 def overrides(self) -> hybrid_property[_T]: ... 

1285 

1286 def update_expression( 

1287 self, meth: _HybridUpdaterType[_T] 

1288 ) -> hybrid_property[_T]: ... 

1289 

1290 def bulk_dml( 

1291 self, meth: _HybridBulkDMLType[_T] 

1292 ) -> hybrid_property[_T]: ... 

1293 

1294 

1295class hybrid_method(interfaces.InspectionAttrInfo, Generic[_P, _R]): 

1296 """A decorator which allows definition of a Python object method with both 

1297 instance-level and class-level behavior. 

1298 

1299 """ 

1300 

1301 is_attribute = True 

1302 extension_type = HybridExtensionType.HYBRID_METHOD 

1303 

1304 def __init__( 

1305 self, 

1306 func: Callable[Concatenate[Any, _P], _R], 

1307 expr: Optional[ 

1308 Callable[Concatenate[Any, _P], SQLCoreOperations[_R]] 

1309 ] = None, 

1310 ): 

1311 """Create a new :class:`.hybrid_method`. 

1312 

1313 Usage is typically via decorator:: 

1314 

1315 from sqlalchemy.ext.hybrid import hybrid_method 

1316 

1317 

1318 class SomeClass: 

1319 @hybrid_method 

1320 def value(self, x, y): 

1321 return self._value + x + y 

1322 

1323 @value.expression 

1324 @classmethod 

1325 def value(cls, x, y): 

1326 return func.some_function(cls._value, x, y) 

1327 

1328 """ 

1329 self.func = func 

1330 if expr is not None: 

1331 self.expression(expr) 

1332 else: 

1333 self.expression(func) # type: ignore[arg-type] 

1334 

1335 @property 

1336 def inplace(self) -> Self: 

1337 """Return the inplace mutator for this :class:`.hybrid_method`. 

1338 

1339 The :class:`.hybrid_method` class already performs "in place" mutation 

1340 when the :meth:`.hybrid_method.expression` decorator is called, 

1341 so this attribute returns Self. 

1342 

1343 .. versionadded:: 2.0.4 

1344 

1345 .. seealso:: 

1346 

1347 :ref:`hybrid_pep484_naming` 

1348 

1349 """ 

1350 return self 

1351 

1352 @overload 

1353 def __get__( 

1354 self, instance: Literal[None], owner: Type[object] 

1355 ) -> Callable[_P, SQLCoreOperations[_R]]: ... 

1356 

1357 @overload 

1358 def __get__( 

1359 self, instance: object, owner: Type[object] 

1360 ) -> Callable[_P, _R]: ... 

1361 

1362 def __get__( 

1363 self, instance: Optional[object], owner: Type[object] 

1364 ) -> Union[Callable[_P, _R], Callable[_P, SQLCoreOperations[_R]]]: 

1365 if instance is None: 

1366 return self.expr.__get__(owner, owner) # type: ignore[no-any-return] # noqa: E501 

1367 else: 

1368 return self.func.__get__(instance, owner) # type: ignore[no-any-return] # noqa: E501 

1369 

1370 def expression( 

1371 self, expr: Callable[Concatenate[Any, _P], SQLCoreOperations[_R]] 

1372 ) -> hybrid_method[_P, _R]: 

1373 """Provide a modifying decorator that defines a 

1374 SQL-expression producing method.""" 

1375 

1376 self.expr = expr 

1377 if not self.expr.__doc__: 

1378 self.expr.__doc__ = self.func.__doc__ 

1379 return self 

1380 

1381 

1382def _unwrap_classmethod(meth: _T) -> _T: 

1383 if isinstance(meth, classmethod): 

1384 return meth.__func__ # type: ignore[return-value] 

1385 else: 

1386 return meth 

1387 

1388 

1389class hybrid_property(interfaces.InspectionAttrInfo, ORMDescriptor[_T]): 

1390 """A decorator which allows definition of a Python descriptor with both 

1391 instance-level and class-level behavior. 

1392 

1393 """ 

1394 

1395 is_attribute = True 

1396 extension_type = HybridExtensionType.HYBRID_PROPERTY 

1397 

1398 __name__: str 

1399 

1400 def __init__( 

1401 self, 

1402 fget: _HybridGetterType[_T], 

1403 fset: Optional[_HybridSetterType[_T]] = None, 

1404 fdel: Optional[_HybridDeleterType[_T]] = None, 

1405 expr: Optional[_HybridExprCallableType[_T]] = None, 

1406 custom_comparator: Optional[Comparator[_T]] = None, 

1407 update_expr: Optional[_HybridUpdaterType[_T]] = None, 

1408 bulk_dml_setter: Optional[_HybridBulkDMLType[_T]] = None, 

1409 ): 

1410 """Create a new :class:`.hybrid_property`. 

1411 

1412 Usage is typically via decorator:: 

1413 

1414 from sqlalchemy.ext.hybrid import hybrid_property 

1415 

1416 

1417 class SomeClass: 

1418 @hybrid_property 

1419 def value(self): 

1420 return self._value 

1421 

1422 @value.setter 

1423 def value(self, value): 

1424 self._value = value 

1425 

1426 """ 

1427 self.fget = fget 

1428 self.fset = fset 

1429 self.fdel = fdel 

1430 self.expr = _unwrap_classmethod(expr) 

1431 self.custom_comparator = _unwrap_classmethod(custom_comparator) 

1432 self.update_expr = _unwrap_classmethod(update_expr) 

1433 self.bulk_dml_setter = _unwrap_classmethod(bulk_dml_setter) 

1434 util.update_wrapper(self, fget) # type: ignore[arg-type] 

1435 

1436 @overload 

1437 def __get__(self, instance: Any, owner: Literal[None]) -> Self: ... 

1438 

1439 @overload 

1440 def __get__( 

1441 self, instance: Literal[None], owner: Type[object] 

1442 ) -> _HybridClassLevelAccessor[_T]: ... 

1443 

1444 @overload 

1445 def __get__(self, instance: object, owner: Type[object]) -> _T: ... 

1446 

1447 def __get__( 

1448 self, instance: Optional[object], owner: Optional[Type[object]] 

1449 ) -> Union[hybrid_property[_T], _HybridClassLevelAccessor[_T], _T]: 

1450 if owner is None: 

1451 return self 

1452 elif instance is None: 

1453 return self._expr_comparator(owner) 

1454 else: 

1455 return self.fget(instance) 

1456 

1457 def __set__( 

1458 self, instance: object, value: Union[SQLCoreOperations[_T], _T] 

1459 ) -> None: 

1460 if self.fset is None: 

1461 raise AttributeError("can't set attribute") 

1462 self.fset(instance, value) # type: ignore[arg-type] 

1463 

1464 def __delete__(self, instance: object) -> None: 

1465 if self.fdel is None: 

1466 raise AttributeError("can't delete attribute") 

1467 self.fdel(instance) 

1468 

1469 def _copy(self, **kw: Any) -> hybrid_property[_T]: 

1470 defaults = { 

1471 key: value 

1472 for key, value in self.__dict__.items() 

1473 if not key.startswith("_") 

1474 } 

1475 defaults.update(**kw) 

1476 return type(self)(**defaults) 

1477 

1478 @property 

1479 def overrides(self) -> Self: 

1480 """Prefix for a method that is overriding an existing attribute. 

1481 

1482 The :attr:`.hybrid_property.overrides` accessor just returns 

1483 this hybrid object, which when called at the class level from 

1484 a parent class, will de-reference the "instrumented attribute" 

1485 normally returned at this level, and allow modifying decorators 

1486 like :meth:`.hybrid_property.expression` and 

1487 :meth:`.hybrid_property.comparator` 

1488 to be used without conflicting with the same-named attributes 

1489 normally present on the :class:`.QueryableAttribute`:: 

1490 

1491 class SuperClass: 

1492 # ... 

1493 

1494 @hybrid_property 

1495 def foobar(self): 

1496 return self._foobar 

1497 

1498 

1499 class SubClass(SuperClass): 

1500 # ... 

1501 

1502 @SuperClass.foobar.overrides.expression 

1503 def foobar(cls): 

1504 return func.subfoobar(self._foobar) 

1505 

1506 .. seealso:: 

1507 

1508 :ref:`hybrid_reuse_subclass` 

1509 

1510 """ 

1511 return self 

1512 

1513 class _InPlace(Generic[_TE]): 

1514 """A builder helper for .hybrid_property. 

1515 

1516 .. versionadded:: 2.0.4 

1517 

1518 """ 

1519 

1520 __slots__ = ("attr",) 

1521 

1522 def __init__(self, attr: hybrid_property[_TE]): 

1523 self.attr = attr 

1524 

1525 def _set(self, **kw: Any) -> hybrid_property[_TE]: 

1526 for k, v in kw.items(): 

1527 setattr(self.attr, k, _unwrap_classmethod(v)) 

1528 return self.attr 

1529 

1530 def getter(self, fget: _HybridGetterType[_TE]) -> hybrid_property[_TE]: 

1531 return self._set(fget=fget) 

1532 

1533 def setter(self, fset: _HybridSetterType[_TE]) -> hybrid_property[_TE]: 

1534 return self._set(fset=fset) 

1535 

1536 def deleter( 

1537 self, fdel: _HybridDeleterType[_TE] 

1538 ) -> hybrid_property[_TE]: 

1539 return self._set(fdel=fdel) 

1540 

1541 def expression( 

1542 self, expr: _HybridExprCallableType[_TE] 

1543 ) -> hybrid_property[_TE]: 

1544 return self._set(expr=expr) 

1545 

1546 def comparator( 

1547 self, comparator: _HybridComparatorCallableType[_TE] 

1548 ) -> hybrid_property[_TE]: 

1549 return self._set(custom_comparator=comparator) 

1550 

1551 def update_expression( 

1552 self, meth: _HybridUpdaterType[_TE] 

1553 ) -> hybrid_property[_TE]: 

1554 return self._set(update_expr=meth) 

1555 

1556 def bulk_dml( 

1557 self, meth: _HybridBulkDMLType[_TE] 

1558 ) -> hybrid_property[_TE]: 

1559 return self._set(bulk_dml_setter=meth) 

1560 

1561 @property 

1562 def inplace(self) -> _InPlace[_T]: 

1563 """Return the inplace mutator for this :class:`.hybrid_property`. 

1564 

1565 This is to allow in-place mutation of the hybrid, allowing the first 

1566 hybrid method of a certain name to be reused in order to add 

1567 more methods without having to name those methods the same, e.g.:: 

1568 

1569 class Interval(Base): 

1570 # ... 

1571 

1572 @hybrid_property 

1573 def radius(self) -> float: 

1574 return abs(self.length) / 2 

1575 

1576 @radius.inplace.setter 

1577 def _radius_setter(self, value: float) -> None: 

1578 self.length = value * 2 

1579 

1580 @radius.inplace.expression 

1581 def _radius_expression(cls) -> ColumnElement[float]: 

1582 return type_coerce(func.abs(cls.length) / 2, Float) 

1583 

1584 .. versionadded:: 2.0.4 

1585 

1586 .. seealso:: 

1587 

1588 :ref:`hybrid_pep484_naming` 

1589 

1590 """ 

1591 return hybrid_property._InPlace(self) 

1592 

1593 def getter(self, fget: _HybridGetterType[_T]) -> hybrid_property[_T]: 

1594 """Provide a modifying decorator that defines a getter method.""" 

1595 

1596 return self._copy(fget=fget) 

1597 

1598 def setter(self, fset: _HybridSetterType[_T]) -> hybrid_property[_T]: 

1599 """Provide a modifying decorator that defines a setter method.""" 

1600 

1601 return self._copy(fset=fset) 

1602 

1603 def deleter(self, fdel: _HybridDeleterType[_T]) -> hybrid_property[_T]: 

1604 """Provide a modifying decorator that defines a deletion method.""" 

1605 

1606 return self._copy(fdel=fdel) 

1607 

1608 def expression( 

1609 self, expr: _HybridExprCallableType[_T] 

1610 ) -> hybrid_property[_T]: 

1611 """Provide a modifying decorator that defines a SQL-expression 

1612 producing method. 

1613 

1614 When a hybrid is invoked at the class level, the SQL expression given 

1615 here is wrapped inside of a specialized :class:`.QueryableAttribute`, 

1616 which is the same kind of object used by the ORM to represent other 

1617 mapped attributes. The reason for this is so that other class-level 

1618 attributes such as docstrings and a reference to the hybrid itself may 

1619 be maintained within the structure that's returned, without any 

1620 modifications to the original SQL expression passed in. 

1621 

1622 .. note:: 

1623 

1624 When referring to a hybrid property from an owning class (e.g. 

1625 ``SomeClass.some_hybrid``), an instance of 

1626 :class:`.QueryableAttribute` is returned, representing the 

1627 expression or comparator object as well as this hybrid object. 

1628 However, that object itself has accessors called ``expression`` and 

1629 ``comparator``; so when attempting to override these decorators on a 

1630 subclass, it may be necessary to qualify it using the 

1631 :attr:`.hybrid_property.overrides` modifier first. See that 

1632 modifier for details. 

1633 

1634 .. seealso:: 

1635 

1636 :ref:`hybrid_distinct_expression` 

1637 

1638 """ 

1639 

1640 return self._copy(expr=expr) 

1641 

1642 def comparator( 

1643 self, comparator: _HybridComparatorCallableType[_T] 

1644 ) -> hybrid_property[_T]: 

1645 """Provide a modifying decorator that defines a custom 

1646 comparator producing method. 

1647 

1648 The return value of the decorated method should be an instance of 

1649 :class:`~.hybrid.Comparator`. 

1650 

1651 .. note:: The :meth:`.hybrid_property.comparator` decorator 

1652 **replaces** the use of the :meth:`.hybrid_property.expression` 

1653 decorator. They cannot be used together. 

1654 

1655 When a hybrid is invoked at the class level, the 

1656 :class:`~.hybrid.Comparator` object given here is wrapped inside of a 

1657 specialized :class:`.QueryableAttribute`, which is the same kind of 

1658 object used by the ORM to represent other mapped attributes. The 

1659 reason for this is so that other class-level attributes such as 

1660 docstrings and a reference to the hybrid itself may be maintained 

1661 within the structure that's returned, without any modifications to the 

1662 original comparator object passed in. 

1663 

1664 .. note:: 

1665 

1666 When referring to a hybrid property from an owning class (e.g. 

1667 ``SomeClass.some_hybrid``), an instance of 

1668 :class:`.QueryableAttribute` is returned, representing the 

1669 expression or comparator object as this hybrid object. However, 

1670 that object itself has accessors called ``expression`` and 

1671 ``comparator``; so when attempting to override these decorators on a 

1672 subclass, it may be necessary to qualify it using the 

1673 :attr:`.hybrid_property.overrides` modifier first. See that 

1674 modifier for details. 

1675 

1676 """ 

1677 return self._copy(custom_comparator=comparator) 

1678 

1679 def update_expression( 

1680 self, meth: _HybridUpdaterType[_T] 

1681 ) -> hybrid_property[_T]: 

1682 """Provide a modifying decorator that defines an UPDATE tuple 

1683 producing method. 

1684 

1685 The method accepts a single value, which is the value to be 

1686 rendered into the SET clause of an UPDATE statement. The method 

1687 should then process this value into individual column expressions 

1688 that fit into the ultimate SET clause, and return them as a 

1689 sequence of 2-tuples. Each tuple 

1690 contains a column expression as the key and a value to be rendered. 

1691 

1692 E.g.:: 

1693 

1694 class Person(Base): 

1695 # ... 

1696 

1697 first_name = Column(String) 

1698 last_name = Column(String) 

1699 

1700 @hybrid_property 

1701 def fullname(self): 

1702 return first_name + " " + last_name 

1703 

1704 @fullname.update_expression 

1705 def fullname(cls, value): 

1706 fname, lname = value.split(" ", 1) 

1707 return [(cls.first_name, fname), (cls.last_name, lname)] 

1708 

1709 """ 

1710 return self._copy(update_expr=meth) 

1711 

1712 def bulk_dml(self, meth: _HybridBulkDMLType[_T]) -> hybrid_property[_T]: 

1713 """Define a setter for bulk dml. 

1714 

1715 .. versionadded:: 2.1 

1716 

1717 """ 

1718 return self._copy(bulk_dml=meth) 

1719 

1720 @util.memoized_property 

1721 def _expr_comparator( 

1722 self, 

1723 ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]: 

1724 if self.custom_comparator is not None: 

1725 return self._get_comparator(self.custom_comparator) 

1726 elif self.expr is not None: 

1727 return self._get_expr(self.expr) 

1728 else: 

1729 return self._get_expr(cast(_HybridExprCallableType[_T], self.fget)) 

1730 

1731 def _get_expr( 

1732 self, expr: _HybridExprCallableType[_T] 

1733 ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]: 

1734 def _expr(cls: Any) -> ExprComparator[_T]: 

1735 return ExprComparator(cls, expr(cls), self) 

1736 

1737 util.update_wrapper(_expr, expr) 

1738 

1739 return self._get_comparator(_expr) 

1740 

1741 def _get_comparator( 

1742 self, comparator: Any 

1743 ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]: 

1744 proxy_attr = attributes._create_proxied_attribute(self) 

1745 

1746 def expr_comparator( 

1747 owner: Type[object], 

1748 ) -> _HybridClassLevelAccessor[_T]: 

1749 # because this is the descriptor protocol, we don't really know 

1750 # what our attribute name is. so search for it through the 

1751 # MRO. 

1752 for lookup in owner.__mro__: 

1753 if self.__name__ in lookup.__dict__: 

1754 if lookup.__dict__[self.__name__] is self: 

1755 name = self.__name__ 

1756 break 

1757 else: 

1758 name = attributes._UNKNOWN_ATTR_KEY # type: ignore[assignment,unused-ignore] # noqa: E501 

1759 

1760 return cast( 

1761 "_HybridClassLevelAccessor[_T]", 

1762 proxy_attr( 

1763 owner, 

1764 name, 

1765 self, 

1766 comparator(owner), 

1767 doc=comparator.__doc__ or self.__doc__, 

1768 ), 

1769 ) 

1770 

1771 return expr_comparator 

1772 

1773 

1774class Comparator(interfaces.PropComparator[_T]): 

1775 """A helper class that allows easy construction of custom 

1776 :class:`~.orm.interfaces.PropComparator` 

1777 classes for usage with hybrids.""" 

1778 

1779 def __init__( 

1780 self, expression: Union[_HasClauseElement[_T], SQLColumnExpression[_T]] 

1781 ): 

1782 self.expression = expression 

1783 

1784 def __clause_element__(self) -> roles.ColumnsClauseRole: 

1785 expr = self.expression 

1786 if is_has_clause_element(expr): 

1787 ret_expr = expr.__clause_element__() 

1788 else: 

1789 if TYPE_CHECKING: 

1790 assert isinstance(expr, ColumnElement) 

1791 ret_expr = expr 

1792 

1793 if TYPE_CHECKING: 

1794 # see test_hybrid->test_expression_isnt_clause_element 

1795 # that exercises the usual place this is caught if not 

1796 # true 

1797 assert isinstance(ret_expr, ColumnElement) 

1798 return ret_expr 

1799 

1800 @util.non_memoized_property 

1801 def property(self) -> interfaces.MapperProperty[_T]: 

1802 raise NotImplementedError() 

1803 

1804 def adapt_to_entity( 

1805 self, adapt_to_entity: AliasedInsp[Any] 

1806 ) -> Comparator[_T]: 

1807 # interesting.... 

1808 return self 

1809 

1810 

1811class ExprComparator(Comparator[_T]): 

1812 def __init__( 

1813 self, 

1814 cls: Type[Any], 

1815 expression: Union[_HasClauseElement[_T], SQLColumnExpression[_T]], 

1816 hybrid: hybrid_property[_T], 

1817 ): 

1818 self.cls = cls 

1819 self.expression = expression 

1820 self.hybrid = hybrid 

1821 

1822 def __getattr__(self, key: str) -> Any: 

1823 return getattr(self.expression, key) 

1824 

1825 @util.ro_non_memoized_property 

1826 def info(self) -> _InfoType: 

1827 return self.hybrid.info 

1828 

1829 def _bulk_update_tuples( 

1830 self, 

1831 value: Any, 

1832 ) -> Sequence[Tuple[_DMLColumnArgument, Any]]: 

1833 if isinstance(self.expression, attributes.QueryableAttribute): 

1834 return self.expression._bulk_update_tuples(value) 

1835 elif self.hybrid.update_expr is not None: 

1836 return self.hybrid.update_expr(self.cls, value) 

1837 else: 

1838 return [(self.expression, value)] 

1839 

1840 def _bulk_dml_setter(self, key: str) -> Optional[Callable[..., Any]]: 

1841 """return a callable that will process a bulk INSERT value""" 

1842 

1843 meth = None 

1844 

1845 def prop(mapping: MutableMapping[str, Any]) -> None: 

1846 nonlocal meth 

1847 value = mapping[key] 

1848 

1849 if meth is None: 

1850 if self.hybrid.bulk_dml_setter is None: 

1851 raise exc.InvalidRequestError( 

1852 "Can't evaluate bulk DML statement; please " 

1853 "supply a bulk_dml decorated function" 

1854 ) 

1855 

1856 meth = self.hybrid.bulk_dml_setter 

1857 

1858 meth(self.cls, mapping, value) 

1859 

1860 return prop 

1861 

1862 @util.non_memoized_property 

1863 def property(self) -> MapperProperty[_T]: 

1864 # this accessor is not normally used, however is accessed by things 

1865 # like ORM synonyms if the hybrid is used in this context; the 

1866 # .property attribute is not necessarily accessible 

1867 return self.expression.property # type: ignore[no-any-return, union-attr] # noqa: E501 

1868 

1869 def operate( 

1870 self, op: OperatorType, *other: Any, **kwargs: Any 

1871 ) -> ColumnElement[Any]: 

1872 return op(self.expression, *other, **kwargs) 

1873 

1874 def reverse_operate( 

1875 self, op: OperatorType, other: Any, **kwargs: Any 

1876 ) -> ColumnElement[Any]: 

1877 return op(other, self.expression, **kwargs) # type: ignore[no-any-return] # noqa: E501