Repository navigation
Description
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