Skip to content

sqlalchemy.orm.relationship: "overlaps" errors and behavior are confusing #7309

Description

@jvanasco

Describe the bug

In the attached example, multiple classes have an overlapping sa.orm.relationship to the class RevisionTrackedHistory.

As SQLAlchemy discovers the overlaps, the suggested fixes in the error message iteratively build up:

  • To silence this warning, add the parameter 'overlaps="revision_tracked_history__all"' to the 'Bar.revision_tracked_history__all' relationship.
  • To silence this warning, add the parameter 'overlaps="revision_tracked_history__all,revision_tracked_history__all"' to the 'Biz.revision_tracked_history__all'

Based on the errors and the documentation, when the relationships share the same name, as in this example it becomes unclear if the argument to "overlaps" is supposed to simply be the name of THIS relationship, or of the other relationships.

Note how the relationship name revision_tracked_history__all is duplicated. In the production instance, this gets around 30 repeats of the same relationship name.

Changing the names of each relationship to a discretely indentifiable/unique name will show the name should be the OTHER relationships (e.g. adding the suffix 1/2/3 to each item, in order):

  • To silence this warning, add the parameter 'overlaps="revision_tracked_history__all1"' to the 'Bar.revision_tracked_history__all2" relationship.
  • To silence this warning, add the parameter 'overlaps="revision_tracked_history__all1,revision_tracked_history__all2"' to the 'Biz.revision_tracked_history__all3'

This, however, is still confusing as the items are not easily cross-referenced. They are also not uniquely addressable. One can not configure the relationship with :

overlaps="Foo.revision_tracked_history__all,Bar.revision_tracked_history__all"

SQLAlchemy demands the following for all affected classes

overlaps="revision_tracked_history__all"

IMHO, the "overlaps" error should point to the fully-qualified name of the overlapping relationships (Class + attribute), and the relationship constructor should require fully qualified relationship names as well.

To Reproduce

import sqlalchemy
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import create_engine
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import UnicodeText
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import aliased
from sqlalchemy.orm import relationship


engine = create_engine("sqlite:///:memory:")

metadata_obj = MetaData()
DeclaredTable = declarative_base(metadata=metadata_obj)


class Mixin_RevisionTracking(object):
    _revision_tracked_object_type_id = None


class RevisionTrackedHistory(DeclaredTable):
    __tablename__ = "revision_tracked_history"
    id = Column(Integer, primary_key=True)
    revision_tracked_object_type_id = Column(Integer, nullable=False)
    revision_tracked_object_id = Column(Integer, nullable=False)
    revision_id = Column(Integer, nullable=False)


class Foo(Mixin_RevisionTracking, DeclaredTable):
    __tablename__ = "foo"
    _revision_tracked_object_type_id = 1
    id = Column(Integer, primary_key=True)
    revision_tracked_history__all = relationship(
        "RevisionTrackedHistory",
        primaryjoin="""and_(Foo.id==foreign(RevisionTrackedHistory.revision_tracked_object_id),
                                            RevisionTrackedHistory.revision_tracked_object_type_id==%s
                                            )"""
        % _revision_tracked_object_type_id,
        order_by="RevisionTrackedHistory.revision_id.desc()",
    )


class Bar(Mixin_RevisionTracking, DeclaredTable):
    __tablename__ = "bar"
    _revision_tracked_object_type_id = 2
    id = Column(Integer, primary_key=True)
    revision_tracked_history__all = relationship(
        "RevisionTrackedHistory",
        primaryjoin="""and_(Bar.id==foreign(RevisionTrackedHistory.revision_tracked_object_id),
                                            RevisionTrackedHistory.revision_tracked_object_type_id==%s
                                            )"""
        % _revision_tracked_object_type_id,
        order_by="RevisionTrackedHistory.revision_id.desc()",
    )


class Biz(Mixin_RevisionTracking, DeclaredTable):
    __tablename__ = "biz"
    _revision_tracked_object_type_id = 3
    id = Column(Integer, primary_key=True)
    revision_tracked_history__all = relationship(
        "RevisionTrackedHistory",
        primaryjoin="""and_(Biz.id==foreign(RevisionTrackedHistory.revision_tracked_object_id),
                                            RevisionTrackedHistory.revision_tracked_object_type_id==%s
                                            )"""
        % _revision_tracked_object_type_id,
        order_by="RevisionTrackedHistory.revision_id.desc()",
    )


sqlalchemy.orm.configure_mappers()


metadata_obj.create_all(engine)

Error

/Users/jvanasco/Desktop/example_overlaps.py:74: SAWarning: relationship 'Bar.revision_tracked_history__all' will copy column bar.id to column revision_tracked_history.revision_tracked_object_id, which conflicts with relationship(s): 'Foo.revision_tracked_history__all' (copies foo.id to revision_tracked_history.revision_tracked_object_id). If this is not the intention, consider if these relationships should be linked with back_populates, or if viewonly=True should be applied to one or more if they are read-only. For the less common case that foreign key constraints are partially overlapping, the orm.foreign() annotation can be used to isolate the columns that should be written towards.   To silence this warning, add the parameter 'overlaps="revision_tracked_history__all"' to the 'Bar.revision_tracked_history__all' relationship. (Background on this error at: https://sqlalche.me/e/14/qzyx)
  sqlalchemy.orm.configure_mappers()
/Users/jvanasco/Desktop/example_overlaps.py:74: SAWarning: relationship 'Biz.revision_tracked_history__all' will copy column biz.id to column revision_tracked_history.revision_tracked_object_id, which conflicts with relationship(s): 'Bar.revision_tracked_history__all' (copies bar.id to revision_tracked_history.revision_tracked_object_id), 'Foo.revision_tracked_history__all' (copies foo.id to revision_tracked_history.revision_tracked_object_id). If this is not the intention, consider if these relationships should be linked with back_populates, or if viewonly=True should be applied to one or more if they are read-only. For the less common case that foreign key constraints are partially overlapping, the orm.foreign() annotation can be used to isolate the columns that should be written towards.   To silence this warning, add the parameter 'overlaps="revision_tracked_history__all,revision_tracked_history__all"' to the 'Biz.revision_tracked_history__all' relationship. (Background on this error at: https://sqlalche.me/e/14/qzyx)
  sqlalchemy.orm.configure_mappers()

Versions

  • OS:
  • Python: 3.8
  • SQLAlchemy: 1.4.26
  • Database:
  • DBAPI (eg: psycopg, cx_oracle, mysqlclient):

Additional context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

code review in progresscode has been provided that's in review as PR and/or gerritormuse casenot really a feature or a bug; can be support for new DB features or user use cases not anticipated

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions