]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
ensure collection adapter is serialized
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 2 Aug 2023 17:34:03 +0000 (13:34 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 2 Aug 2023 17:37:22 +0000 (13:37 -0400)
Fixed issue where dictionary-based collections such as
:func:`_orm.attribute_keyed_dict` did not fully pickle/unpickle correctly,
leading to issues when attempting to mutate such a collection after
unpickling.

Fixes: #10175
Change-Id: I1281f8695a0c95a20cab9449ee9c5d2db0d544fe

doc/build/changelog/unreleased_20/10175.rst [new file with mode: 0644]
lib/sqlalchemy/orm/mapped_collection.py
test/orm/test_pickled.py

diff --git a/doc/build/changelog/unreleased_20/10175.rst b/doc/build/changelog/unreleased_20/10175.rst
new file mode 100644 (file)
index 0000000..856b55b
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 10175
+
+    Fixed issue where dictionary-based collections such as
+    :func:`_orm.attribute_keyed_dict` did not fully pickle/unpickle correctly,
+    leading to issues when attempting to mutate such a collection after
+    unpickling.
+
index fb6b05d78ea59cddb5a554b140e3d997f42e4ffb..9e479d0d308ae458e34395fd98d175c40cd2e439 100644 (file)
@@ -23,6 +23,7 @@ from typing import Union
 
 from . import base
 from .collections import collection
+from .collections import collection_adapter
 from .. import exc as sa_exc
 from .. import util
 from ..sql import coercions
@@ -33,6 +34,7 @@ from ..util.typing import Literal
 if TYPE_CHECKING:
     from . import AttributeEventToken
     from . import Mapper
+    from .collections import CollectionAdapter
     from ..sql.elements import ColumnElement
 
 _KT = TypeVar("_KT", bound=Any)
@@ -376,19 +378,31 @@ class KeyFuncDict(Dict[_KT, _VT]):
 
     @classmethod
     def _unreduce(
-        cls, keyfunc: _F, values: Dict[_KT, _KT]
+        cls,
+        keyfunc: _F,
+        values: Dict[_KT, _KT],
+        adapter: Optional[CollectionAdapter] = None,
     ) -> "KeyFuncDict[_KT, _KT]":
         mp: KeyFuncDict[_KT, _KT] = KeyFuncDict(keyfunc)
         mp.update(values)
+        # note that the adapter sets itself up onto this collection
+        # when its `__setstate__` method is called
         return mp
 
     def __reduce__(
         self,
     ) -> Tuple[
         Callable[[_KT, _KT], KeyFuncDict[_KT, _KT]],
-        Tuple[Any, Union[Dict[_KT, _KT], Dict[_KT, _KT]]],
+        Tuple[Any, Union[Dict[_KT, _KT], Dict[_KT, _KT]], CollectionAdapter],
     ]:
-        return (KeyFuncDict._unreduce, (self.keyfunc, dict(self)))
+        return (
+            KeyFuncDict._unreduce,
+            (
+                self.keyfunc,
+                dict(self),
+                collection_adapter(self),
+            ),
+        )
 
     @util.preload_module("sqlalchemy.orm.attributes")
     def _raise_for_unpopulated(
index 87461bc102f2a61b78c3a3df54adc7c3fdb843c3..96dec4a60b70d501cafcf681405061d86f6aec07 100644 (file)
@@ -10,6 +10,7 @@ from sqlalchemy import testing
 from sqlalchemy.orm import aliased
 from sqlalchemy.orm import attributes
 from sqlalchemy.orm import clear_mappers
+from sqlalchemy.orm import collections
 from sqlalchemy.orm import exc as orm_exc
 from sqlalchemy.orm import lazyload
 from sqlalchemy.orm import relationship
@@ -22,6 +23,7 @@ from sqlalchemy.orm.collections import column_keyed_dict
 from sqlalchemy.testing import assert_raises_message
 from sqlalchemy.testing import eq_
 from sqlalchemy.testing import fixtures
+from sqlalchemy.testing import is_not_none
 from sqlalchemy.testing.fixtures import fixture_session
 from sqlalchemy.testing.pickleable import Address
 from sqlalchemy.testing.pickleable import AddressWMixin
@@ -592,6 +594,8 @@ class PickleTest(fixtures.MappedTest):
             eq_(u1.addresses, repickled.addresses)
             eq_(repickled.addresses["email1"], Address(email_address="email1"))
 
+            is_not_none(collections.collection_adapter(repickled.addresses))
+
     def test_column_mapped_collection(self):
         users, addresses = self.tables.users, self.tables.addresses
 
@@ -618,6 +622,8 @@ class PickleTest(fixtures.MappedTest):
             eq_(u1.addresses, repickled.addresses)
             eq_(repickled.addresses["email1"], Address(email_address="email1"))
 
+            is_not_none(collections.collection_adapter(repickled.addresses))
+
     def test_composite_column_mapped_collection(self):
         users, addresses = self.tables.users, self.tables.addresses
 
@@ -646,6 +652,7 @@ class PickleTest(fixtures.MappedTest):
                 repickled.addresses[(1, "email1")],
                 Address(id=1, email_address="email1"),
             )
+            is_not_none(collections.collection_adapter(repickled.addresses))
 
 
 class OptionsTest(_Polymorphic):