Skip to content

False SAWarning on second pickle round-trip of an ORM instance with a per-instance lazy loader #13574

Description

@zzzeek

Reported by @BlastyCZ in discussion #13573; confirmed against main.

_LoadLazyAttribute.__getstate__() warns whenever self.extra_criteria is not None, but the state it emits sets extra_criteria to () rather than None:

    def __getstate__(self):
        if self.extra_criteria is not None:
            util.warn(
                "Can't reliably serialize a lazyload() option that "
                "contains additional criteria; please use eager loading "
                "for this case"
            )
        return {
            "key": self.key,
            "strategy_key": self.strategy_key,
            "loadopt": self.loadopt,
            "extra_criteria": (),
        }

The instance-level callable is created with extra_criteria=None when no criteria are present (strategies.py, _LazyLoader.create_row_processor), so the first pickle is silent. After a round trip the attribute is (), which is falsely truthy for the is not None test, so every subsequent pickle of the same object emits:

SAWarning: Can't reliably serialize a lazyload() option that contains additional criteria; please use eager loading for this case

() and None both mean "no additional criteria", so no warning should be emitted. This shows up naturally when instances cross more than one serialization boundary, e.g. multiprocessing.

Present since 1.4 (56f9c77).

Reproducer

import pickle
import warnings

from sqlalchemy import ForeignKey, create_engine, inspect, select
from sqlalchemy.exc import SAWarning
from sqlalchemy.orm import (
    DeclarativeBase,
    Mapped,
    Session,
    mapped_column,
    raiseload,
    relationship,
)


class Base(DeclarativeBase):
    pass


class Parent(Base):
    __tablename__ = "parent"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[list["Child"]] = relationship()


class Child(Base):
    __tablename__ = "child"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))


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

with Session(engine) as session:
    session.add(Parent(children=[Child()]))
    session.commit()

with Session(engine) as session:
    parent = session.scalar(select(Parent).options(raiseload("*")))
    print("before:", inspect(parent).callables["children"].extra_criteria)
    payload = pickle.dumps(parent)

parent = pickle.loads(payload)
print("after unpickle:", inspect(parent).callables["children"].extra_criteria)

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always", SAWarning)
    pickle.dumps(parent)

print("warnings:", [str(w.message) for w in caught])

Output on main:

before: None
after unpickle: ()
warnings: ["Can't reliably serialize a lazyload() option that contains additional criteria; please use eager loading for this case"]

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingloader optionsORM options like joinedload(), load_only(), these are complicated and have a lot of issuesorm

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions