Skip to content

Deferred Loader Error When Reverting to State With No Polymorphic Children #153

@gavmor

Description

@gavmor

[Context: this is an issue written for the SQLAlchemy-History repository to try and address some frustrations we have encountered.]

SQLAlchemy error: Deferred loader for attribute 'radius' failed to populate correctly

Lost a lot of time to what, ultimately, felt like a pretty silly oversight due to the specificity of this SQLAlchemy message coming up when reverting relations to a transaction before which they exist.

Let's say we have a class composed of polymorphic components:

class Car(Base):
    __tablename__ = "car"
    __versioned__ = {}

    id: Mapped[int] = mapped_column(primary_key=True)
    
    parts: Mapped[List["Part"]] = relationship(
        back_populates="car", 
        cascade="all, delete-orphan"
    )


class Part(Base):
    __tablename__ = "part"
    __versioned__ = {}

    id: Mapped[int] = mapped_column(primary_key=True)
    car_id: Mapped[Optional[int]] = mapped_column(ForeignKey("car.id"))    
    car: Mapped["Car"] = relationship(back_populates="parts")

    type: Mapped[str] = mapped_column(String(50))
    __mapper_args__ = {
        "polymorphic_identity": "part",
        "polymorphic_on": "type",
    }


class Tire(Part):
    __tablename__ = "tire"
    __versioned__ = {}

    id: Mapped[int] = mapped_column(ForeignKey("part.id"), primary_key=True)
    
    radius: Mapped[int]
    width: Mapped[int]

    __mapper_args__ = {
        "polymorphic_identity": "tire",
    }

# # For example, another polymorphic child:
# class Engine(Part):
#     __tablename__ = "engine"
#     __versioned__ = {}

#     id: Mapped[int] = mapped_column(ForeignKey("part.id"), primary_key=True)
#     horsepower: Mapped[int]

#     __mapper_args__ = {
#         "polymorphic_identity": "engine",
#     }
    

When, in our application, we try to revert a Car instance to a previous transaction that predates the addition of any Tire or Engine parts, we encounter the error:

parts = car.revert(relations=["parts"]).parts
tire = parts[0]
tire.radius # Throws the error

Note: We can bring this about sooner by specifying lazy="selectin" in the relationship definition. This forces SQLAlchemy to use a different loading strategy that surfaces the issue more quickly.

To reiterate, this happens because we have no parts at that transaction point.

Note: Is there a simpler use case that produces the same error? (eg. a simple one-to-one relation) Or is this inherently tied to polymorphic relationships?

How might we update SQLAlchemy-History to better handle this scenario? Ideally, reverting to a transaction where no polymorphic children exist should not lead to an error when accessing attributes of those children. Instead, it should gracefully handle the absence of those children, perhaps by returning None or an empty collection, depending on the context. At the very least, the error message could be more descriptive to indicate that the issue arises from attempting to access attributes of non-existent polymorphic children.

Or, how can we adjust our usage of SQLAlchemy-History to avoid or pre-empt this error?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions