column overlap warning in relationship needs tuning for sibling classes

Issue #4078 new
Mike Bayer repo owner created an issue
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.engine import create_engine
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.schema import Column, ForeignKey, ForeignKeyConstraint
from sqlalchemy.types import Integer, String
Base = declarative_base()


class A(Base):

    __tablename__ = 'a'

    id = Column(Integer, primary_key=True)
    a_members = relationship('AMember', backref='a')


class AMember(Base):

    __tablename__ = 'a_member'

    a_id = Column(Integer, ForeignKey('a.id'), primary_key=True)
    a_member_id = Column(Integer, primary_key=True)


class B(Base):
    __tablename__ = 'b'

    __mapper_args__ = {
        'polymorphic_on': 'type'
    }

    id = Column(Integer, primary_key=True)
    type = Column(String)

    a_id = Column(Integer, ForeignKey('a.id'), nullable=False)
    a_member_id = Column(Integer)

    __table_args__ = (
        ForeignKeyConstraint(
            ('a_id', 'a_member_id'),
            ('a_member.a_id', 'a_member.a_member_id')),
    )

    # if viewonly is removed, warning should emit:
    # "relationship 'B.a' will copy column a.id to column b.a_id, which
    # "conflicts with relationship(s): 'BSub2.a_member' (copies a_member.a_id to b.a_id). ""
    a = relationship('A', viewonly=True)

    # if uncommented, warning should emit:
    # relationship 'B.a_member' will copy column a_member.a_id to column
    # b.a_id, which conflicts with relationship(s): 'BSub1.a' (copies a.id to b.a_id)
    # a_member = relationship('AMember')

# however, *no* warning should be emitted otherwise.


class BSub1(B):

    a = relationship('A')

    __mapper_args__ = {'polymorphic_identity': 'bsub1'}


class BSub2(B):

    a_member = relationship('AMember')

    @classmethod
    def __declare_first__(cls):
        cls.__table__.append_constraint(
            ForeignKeyConstraint(
                ('a_id', 'a_member_id'),
                ('a_member.a_id', 'a_member.a_member_id'))
        )

    __mapper_args__ = {'polymorphic_identity': 'bsub2'}


engine = create_engine('sqlite://', echo=True)
Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine, autoflush=False)
session = Session()


bsub2 = BSub2()
am1 = AMember(a_member_id=1)
a1 = A(a_members=[am1])
bsub2.a_member = am1

bsub1 = BSub1()
a2 = A()
bsub1.a = a2

session.add_all([bsub1, bsub2, am1, a1, a2])
session.commit()

assert bsub1.a is a2
assert bsub2.a is a1

# meaningless, because BSub1 doesn't have a_member
bsub1.a_member = am1

# meaningless, because BSub2's ".a" is viewonly=True
bsub2.a = a2

session.commit()
assert bsub1.a is a2  # beacuse bsub1.a_member is not a relationship
assert bsub2.a is a1  # because bsub2.a is viewonly=True

# everyone has a B.a relationship
print session.query(B, A).outerjoin(B.a).all()

patch:

diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 1d172f71a..edfe45030 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -2450,6 +2450,15 @@ class Mapper(InspectionAttr):
             m = m.inherits
         return bool(m)

+    def shares_lineage(self, other):
+        """Return True if either this mapper or given mapper inherit from the other.
+
+        This is a bidirectional form of "isa".
+
+        """
+
+        return self.isa(other) or other.isa(self)
+
     def iterate_to_root(self):
         m = self
         while m:
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index 94c0d6694..89ef641f8 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -2693,6 +2693,7 @@ class JoinCondition(object):
                 prop_to_from = self._track_overlapping_sync_targets[to_]
                 for pr, fr_ in prop_to_from.items():
                     if pr.mapper in mapperlib._mapper_registry and \
+                        self.prop.parent.shares_lineage(pr.parent) and \
                         fr_ is not from_ and \
                             pr not in self.prop._reverse_property:
                         other_props.append((pr, fr_))

Comments (2)

  1. Mike Bayer reporter

    whoops, this should also not emit a warning and it still is:

    class B(Base):
        __tablename__ = 'b'
    
        __mapper_args__ = {
            'polymorphic_on': 'type'
        }
    
        id = Column(Integer, primary_key=True)
        type = Column(String)
    
        a_id = Column(Integer, ForeignKey('a.id'), nullable=False)
        a_member_id = Column(Integer)
    
        __table_args__ = (
            ForeignKeyConstraint(
                ('a_id', 'a_member_id'),
                ('a_member.a_id', 'a_member.a_member_id')),
        )
    
        # if viewonly is removed, warning should emit:
        # "relationship 'B.a' will copy column a.id to column b.a_id, which
        # "conflicts with relationship(s): 'BSub2.a_member' (copies a_member.a_id to b.a_id). ""
        a = relationship('A') #, viewonly=True)
    
        # if uncommented, warning should emit:
        # relationship 'B.a_member' will copy column a_member.a_id to column
        # b.a_id, which conflicts with relationship(s): 'BSub1.a' (copies a.id to b.a_id)
        # a_member = relationship('AMember')
    
    # however, *no* warning should be emitted otherwise.
    
    
    class BSub1(B):
    
    #    a = relationship('A')
    
        __mapper_args__ = {'polymorphic_identity': 'bsub1'}
    
    
    class BSub2(B):
    
        a = relationship('A', viewonly=True)
        a_member = relationship('AMember')
    
        @classmethod
        def __declare_first__(cls):
            cls.__table__.append_constraint(
                ForeignKeyConstraint(
                    ('a_id', 'a_member_id'),
                    ('a_member.a_id', 'a_member.a_member_id'))
            )
    
        __mapper_args__ = {'polymorphic_identity': 'bsub2'}
    
  2. Mike Bayer reporter

    OK next patch, this is way better though have to think about it more

    diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
    index 94c0d6694..936b43b28 100644
    --- a/lib/sqlalchemy/orm/relationships.py
    +++ b/lib/sqlalchemy/orm/relationships.py
    @@ -1803,6 +1803,15 @@ class RelationshipProperty(StrategizedProperty):
                     (self.key, self.parent.class_)
                 )
    
    +    def _persists_for(self, mapper):
    +        """Return True if this property will persist values on behalf
    +        of the given mapper.
    +
    +        """
    +
    +        return self.key in mapper.relationships and \
    +            mapper.relationships[self.key] is self
    +
         def _columns_are_mapped(self, *cols):
             """Return True if all columns in the given collection are
             mapped by the tables referenced by this :class:`.Relationship`.
    @@ -2693,8 +2702,10 @@ class JoinCondition(object):
                     prop_to_from = self._track_overlapping_sync_targets[to_]
                     for pr, fr_ in prop_to_from.items():
                         if pr.mapper in mapperlib._mapper_registry and \
    +                        self.prop._persists_for(pr.parent) and \
                             fr_ is not from_ and \
                                 pr not in self.prop._reverse_property:
    +
                             other_props.append((pr, fr_))
    
                     if other_props:
    
  3. Log in to comment