]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Improved checking for an existing backref name conflict during
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 3 Mar 2013 18:51:54 +0000 (13:51 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 3 Mar 2013 18:51:54 +0000 (13:51 -0500)
mapper configuration; will now test for name conflicts on
superclasses and subclasses, in addition to the current mapper,
as these conflicts break things just as much.  This is new for
0.8, but see below for a warning that will also be triggered
in 0.7.11.
- Improved the error message emitted when a "backref loop" is detected,
that is when an attribute event triggers a bidirectional
assignment between two other attributes with no end.
This condition can occur not just when an object of the wrong
type is assigned, but also when an attribute is mis-configured
to backref into an existing backref pair.  Also in 0.7.11.
- A warning is emitted when a MapperProperty is assigned to a mapper
that replaces an existing property, if the properties in question
aren't plain column-based properties.   Replacement of relationship
properties is rarely (ever?) what is intended and usually refers to a
mapper mis-configuration.   Also in 0.7.11.
[ticket:2674]

doc/build/changelog/changelog_07.rst
doc/build/changelog/changelog_08.rst
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/properties.py
test/orm/test_attributes.py
test/orm/test_compile.py
test/orm/test_mapper.py

index 41aac067b1d326d3b05e3de6fc2e6ef764047de0..df919dc3d9b51c62c91417766ff98577f3502810 100644 (file)
       Fixed an import of "logging" in test_execute which was not
       working on some linux platforms.
 
+    .. change::
+        :tags: bug, orm
+        :tickets: 2674
+
+      Improved the error message emitted when a "backref loop" is detected,
+      that is when an attribute event triggers a bidirectional
+      assignment between two other attributes with no end.
+      This condition can occur not just when an object of the wrong
+      type is assigned, but also when an attribute is mis-configured
+      to backref into an existing backref pair.
+
+    .. change::
+      :tags: bug, orm
+      :tickets: 2674
+
+      A warning is emitted when a MapperProperty is assigned to a mapper
+      that replaces an existing property, if the properties in question
+      aren't plain column-based properties.   Replacement of relationship
+      properties is rarely (ever?) what is intended and usually refers to a
+      mapper mis-configuration.   This will also warn if a backref configures
+      itself on top of an existing one in an inheritance relationship
+      (which is an error in 0.8).
+
 .. changelog::
     :version: 0.7.10
     :released: Thu Feb 7 2013
index 370de2603984ad44b60692be784f9fa66426747c..59f2b3ad13879cd6a186f2abf8ec8291ff4075d3 100644 (file)
 
       * :ref:`metadata_create_drop_tables`
 
+    .. change::
+        :tags: bug, orm
+        :tickets: 2674
+
+      Improved checking for an existing backref name conflict during
+      mapper configuration; will now test for name conflicts on
+      superclasses and subclasses, in addition to the current mapper,
+      as these conflicts break things just as much.  This is new for
+      0.8, but see below for a warning that will also be triggered
+      in 0.7.11.
+
+    .. change::
+        :tags: bug, orm
+        :tickets: 2674
+
+      Improved the error message emitted when a "backref loop" is detected,
+      that is when an attribute event triggers a bidirectional
+      assignment between two other attributes with no end.
+      This condition can occur not just when an object of the wrong
+      type is assigned, but also when an attribute is mis-configured
+      to backref into an existing backref pair.  Also in 0.7.11.
+
+    .. change::
+      :tags: bug, orm
+      :tickets: 2674
+
+      A warning is emitted when a MapperProperty is assigned to a mapper
+      that replaces an existing property, if the properties in question
+      aren't plain column-based properties.   Replacement of relationship
+      properties is rarely (ever?) what is intended and usually refers to a
+      mapper mis-configuration.   Also in 0.7.11.
+
     .. change::
         :tags: feature, orm
 
index c9385daaaf1a831781e1bb13b96c40aeb4305ce6..b281fabec4b022ec08332324c13d251f95175aa2 100644 (file)
@@ -433,6 +433,9 @@ class AttributeImpl(object):
 
         self.expire_missing = expire_missing
 
+    def __str__(self):
+        return "%s.%s" % (self.class_.__name__, self.key)
+
     def _get_active_history(self):
         """Backwards compat for impl.active_history"""
 
@@ -1043,11 +1046,18 @@ def backref_listeners(attribute, key, uselist):
 
     parent_token = attribute.impl.parent_token
 
-    def _acceptable_key_err(child_state, initiator):
+    def _acceptable_key_err(child_state, initiator, child_impl):
         raise ValueError(
-            "Object %s not associated with attribute of "
-            "type %s" % (orm_util.state_str(child_state),
-                    manager_of_class(initiator.class_)[initiator.key]))
+            "Bidirectional attribute conflict detected: "
+            'Passing object %s to attribute "%s" '
+            'triggers a modify event on attribute "%s" '
+            'via the backref "%s".' % (
+                orm_util.state_str(child_state),
+                initiator.parent_token,
+                child_impl.parent_token,
+                attribute.impl.parent_token
+            )
+        )
 
     def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
         if oldchild is child:
@@ -1069,7 +1079,7 @@ def backref_listeners(attribute, key, uselist):
             child_impl = child_state.manager[key].impl
             if initiator.parent_token is not parent_token and \
                 initiator.parent_token is not child_impl.parent_token:
-                _acceptable_key_err(state, initiator)
+                _acceptable_key_err(state, initiator, child_impl)
             child_impl.append(
                                 child_state,
                                 child_dict,
@@ -1085,9 +1095,11 @@ def backref_listeners(attribute, key, uselist):
         child_state, child_dict = instance_state(child), \
                                     instance_dict(child)
         child_impl = child_state.manager[key].impl
+
+        print initiator.parent_token, parent_token, child_impl.parent_token
         if initiator.parent_token is not parent_token and \
             initiator.parent_token is not child_impl.parent_token:
-            _acceptable_key_err(state, initiator)
+            _acceptable_key_err(state, initiator, child_impl)
         child_impl.append(
                                 child_state,
                                 child_dict,
index 447a5fce126fef87b5d5214d7f7b976002c3b519..4e7b4d272b75457da0e3bc84203e3046f2eab709 100644 (file)
@@ -1094,7 +1094,7 @@ class Mapper(_InspectionAttr):
                 # initialized; check for 'readonly'
                 if hasattr(self, '_readonly_props') and \
                     (not hasattr(col, 'table') or
-                    col.table not in self._cols_by_table):
+                        col.table not in self._cols_by_table):
                         self._readonly_props.add(prop)
 
             else:
@@ -1132,6 +1132,16 @@ class Mapper(_InspectionAttr):
                         "%r for column %r" % (syn, key, key, syn)
                     )
 
+        if key in self._props and \
+                not isinstance(prop, properties.ColumnProperty) and \
+                not isinstance(self._props[key], properties.ColumnProperty):
+            util.warn("Property %s on %s being replaced with new "
+                            "property %s; the old property will be discarded" % (
+                            self._props[key],
+                            self,
+                            prop,
+                        ))
+
         self._props[key] = prop
 
         if not self.non_primary:
index a0d8c92d1a28a7ac548e2e960daaaff05cb14dd8..79a1b81d36c1893eac880840adcd0307b0bfe9a8 100644 (file)
@@ -816,7 +816,10 @@ class RelationshipProperty(StrategizedProperty):
                 adapt_source=adapt_source)
 
     def __str__(self):
-        return str(self.parent.class_.__name__) + "." + self.key
+        if self.parent:
+            return str(self.parent.class_.__name__) + "." + self.key
+        else:
+            return "." + self.key
 
     def merge(self,
                     session,
@@ -1201,11 +1204,15 @@ class RelationshipProperty(StrategizedProperty):
             else:
                 backref_key, kwargs = self.backref
             mapper = self.mapper.primary_mapper()
-            if mapper.has_property(backref_key):
-                raise sa_exc.ArgumentError("Error creating backref "
-                        "'%s' on relationship '%s': property of that "
-                        "name exists on mapper '%s'" % (backref_key,
-                        self, mapper))
+
+            check = set(mapper.iterate_to_root()).\
+                        union(mapper.self_and_descendants)
+            for m in check:
+                if m.has_property(backref_key):
+                    raise sa_exc.ArgumentError("Error creating backref "
+                            "'%s' on relationship '%s': property of that "
+                            "name exists on mapper '%s'" % (backref_key,
+                            self, m))
 
             # determine primaryjoin/secondaryjoin for the
             # backref.  Use the one we had, so that
index 1fc70fd77b63fed7d925d9191a6c2b095e4c4994..d60c55edd204e2a59c64698a36847cd0d085bbdb 100644 (file)
@@ -1170,7 +1170,10 @@ class CyclicBackrefAssertionTest(fixtures.TestBase):
         b1 = B()
         assert_raises_message(
             ValueError,
-            "Object <B at .*> not associated with attribute of type C.a",
+            'Bidirectional attribute conflict detected: '
+            'Passing object <B at .*> to attribute "C.a" '
+            'triggers a modify event on attribute "C.b" '
+            'via the backref "B.c".',
             setattr, c1, 'a', b1
         )
 
@@ -1180,10 +1183,14 @@ class CyclicBackrefAssertionTest(fixtures.TestBase):
         b1 = B()
         assert_raises_message(
             ValueError,
-            "Object <B at .*> not associated with attribute of type C.a",
+            'Bidirectional attribute conflict detected: '
+            'Passing object <B at .*> to attribute "C.a" '
+            'triggers a modify event on attribute "C.b" '
+            'via the backref "B.c".',
             c1.a.append, b1
         )
 
+
     def _scalar_fixture(self):
         class A(object):
             pass
@@ -1225,6 +1232,36 @@ class CyclicBackrefAssertionTest(fixtures.TestBase):
 
         return A, B, C
 
+    def _broken_collection_fixture(self):
+        class A(object):
+            pass
+        class B(object):
+            pass
+        instrumentation.register_class(A)
+        instrumentation.register_class(B)
+
+        attributes.register_attribute(A, 'b', backref='a1', useobject=True)
+        attributes.register_attribute(B, 'a1', backref='b', useobject=True,
+                                                uselist=True)
+
+        attributes.register_attribute(B, 'a2', backref='b', useobject=True,
+                                                uselist=True)
+
+        return A, B
+
+    def test_broken_collection_assertion(self):
+        A, B = self._broken_collection_fixture()
+        b1 = B()
+        a1 = A()
+        assert_raises_message(
+            ValueError,
+            'Bidirectional attribute conflict detected: '
+            'Passing object <A at .*> to attribute "B.a2" '
+            'triggers a modify event on attribute "B.a1" '
+            'via the backref "A.b".',
+            b1.a2.append, a1
+        )
+
 class PendingBackrefTest(fixtures.ORMTest):
     def setup(self):
         global Post, Blog, called, lazy_load
index fb32fb0b9b058be0aa271e6256e8617c1d59ca0a..ad6778e97ae342989b79b7f68a443d8ed4d2e374 100644 (file)
@@ -167,8 +167,10 @@ class CompileTest(fixtures.ORMTest):
         b = Table('b', meta, Column('id', Integer, primary_key=True),
                                 Column('a_id', Integer, ForeignKey('a.id')))
 
-        class A(object):pass
-        class B(object):pass
+        class A(object):
+            pass
+        class B(object):
+            pass
 
         mapper(A, a, properties={
             'b':relationship(B, backref='a')
@@ -183,3 +185,29 @@ class CompileTest(fixtures.ORMTest):
             configure_mappers
         )
 
+    def test_conflicting_backref_subclass(self):
+        meta = MetaData()
+
+        a = Table('a', meta, Column('id', Integer, primary_key=True))
+        b = Table('b', meta, Column('id', Integer, primary_key=True),
+                                Column('a_id', Integer, ForeignKey('a.id')))
+
+        class A(object):
+            pass
+        class B(object):
+            pass
+        class C(B):
+            pass
+
+        mapper(A, a, properties={
+            'b': relationship(B, backref='a'),
+            'c': relationship(C, backref='a')
+        })
+        mapper(B, b)
+        mapper(C, None, inherits=B)
+
+        assert_raises_message(
+            sa_exc.ArgumentError,
+            "Error creating backref",
+            configure_mappers
+        )
index 8c5b9cd849d13f5082445d2ca36508e8663b2173..66082b549449e5664563cbe9ea70a3dee6214217 100644 (file)
@@ -488,7 +488,7 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
         assert hasattr(User, 'addresses')
         assert "addresses" in [p.key for p in m1._polymorphic_properties]
 
-    def test_replace_property(self):
+    def test_replace_col_prop_w_syn(self):
         users, User = self.tables.users, self.classes.User
 
         m = mapper(User, users)
@@ -514,6 +514,24 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
         u.name = 'jacko'
         assert m._columntoproperty[users.c.name] is m.get_property('_name')
 
+    def test_replace_rel_prop_with_rel_warns(self):
+        users, User = self.tables.users, self.classes.User
+        addresses, Address = self.tables.addresses, self.classes.Address
+
+        m = mapper(User, users, properties={
+                "addresses": relationship(Address)
+            })
+        mapper(Address, addresses)
+
+        assert_raises_message(
+            sa.exc.SAWarning,
+            "Property User.addresses on Mapper|User|users being replaced "
+            "with new property User.addresses; the old property will "
+            "be discarded",
+            m.add_property,
+            "addresses", relationship(Address)
+        )
+
     def test_add_column_prop_deannotate(self):
         User, users = self.classes.User, self.tables.users
         Address, addresses = self.classes.Address, self.tables.addresses