From 179df4fd16dd80d5d4d59427b3a4ebe27db33f00 Mon Sep 17 00:00:00 2001 From: Ashwin A Nayar Date: Wed, 24 Dec 2025 21:41:23 +0530 Subject: [PATCH] fix(#64): cartesian product in reln reflection to non-versioned classes In many-to-many relationships when one of the classes is not versioned, we were not including the secondaryjoin in the filter criteria leading to a cartesian product. --- sqlalchemy_history/relationship_builder.py | 7 ++++++- .../test_non_versioned_classes.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/sqlalchemy_history/relationship_builder.py b/sqlalchemy_history/relationship_builder.py index 3610d4f..677f0b0 100644 --- a/sqlalchemy_history/relationship_builder.py +++ b/sqlalchemy_history/relationship_builder.py @@ -83,7 +83,12 @@ def criteria(self, obj): return self.many_to_one_criteria(obj) else: reflector = VersionExpressionReflector(obj, self.property) - return reflector(self.property.primaryjoin) + criteria = reflector(self.property.primaryjoin) + + # For many-to-many relationships, we also need to include the secondary join + if direction.name == "MANYTOMANY" and self.property.secondaryjoin is not None: + criteria = sa.and_(criteria, reflector(self.property.secondaryjoin)) + return criteria def many_to_many_criteria(self, obj): """Returns the many-to-many query. diff --git a/tests/relationships/test_non_versioned_classes.py b/tests/relationships/test_non_versioned_classes.py index d1ee3ec..b0c5fd8 100644 --- a/tests/relationships/test_non_versioned_classes.py +++ b/tests/relationships/test_non_versioned_classes.py @@ -101,3 +101,22 @@ def test_single_insert(self): self.session.commit() assert len(article.versions[0].tags) == 1 assert isinstance(article.versions[0].tags[0], self.Tag) + + def test_no_cartesian_product_with_multiple_unrelated_tags(self): + # Create an article with one tag + article = self.Article(name="Some article") + tag1 = self.Tag(name="tag1") + article.tags.append(tag1) + self.session.add(article) + self.session.commit() + + # Create another article with a different tag + article2 = self.Article(name="Another article") + tag2 = self.Tag(name="tag2") + article2.tags.append(tag2) + self.session.add(article2) + self.session.commit() + + # Ensure the first article's version only has its own tag, not all tags + assert len(article.versions[0].tags) == 1 + assert article.versions[0].tags[0] == tag1