Skip to content

Commit f5a474f

Browse files
committed
✨ Add support for hybrid_property
1 parent 75ce455 commit f5a474f

File tree

3 files changed

+59
-2
lines changed

3 files changed

+59
-2
lines changed

sqlmodel/main.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from sqlalchemy import Boolean, Column, Date, DateTime
3535
from sqlalchemy import Enum as sa_Enum
3636
from sqlalchemy import Float, ForeignKey, Integer, Interval, Numeric, inspect
37+
from sqlalchemy.ext.hybrid import hybrid_property
3738
from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship
3839
from sqlalchemy.orm.attributes import set_attribute
3940
from sqlalchemy.orm.decl_api import DeclarativeMeta
@@ -290,7 +291,11 @@ def get_config(name: str) -> Any:
290291
# If it was passed by kwargs, ensure it's also set in config
291292
new_cls.__config__.table = config_table
292293
for k, v in new_cls.__fields__.items():
293-
col = get_column_from_field(v)
294+
col = v
295+
# Treat `hybrid_property` properties as already specified columns
296+
# and let sqlalchemy take care of them
297+
if not issubclass(v.type_, hybrid_property):
298+
col = get_column_from_field(v)
294299
setattr(new_cls, k, col)
295300
# Set a config flag to tell FastAPI that this should be read with a field
296301
# in orm_mode instead of preemptively converting it to a dict.
@@ -326,6 +331,10 @@ def __init__(
326331
if getattr(cls.__config__, "table", False) and not base_is_table:
327332
dict_used = dict_.copy()
328333
for field_name, field_value in cls.__fields__.items():
334+
# Ignore `hybrid_property` properties as already specified columns
335+
# and let sqlalchemy take care of them
336+
if issubclass(field_value.type_, hybrid_property):
337+
continue
329338
dict_used[field_name] = get_column_from_field(field_value)
330339
for rel_name, rel_info in cls.__sqlmodel_relationships__.items():
331340
if rel_info.sa_relationship:

tests/conftest.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import pytest
77
from pydantic import BaseModel
8-
from sqlmodel import SQLModel
8+
from sqlmodel import SQLModel, create_engine
99
from sqlmodel.main import default_registry
1010

1111
top_level_path = Path(__file__).resolve().parent.parent
@@ -23,6 +23,13 @@ def clear_sqlmodel():
2323
default_registry.dispose()
2424

2525

26+
@pytest.fixture()
27+
def in_memory_engine(clear_sqlmodel):
28+
engine = create_engine("sqlite:///memory")
29+
yield engine
30+
SQLModel.metadata.drop_all(engine, checkfirst=True)
31+
32+
2633
@pytest.fixture()
2734
def cov_tmp_path(tmp_path: Path):
2835
yield tmp_path
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import Optional
2+
3+
from sqlalchemy import func
4+
from sqlalchemy.ext.hybrid import hybrid_property
5+
from sqlmodel import Field, Session, SQLModel, select
6+
7+
8+
def test_hybrid_property(in_memory_engine):
9+
class Interval(SQLModel, table=True):
10+
id: Optional[int] = Field(default=None, primary_key=True)
11+
length: float
12+
13+
@hybrid_property
14+
def radius(self) -> float:
15+
return abs(self.length) / 2
16+
17+
@radius.expression
18+
def radius(cls) -> float:
19+
return func.abs(cls.length) / 2
20+
21+
class Config:
22+
arbitrary_types_allowed = True
23+
24+
SQLModel.metadata.create_all(in_memory_engine)
25+
session = Session(in_memory_engine)
26+
27+
interval = Interval(length=-2)
28+
assert interval.radius == 1
29+
30+
session.add(interval)
31+
session.commit()
32+
interval_2 = session.exec(select(Interval)).all()[0]
33+
assert interval_2.radius == 1
34+
35+
interval_3 = session.exec(select(Interval).where(Interval.radius == 1)).all()[0]
36+
assert interval_3.radius == 1
37+
38+
intervals = session.exec(select(Interval).where(Interval.radius > 1)).all()
39+
assert len(intervals) == 0
40+
41+
assert session.exec(select(Interval.radius + 1)).all()[0] == 2.0

0 commit comments

Comments
 (0)