]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Expand sibling tests for overlaps warning
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 31 Mar 2021 17:16:04 +0000 (13:16 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 31 Mar 2021 17:23:53 +0000 (13:23 -0400)
Scaled back the warning message added in :ticket:`5171` to not warn for
overlapping columns in an inheritance scenario where a particular
relationship is local to a subclass and therefore does not represent an
overlap.

Add errors documentation for the warning and also expand
``util.warn()`` to include a code parameter.

Fixes: #6171
Change-Id: Icb1f12d8d645d439ffd2bbb7371c6b00042b6ae3

doc/build/changelog/unreleased_14/6171.rst [new file with mode: 0644]
doc/build/errors.rst
lib/sqlalchemy/orm/relationships.py
lib/sqlalchemy/util/langhelpers.py
test/orm/test_relationships.py

diff --git a/doc/build/changelog/unreleased_14/6171.rst b/doc/build/changelog/unreleased_14/6171.rst
new file mode 100644 (file)
index 0000000..608a77b
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: bug, orm, regression
+    :tickets: 6171
+
+    Scaled back the warning message added in :ticket:`5171` to not warn for
+    overlapping columns in an inheritance scenario where a particular
+    relationship is local to a subclass and therefore does not represent an
+    overlap.
index 814f62a67c71154631e70d5b4df7117707e79bb9..3bd7b7a5b80502e4cc2e7614a27bad345f0ca747 100644 (file)
@@ -1116,6 +1116,83 @@ message for details.
     :ref:`error_bbf0`
 
 
+.. _error_qzyx:
+
+relationship X will copy column Q to column P, which conflicts with relationship(s): 'Y'
+----------------------------------------------------------------------------------------
+
+This warning refers to the case when two or more relationships will write data to the
+same columns on flush, but the ORM does not have any kind of back population configuration
+between the two relationships.  The fix is usually to install the correct
+:paramref:`_orm.back_populates` configuration.   Given the following mapping::
+
+  class Parent(Base):
+      __tablename__ = "parent"
+      id = Column(Integer, primary_key=True)
+      children = relationship("Child")
+
+
+  class Child(Base):
+      __tablename__ = "child"
+      id = Column(Integer, primary_key=True)
+      parent_id = Column(ForeignKey("parent.id"))
+      parent = relationship("Parent")
+
+The above mapping will generate warnings::
+
+  SAWarning: relationship 'Child.parent' will copy column parent.id to column child.parent_id,
+  which conflicts with relationship(s): 'Parent.children' (copies parent.id to child.parent_id).
+
+The relationships ``Child.parent`` and ``Parent.children`` appear to be in conflict.
+The solution is to apply :paramref:`_orm.relationship.back_populates`::
+
+  class Parent(Base):
+      __tablename__ = "parent"
+      id = Column(Integer, primary_key=True)
+      children = relationship("Child", back_populates="parent")
+
+
+  class Child(Base):
+      __tablename__ = "child"
+      id = Column(Integer, primary_key=True)
+      parent_id = Column(ForeignKey("parent.id"))
+      parent = relationship("Parent", back_populates="children")
+
+For more customized relationships where an "overlap" situation may be
+intentional and cannot be resolved, the :paramref:`_orm.relationship.overlaps`
+parameter may specify the names of relationships for which the warning should
+not take effect. This typically occurs for two or more relationships to the
+same underlying table that include custom
+:paramref:`_orm.relationship.primaryjoin` conditions that limit the related
+items in each case::
+
+  class Parent(Base):
+      __tablename__ = "parent"
+      id = Column(Integer, primary_key=True)
+      c1 = relationship(
+          "Child",
+          primaryjoin="and_(Parent.id == Child.parent_id, Child.flag == 0)",
+          backref="parent",
+          overlaps="c2, parent"
+      )
+      c2 = relationship(
+          "Child",
+          primaryjoin="and_(Parent.id == Child.parent_id, Child.flag == 1)",
+          overlaps="c1, parent"
+      )
+
+
+  class Child(Base):
+      __tablename__ = "child"
+      id = Column(Integer, primary_key=True)
+      parent_id = Column(ForeignKey("parent.id"))
+
+      flag = Column(Integer)
+
+
+Above, the ORM will know that the overlap between ``Parent.c1``,
+``Parent.c2`` and ``Child.parent`` is intentional.
+
 AsyncIO Exceptions
 ==================
 
index c1fcc63c1ef8bf05d41d57568f56320ceb00ab27..2ed9d859abf285e0970b238525288a6621c4b9ac 100644 (file)
@@ -364,6 +364,10 @@ class RelationshipProperty(StrategizedProperty):
 
            .. versionadded:: 1.4
 
+           .. seealso::
+
+                :ref:`error_qzyx` - usage example
+
         :param bake_queries=True:
           Use the :class:`.BakedQuery` cache to cache the construction of SQL
           used in lazy loads.  True by default.   Set to False if the
@@ -3423,6 +3427,8 @@ class JoinCondition(object):
                         and self.prop.key not in pr._overlaps
                         and not self.prop.parent.is_sibling(pr.parent)
                         and not self.prop.mapper.is_sibling(pr.mapper)
+                        and not self.prop.parent.is_sibling(pr.mapper)
+                        and not self.prop.mapper.is_sibling(pr.parent)
                         and (
                             self.prop.key != pr.key
                             or not self.prop.parent.common_parent(pr.parent)
@@ -3453,7 +3459,8 @@ class JoinCondition(object):
                                 "'%s' (copies %s to %s)" % (pr, fr_, to_)
                                 for (pr, fr_) in other_props
                             ),
-                        )
+                        ),
+                        code="qzyx",
                     )
                 self._track_overlapping_sync_targets[to_][self.prop] = from_
 
index 51b9071d565bb0f3fee67da004a3d6673549042d..b31f316fee3090f69b17ba33a29b8e2103e00275 100644 (file)
@@ -1604,13 +1604,16 @@ class _hash_limit_string(compat.text_type):
         return hash(self) == hash(other)
 
 
-def warn(msg):
+def warn(msg, code=None):
     """Issue a warning.
 
     If msg is a string, :class:`.exc.SAWarning` is used as
     the category.
 
     """
+    if code:
+        msg = "%s %s" % (msg, exc.SQLAlchemyError(msg, code=code)._code_str())
+
     warnings.warn(msg, exc.SAWarning, stacklevel=2)
 
 
index 8d73cd40e1e6a8d01e164323a4785916f781020d..a065b4046a4780fb0517525cf534aa8109d56daa 100644 (file)
@@ -802,6 +802,29 @@ class OverlappingFksSiblingTest(fixtures.TestBase):
 
         configure_mappers()
 
+    def _fixture_four(self):
+        Base = declarative_base(metadata=self.metadata)
+
+        class A(Base):
+            __tablename__ = "a"
+
+            id = Column(Integer, primary_key=True)
+
+            c_id = Column(ForeignKey("c.id"))
+
+        class B1(A):
+            pass
+
+        class B2(A):
+            pass
+
+        class C(Base):
+            __tablename__ = "c"
+
+            id = Column(Integer, primary_key=True)
+            b1 = relationship(B1, backref="c")
+            b2 = relationship(B2, backref="c")
+
     @testing.provide_metadata
     def _test_fixture_one_run(self, **kw):
         A, AMember, B, BSub1, BSub2 = self._fixture_one(**kw)
@@ -853,6 +876,10 @@ class OverlappingFksSiblingTest(fixtures.TestBase):
             setup_backrefs=False,
         )
 
+    @testing.provide_metadata
+    def test_fixture_four(self):
+        self._fixture_four()
+
     @testing.provide_metadata
     def test_simple_backrefs_works(self):
         self._fixture_two(setup_backrefs=True)