hybrid_property.expression to return date from column or today's date

Question:

I have a SQLAlchemy model in my Flask app that looks something like this:

class MyModel(db.model):
    _date_start = db.Column(db.Date, nullable=False, default=date.today)
    _date_end = db.Column(db.Date, nullable=True, default=None)

    @hybrid_property
    def date_start(self) -> date:
        return self._date_start

    @hybrid_property
    def date_end(self) -> date:
        return self._date_end

    @hybrid_property
    def date_last_active(self) -> date:
        if self.date_end:
            return self.date_end
        return date.today()

    @hybrid_method
    def contains(self, date: date) -> bool:
        return (date >= self.date_start and date <= self.date_last_active)

    @contains.expression
    def contains(cls, date: date) -> bool:
        return and_(date >= cls._date_start, date <= cls.date_last_active)

When I create an entry with a null date_end, my contains method fails to find that entry. This appears to be because of the date_last_active hybrid property:

>>> entry = MyModel(_date_start=date(2022, 12, 1)
>>> # ... add and commit, then:
>>> MyModel.query.filter(MyModel.contains(date(2022, 12, 5)).count()
0
>>> MyModel.query.filter(date(2022, 12, 5) >= MyModel.date_start).count()
1
>>> MyModel.query.filter(date(2022, 12, 5) <= MyModel.date_last_active).count()
0
>>> MyModel.query.filter(date(2022, 12, 5) <= func.current_date()).count()
1

I’d guessed that the conditional is what’s tripping it up, so I tried adding an expression with a case statement, but I can’t get it to work. Two expressions I’ve tried are:

# case attempt 1
@date_last_active.expression
def date_last_active(cls) -> date:
    return case(
        (cls._date_end.is_(None), func.current_date()),
        (not_(cls._date_end.is_(None)), cls._date_end)
    )
# Raises: NotImplementedError: Operator 'getitem' is not supported on this expression

# case attempt 2
@date_last_active.expression
def date_last_active(cls) -> date:
    return case(
        {cls._date_end.is_(None): func.current_date()},
        value=cls._date_end,
        else_=cls._date_end
    )
# Still doesn't find the entry:
# >>> DailyStreak.query.filter(date(2022, 12, 5) <= DailyStreak.date_last_active).count()
# 0

I feel like I’m close, but I’m just not seeing where I need to go. Thanks for the help!

Asked By: DukeSilver

||

Answers:

Expanding on what @IljaEverilä said this seems to work:

class MyModel(Base):
    __tablename__ = 'mymodels'

    id = Column(Integer, primary_key=True)

    _date_start = Column(Date, nullable=False, default=date.today)
    _date_end = Column(Date, nullable=True, default=None)

    @hybrid_property
    def date_start(self) -> date:
        return self._date_start

    @hybrid_property
    def date_end(self) -> date:
        return self._date_end

    @hybrid_property
    def date_last_active(self) -> date:
        return func.coalesce(self.date_end, func.current_date())

    @hybrid_method
    def contains(self, date: date) -> bool:
        return (date >= self.date_start and date <= self.date_last_active)

    @contains.expression
    def contains(cls, date: date) -> bool:
        return and_(date >= cls._date_start, date <= cls.date_last_active)

Then the query …

with Session(engine) as session, session.begin():
    entry = MyModel(_date_start=date(2022, 12, 1))
    session.add(entry)

with Session(engine) as session, session.begin():
    print(session.query(MyModel).filter(MyModel.contains(date(2022, 12, 5))).count())

… outputs something like this:

2022-12-18 11:14:31,913 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2022-12-18 11:14:31,915 INFO sqlalchemy.engine.Engine INSERT INTO mymodels (_date_start, _date_end) VALUES (%(_date_start)s, %(_date_end)s) RETURNING mymodels.id
2022-12-18 11:14:31,915 INFO sqlalchemy.engine.Engine [generated in 0.00026s] {'_date_start': datetime.date(2022, 12, 1), '_date_end': None}
2022-12-18 11:14:31,918 INFO sqlalchemy.engine.Engine COMMIT
2022-12-18 11:14:31,943 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2022-12-18 11:14:31,950 INFO sqlalchemy.engine.Engine SELECT count(*) AS count_1 
FROM (SELECT mymodels.id AS mymodels_id, mymodels._date_start AS mymodels__date_start, mymodels._date_end AS mymodels__date_end 
FROM mymodels 
WHERE mymodels._date_start <= %(date_start_1)s AND coalesce(mymodels._date_end, CURRENT_DATE) >= %(coalesce_1)s) AS anon_1
2022-12-18 11:14:31,950 INFO sqlalchemy.engine.Engine [generated in 0.00022s] {'date_start_1': datetime.date(2022, 12, 5), 'coalesce_1': datetime.date(2022, 12, 5)}
1
2022-12-18 11:14:31,952 INFO sqlalchemy.engine.Engine COMMIT

Answered By: Ian Wilson
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.