]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Track SchemaEventTarget types in as_mutable()
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 30 Mar 2017 21:52:10 +0000 (17:52 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 30 Mar 2017 21:52:56 +0000 (17:52 -0400)
Fixed bug in :mod:`sqlalchemy.ext.mutable` where the
:meth:`.Mutable.as_mutable` method would not track a type that had
been copied using :meth:`.TypeEngine.copy`.  This became more of
a regression in 1.1 compared to 1.0 because the :class:`.TypeDecorator`
class is now a subclass of :class:`.SchemaEventTarget`, which among
other things indicates to the parent :class:`.Column` that the type
should be copied when the :class:`.Column` is.  These copies are
common when using declarative with mixins or abstract classes.

Change-Id: Ib04df862c58263185dbae686c548fea3e12c46f1
Fixes: #3950
(cherry picked from commit 07b63894cb8ff9529b406f196b5d7cb9af209e9e)

doc/build/changelog/changelog_11.rst
lib/sqlalchemy/ext/mutable.py
test/ext/test_mutable.py

index ce704d7b7b31bd9912b9fa6245049c957f16e564..c4735c5de764e7503503d55f206fe9c0861b5965 100644 (file)
 .. changelog::
     :version: 1.1.8
 
+    .. change:: 3950
+        :tags: bug, ext
+        :versions: 1.2.0b1
+        :tickets: 3950
+
+        Fixed bug in :mod:`sqlalchemy.ext.mutable` where the
+        :meth:`.Mutable.as_mutable` method would not track a type that had
+        been copied using :meth:`.TypeEngine.copy`.  This became more of
+        a regression in 1.1 compared to 1.0 because the :class:`.TypeDecorator`
+        class is now a subclass of :class:`.SchemaEventTarget`, which among
+        other things indicates to the parent :class:`.Column` that the type
+        should be copied when the :class:`.Column` is.  These copies are
+        common when using declarative with mixins or abstract classes.
+
     .. change::
         :tags: bug, ext
         :versions: 1.2.0b1
index 3361c447501346dcb42bf7bbb76c9c93cd84c70e..0e35914613cdc51242d8f833868eaa5c47f00cf7 100644 (file)
@@ -352,6 +352,7 @@ from ..orm.attributes import flag_modified
 from .. import event, types
 from ..orm import mapper, object_mapper, Mapper
 from ..util import memoized_property
+from ..sql.base import SchemaEventTarget
 import weakref
 
 
@@ -584,9 +585,26 @@ class Mutable(MutableBase):
         """
         sqltype = types.to_instance(sqltype)
 
+        # a SchemaType will be copied when the Column is copied,
+        # and we'll lose our ability to link that type back to the original.
+        # so track our original type w/ columns
+        if isinstance(sqltype, SchemaEventTarget):
+            @event.listens_for(sqltype, "before_parent_attach")
+            def _add_column_memo(sqltyp, parent):
+                parent.info['_ext_mutable_orig_type'] = sqltyp
+            schema_event_check = True
+        else:
+            schema_event_check = False
+
         def listen_for_type(mapper, class_):
             for prop in mapper.column_attrs:
-                if prop.columns[0].type is sqltype:
+                if (
+                        schema_event_check and
+                        prop.columns[0].info.get('_ext_mutable_orig_type')
+                        is sqltype
+                ) or (
+                    prop.columns[0].type is sqltype
+                ):
                     cls.associate_with_attribute(getattr(class_, prop.key))
 
         event.listen(mapper, 'mapper_configured', listen_for_type)
index 81d2136e369d8379d1260ea76e921533862ab959..5b6d3e7cf7a6c1e9e506866d9532d09dd89dc246 100644 (file)
@@ -725,6 +725,49 @@ class MutableWithScalarJSONTest(_MutableDictTestBase, fixtures.MappedTest):
         self._test_non_mutable()
 
 
+class MutableColumnCopyJSONTest(_MutableDictTestBase, fixtures.MappedTest):
+
+    @classmethod
+    def define_tables(cls, metadata):
+        import json
+        from sqlalchemy.ext.declarative import declarative_base
+
+        class JSONEncodedDict(TypeDecorator):
+            impl = VARCHAR(50)
+
+            def process_bind_param(self, value, dialect):
+                if value is not None:
+                    value = json.dumps(value)
+
+                return value
+
+            def process_result_value(self, value, dialect):
+                if value is not None:
+                    value = json.loads(value)
+                return value
+
+        MutableDict = cls._type_fixture()
+
+        Base = declarative_base(metadata=metadata)
+
+        class AbstractFoo(Base):
+            __abstract__ = True
+
+            id = Column(Integer, primary_key=True,
+                        test_needs_autoincrement=True)
+            data = Column(MutableDict.as_mutable(JSONEncodedDict))
+            non_mutable_data = Column(JSONEncodedDict)
+            unrelated_data = Column(String(50))
+
+        class Foo(AbstractFoo):
+            __tablename__ = "foo"
+
+        assert Foo.data.property.columns[0].type is not AbstractFoo.data.type
+
+    def test_non_mutable(self):
+        self._test_non_mutable()
+
+
 class MutableListWithScalarPickleTest(_MutableListTestBase,
                                       fixtures.MappedTest):