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"]
Reported by @BlastyCZ in discussion #13573; confirmed against main.
_LoadLazyAttribute.__getstate__()warns wheneverself.extra_criteria is not None, but the state it emits setsextra_criteriato()rather thanNone:The instance-level callable is created with
extra_criteria=Nonewhen 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 theis not Nonetest, so every subsequent pickle of the same object emits:()andNoneboth 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
Output on main: