'Create a hybrid_property to return the value of a previous record

I've got as far as this when trying to create a hybrid_property to return the value of the previous record:

from datetime import date
from sqlalchemy import Column, Integer, Date, select, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import sessionmaker

Base = declarative_base()


class A(Base):

    __tablename__ = "a"

    id_ = Column(Integer, primary_key=True)
    record_date = Column(Date)
    example_value = Column(Integer)

    @hybrid_property
    def prev_value(self):
        return
    
    @prev_value.expression
    def prev_value(cls):
        stmt = select(A.example_value)
        stmt = stmt.order_by(A.record_date.desc())
        stmt = stmt.limit(1)
        stmt = stmt.label("prev_value")
        return stmt


engine = create_engine("sqlite:///:memory:")
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine)
session = Session()

session.add(A(record_date=date(2022, 5 ,1), example_value=1))
session.add(A(record_date=date(2022, 5 ,2), example_value=2))
session.add(A(record_date=date(2022, 5 ,3), example_value=3))
session.commit()

prev_value = session.execute(select(A.prev_value).where(A.id_ == 3)).scalar()
print(prev_value)

This is currently returning None when it should return 2.

What should I put in the in-Python method and the expression variant?



Solution 1:[1]

SOLUTION:

@prev_value.expression
def prev_value(cls):
    A1 = aliased(A, name="a_prev")
    stmt = select(A1.example_value)
    stmt = stmt.filter(A1.record_date < cls.record_date)
    stmt = stmt.order_by(A1.record_date.desc())
    stmt = stmt.limit(1)
    stmt = stmt.label("prev_value")
    return stmt

Explanation

When I run the code from your question verbatim (using sqlite), the result I get is actually 3 and not None as you indicated. In fact, the result returned for all rows is the same value of 3 for all the rows asked by a query session.execute(select(A, A.prev_value)) # (1):

(<A [1] (example_value = 1, id_ = 1, record_date = datetime.date(2022, 5, 1))>, 3)
(<A [2] (example_value = 2, id_ = 2, record_date = datetime.date(2022, 5, 2))>, 3)
(<A [3] (example_value = 3, id_ = 3, record_date = datetime.date(2022, 5, 3))>, 3)

Why do I get 3 for your sample code?

I think this is because the sub-query does not have any condition linking it to the requested row. Assuming the previous value should be previous "by record_date", the link to add to the query should be:

stmt = stmt.filter(A.record_date < cls.record_date)

Running it, however, will now generate None for all the result. Let's look at the generated SQL and the reason why none rows are found:

SELECT a.id_,
       a.record_date,
       a.example_value,

  (SELECT a.example_value
   FROM a
   WHERE a.record_date < a.record_date  # >>> the ISSUE is here: always FALSE
   ORDER BY a.record_date DESC
   LIMIT 1) AS prev_value
FROM a

The problem is that the main query and the sub-query are pointing to the same table/view.

Solve the subquery: In order to solve it, we just need to explicitly create a sub-query, and the problem is solved:

@prev_value.expression
def prev_value(cls):
    A1 = aliased(A, name="a_prev")
    stmt = select(A1.example_value)
    stmt = stmt.filter(A1.record_date < cls.record_date)
    stmt = stmt.order_by(A1.record_date.desc())
    stmt = stmt.limit(1)
    stmt = stmt.label("prev_value")
    return stmt

and the same query (1) produces the following result:

(<A [1] (example_value = 1, id_ = 1, record_date = datetime.date(2022, 5, 1))>, None)
(<A [2] (example_value = 2, id_ = 2, record_date = datetime.date(2022, 5, 2))>, 1)
(<A [3] (example_value = 3, id_ = 3, record_date = datetime.date(2022, 5, 3))>, 2)

based on the following generated SQL:

SELECT a.id_,
       a.record_date,
       a.example_value,

  (SELECT a_prev.example_value
   FROM a AS a_prev
   WHERE a_prev.record_date < a.record_date
   ORDER BY a_prev.record_date DESC
   LIMIT 1) AS prev_value
FROM a

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 van