Refined the check which the ORM lazy loader uses to detect "this would be
loading by primary key and the primary key is NULL, skip loading" to take
into account the current setting for the
:paramref:`.orm.Mapper.allow_partial_pks` parameter. If this parameter is
False, then a composite PK value that has partial NULL elements should also
be skipped. This can apply to some composite overlapping foreign key
configurations.
Fixes: #11995
Change-Id: Icf9a52b7405d7400d46bfa944edcbff1a89225a3
(cherry picked from commit
830debc30896203bfd21fea18d323c5d849068d1)
--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 11995
+
+ Refined the check which the ORM lazy loader uses to detect "this would be
+ loading by primary key and the primary key is NULL, skip loading" to take
+ into account the current setting for the
+ :paramref:`.orm.Mapper.allow_partial_pks` parameter. If this parameter is
+ False, then a composite PK value that has partial NULL elements should also
+ be skipped. This can apply to some composite overlapping foreign key
+ configurations.
+
_none_set = frozenset([None, NEVER_SET, PASSIVE_NO_RESULT])
+_none_only_set = frozenset([None])
+
_SET_DEFERRED_EXPIRED = util.symbol("SET_DEFERRED_EXPIRED")
_DEFER_FOR_STATE = util.symbol("DEFER_FOR_STATE")
particular primary key value. A "partial primary key" can occur if
one has mapped to an OUTER JOIN, for example.
+ The :paramref:`.orm.Mapper.allow_partial_pks` parameter also
+ indicates to the ORM relationship lazy loader, when loading a
+ many-to-one related object, if a composite primary key that has
+ partial NULL values should result in an attempt to load from the
+ database, or if a load attempt is not necessary.
+
+ .. versionadded:: 2.0.36 :paramref:`.orm.Mapper.allow_partial_pks`
+ is consulted by the relationship lazy loader strategy, such that
+ when set to False, a SELECT for a composite primary key that
+ has partial NULL values will not be emitted.
+
:param batch: Defaults to ``True``, indicating that save operations
of multiple entities can be batched together for efficiency.
Setting to False indicates
from .session import _state_session
from .state import InstanceState
from .strategy_options import Load
-from .util import _none_set
+from .util import _none_only_set
from .util import AliasedClass
from .. import event
from .. import exc as sa_exc
elif LoaderCallableStatus.NEVER_SET in primary_key_identity:
return LoaderCallableStatus.NEVER_SET
- if _none_set.issuperset(primary_key_identity):
- return None
+ # test for None alone in primary_key_identity based on
+ # allow_partial_pks preference. PASSIVE_NO_RESULT and NEVER_SET
+ # have already been tested above
+ if not self.mapper.allow_partial_pks:
+ if _none_only_set.intersection(primary_key_identity):
+ return None
+ else:
+ if _none_only_set.issuperset(primary_key_identity):
+ return None
if (
self.key in state.dict
from .base import _class_to_mapper as _class_to_mapper
from .base import _MappedAnnotationBase
from .base import _never_set as _never_set # noqa: F401
+from .base import _none_only_set as _none_only_set # noqa: F401
from .base import _none_set as _none_set # noqa: F401
from .base import attribute_str as attribute_str # noqa: F401
from .base import class_mapper as class_mapper
from sqlalchemy.orm import attributes
from sqlalchemy.orm import configure_mappers
from sqlalchemy.orm import exc as orm_exc
+from sqlalchemy.orm import foreign
from sqlalchemy.orm import relationship
+from sqlalchemy.orm import remote
from sqlalchemy.orm import Session
from sqlalchemy.orm import with_parent
from sqlalchemy.testing import assert_raises
self.assert_sql_count(testing.db, go, 1)
+ @testing.fixture()
+ def composite_overlapping_fixture(self, decl_base, connection):
+ def go(allow_partial_pks):
+
+ class Section(decl_base):
+ __tablename__ = "sections"
+ year = Column(Integer, primary_key=True)
+ idx = Column(Integer, primary_key=True)
+ parent_idx = Column(Integer)
+
+ if not allow_partial_pks:
+ __mapper_args__ = {"allow_partial_pks": False}
+
+ ForeignKeyConstraint((year, parent_idx), (year, idx))
+
+ parent = relationship(
+ "Section",
+ primaryjoin=and_(
+ year == remote(year),
+ foreign(parent_idx) == remote(idx),
+ ),
+ )
+
+ decl_base.metadata.create_all(connection)
+ connection.commit()
+
+ with Session(connection) as sess:
+ sess.add(Section(year=5, idx=1, parent_idx=None))
+ sess.commit()
+
+ return Section
+
+ return go
+
+ @testing.variation("allow_partial_pks", [True, False])
+ def test_composite_m2o_load_partial_pks(
+ self, allow_partial_pks, composite_overlapping_fixture
+ ):
+ Section = composite_overlapping_fixture(allow_partial_pks)
+
+ session = fixture_session()
+ section = session.get(Section, (5, 1))
+
+ with self.assert_statement_count(
+ testing.db, 1 if allow_partial_pks else 0
+ ):
+ testing.is_none(section.parent)
+
class CorrelatedTest(fixtures.MappedTest):
@classmethod