Skip to content

Joining with a union on the right raises AssertionError when the two sides share no attributes #1659

Open

Description

@dimitri-yatsenko

What happens

A * (B + C) raises a bare AssertionError when A and the union B + C have no attribute in common, so the join is a cross join. The same join with the union on the left, (B + C) * A, returns the correct rows. A union on the right that shares an attribute with the left side also works.

Reproduction

On master (3c912c4), the same on MySQL 8.0 and PostgreSQL 15:

import datajoint as dj

schema = dj.Schema("join_union_repro")

@schema
class Parent(dj.Manual):
    definition = """
    parent_id : int32
    ---
    label : varchar(8)
    """

@schema
class Child(dj.Manual):
    definition = """
    -> Parent
    child_id : int32
    ---
    color : varchar(8)
    """

@schema
class ParentInfo(dj.Manual):
    definition = """
    -> Parent
    ---
    info : varchar(8)
    """

Parent.insert([{"parent_id": 1, "label": "a"}, {"parent_id": 2, "label": "b"}, {"parent_id": 3, "label": "a"}])
Child.insert([
    {"parent_id": 1, "child_id": 1, "color": "red"},
    {"parent_id": 1, "child_id": 2, "color": "blue"},
    {"parent_id": 2, "child_id": 1, "color": "blue"},
])
ParentInfo.insert([{"parent_id": 3, "info": "x"}])

pids = (Parent & {"parent_id": 1}).proj(pid="parent_id") + ParentInfo.proj(pid="parent_id")
pids.to_dicts()       # [{'pid': 1}, {'pid': 3}]

Child.proj() * pids   # AssertionError
  File ".../datajoint/expression.py", line 436, in join
    assert len(result.support) == len(result._joins) + 1
AssertionError
Query Observed (MySQL 8.0 and PostgreSQL 15) Expected
Child.proj() * pids AssertionError 6 rows: each of the 3 children with pid 1 and with pid 3
Child * pids AssertionError the same 6 rows, with color
Child.proj() * (Parent.proj(pid="parent_id") + ParentInfo.proj(pid="parent_id")) AssertionError 9 rows: each child with pid 1, 2 and 3
pids * Child.proj() (union on the left) 6 rows, correct 6 rows
Child.proj() * ((Parent & {"parent_id": 1}).proj() + ParentInfo.proj()) (shares parent_id) the 2 children of parent 1, correct the same

Cause

QueryExpression.join decides separately for each side whether to wrap it in a subquery. For the right side (src/datajoint/expression.py:410-415), the check for a union tests the left side:

        need_subquery2 = (
            need_subquery2
            or isinstance(other, Aggregation)
            or any(n in other.heading.new_attributes for n in join_attributes)
            or isinstance(self, Union)      # should be: isinstance(other, Union)
        )

A union has no FROM clause of its own. Until it is wrapped, its support is its two operands. join() copies both of them into the result (result._support = self.support + other.support, line 428) but adds only one join step (line 431), so the check at line 436 fails.

When the two sides share an attribute, another rule wraps the union anyway (lines 420-421: len(other.support) > 1 and join_attributes), which hides the mistake. A cross join has no shared attributes, so nothing wraps the union.

The line came in with the fix for #930 (2021). That fix covered the union on the left, and the copy for the right side kept self.

With assertions disabled (python -O), the query runs without an error and returns wrong rows. The second operand of the union is left out of the FROM clause, and pid is read from Child.parent_id: Child.proj() * pids returns (1, 1, 1), (1, 2, 1), (2, 1, 2) instead of the 6 rows above.

The same mistake also wraps the right side whenever the left side is a union, as in pids * Child.proj() (... CROSS JOIN (SELECT parent_id, child_id FROM child) AS ...). That subquery isn't needed, but the results are correct.

Expected

Child.proj() * pids returns the full cross product, like pids * Child.proj() does. The fix is isinstance(other, Union) at line 414. With that one change, all the queries in the table return the expected rows on both backends. The existing test (test_union_join in tests/integration/test_aggr_regressions.py) only puts the union on the left, so a test with the union on the right of a cross join should come with the fix.

Activity

  1. added this to the v2.3.7 milestone on Nov 6, 2026
  2. added
    bugIndicates an unexpected problem or unintended behavior
    on Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIndicates an unexpected problem or unintended behavior

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions