art1415926535 / graphene-sqlalchemy-filter

Filters for Graphene SQLAlchemy integration
https://pypi.org/project/graphene-sqlalchemy-filter/
MIT License
118 stars 34 forks source link

Edge case bug while using FilterableConnectionField within a Union Connection #69

Open FireWhale opened 2 years ago

FireWhale commented 2 years ago

I ran into an interesting bug that is caused by how we calculate the unique key in a NestedFilterableConnectionField:

        # Unique dataloader key for context.
        data_loader_key = tuple((p for p in info.path if isinstance(p, str)))

The bug occurs when there is a polymorphic field that is represented by a Union and a NestedFilterableConnectionField is queried off of the different Union types. The dataloader_key will not be unique even if the root is a different type

Example Setup (Simplified version but hopefully one can fill in the gaps):

class PrimaryRecord(Base):
    id = Column(Integer, primary_key=True, autoincrement=True)
    discriminator = Column(String)
    references = relationship("Reference", secondary='primary_record_reference', backref="primary_records")

class Reference(Base):
    id = Column(Integer, primary_key=True, autoincrement=True)
    url = Column(String, nullable=False)

class PrimaryRecordReference(Base):
    primary_record_id = Column(Integer, ForeignKey('primary_record.id'), primary_key=True)
    reference_id = Column(Integer, ForeignKey('reference.id'), primary_key=True)

class Video(PrimaryRecord):
    id = Column(Integer, ForeignKey(PrimaryRecord.id), primary_key=True)
    __mapper_args__ = {"polymorphic_identity": "Video", 'polymorphic_load': 'inline'}
    title = Column(String, nullable=False)

class Image(PrimaryRecord):
    id = Column(Integer, ForeignKey(PrimaryRecord.id), primary_key=True)
    __mapper_args__ = {"polymorphic_identity": "Image", 'polymorphic_load': 'inline'}
    name = Column(String)

Union classes:

PrimaryRecordUnion(graphene.Union):
    class Meta:
        types=(ImageSQLAlchemyObjectType, VideoSQLAlchemyObjectType)

PrimaryRecordUnionConnection(graphene.Connection):
    class Meta:
        node=PrimaryRecordUnion

union_connection_field = relay.ConnectionField(
    PrimaryRecordUnionConnection
    resolver=resolve_primary_records
)

def resolve_primary_records(record, info, **kwargs):
    return record.primary_records

Sample query:

query { records { edges {
  node {
    __typename
    ... on Video {
      id
      title
      references { edges { node { id } } }
    }
    ... on Image {
      id
      name
      references { edges { node { id } } }
    }
  }
} } }

When using this setup, the data_loader_key for both Image and Video references is 'records', 'edges', 'node', 'references' even though the ModelLoader's parent_model needs to be different (a reference attached to a Video cannot be found when the parent_model is an Image)

I fixed this by simply adding the class name to the unique key, so different parent classes have different data loaders:

        data_loader_key = tuple((p for p in info.path + [root.__class__.__name__] if isinstance(p, str)))

Not sure if it's worth fixing or if there's a better way to do it, but documenting this just in case someone else somehow runs into this particular edge case.