Repository navigation
Description
What happens
dj.U("parent_id") & Child lists each parent_id once: its SQL is SELECT DISTINCT parent_id FROM child. When this expression is used inside another query (a join on either side, extend, aggr, or dj.U(...).aggr), the DISTINCT is dropped. The operand is joined as the plain child table, so it contributes one row per child instead of one row per parent. Joins return repeated rows and aggr counts are too high. Nothing warns.
len() of the same join is correct, because it counts distinct primary-key values. So len(q) and len(q.to_dicts()) disagree. The preview shows four rows above (Total: 2).
Restricting by the expression works: Parent & (dj.U("parent_id") & Child) and Parent - (dj.U("parent_id") & Child) return the right parents.
Reproduction
On master (3c912c4), the results are the same on MySQL 8.0 and PostgreSQL 15:
import datajoint as dj
schema = dj.Schema("u_distinct_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)
"""
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"},
{"parent_id": 2, "child_id": 2, "color": "blue"},
])
has_children = dj.U("parent_id") & Child # alone: parents 1, 2
colors = dj.U("parent_id", "color") & Child # alone: (1, blue), (1, red), (2, blue)| Query | Expected | Actual |
|---|---|---|
Parent * has_children |
(1, a), (2, b) |
(1, a), (1, a), (2, b), (2, b) |
has_children * Parent |
(1, a), (2, b) |
(1, a), (1, a), (2, b), (2, b) |
(has_children & "parent_id < 3") * Parent |
(1, a), (2, b) |
(1, a), (1, a), (2, b), (2, b) |
Parent * colors |
(1, blue), (1, red), (2, blue) |
(2, blue) appears twice |
Parent.extend(has_children) |
parents 1, 2, 3 | 1, 1, 2, 2, 3 |
has_children.extend(Parent) |
(1, a), (2, b) |
each twice |
Parent.aggr(colors, n="count(color)") (colors per parent) |
{1: 2, 2: 1, 3: 0} |
{1: 2, 2: 2, 3: 0} |
Parent.aggr(has_children, n="count(*)", exclude_nonmatching=True) |
{1: 1, 2: 1} |
{1: 2, 2: 2} |
has_children.aggr(Parent, n="count(label)") |
{1: 1, 2: 1} |
{1: 2, 2: 2} |
dj.U("color").aggr(colors, n="count(*)") (parents per color) |
{blue: 2, red: 1} |
{blue: 3, red: 1} |
dj.U().aggr(dj.U("color") & Child, n="count(*)") (number of colors) |
2 |
4 |
len(Parent * has_children) |
2 |
2 (correct, but to_dicts() returns 4 rows) |
Parent & has_children, Parent - has_children |
parents 1, 2 / parent 3 | correct |
The SQL for Parent * has_children is a plain join with no DISTINCT:
SELECT `parent_id`,`label` FROM `parent` JOIN `child` USING (`parent_id`)Cause
dj.U.__and__ (src/datajoint/expression.py:1507) returns a copy of the table expression with _distinct = True (line 1513). Only make_sql reads that flag (line 162), so DISTINCT appears only when the expression is the outermost query or is already wrapped in a subquery.
QueryExpression.join (line 343) decides whether to wrap each operand in a subquery (lines 400–425). The checks are:
- the operands' source tables share columns other than the join columns
- the operand is an aggregation or a union
- a join column is computed
- the operand already joins several tables
_distinct is not among them. An unwrapped operand is merged in by its tables, joins and conditions (lines 426–436). The result is a new expression with _distinct set to False, so the DISTINCT is lost. extend (line 490) and aggr (Aggregation.create, line 1341) go through the same join(), with the expression on either side.
When the operand is wrapped for another reason, the DISTINCT survives. For example, Child * (dj.U("parent_id") & Child) is correct, because both operands come from child and share child_id and color.
dj.U.aggr (line 1532) does not call join(). It builds the aggregation directly from the group's tables, joins and conditions (lines 1566–1573), and drops _distinct the same way.
__len__ (line 1149) counts distinct primary-key values (lines 1163–1174), which hides the repeated rows.
DataJoint 0.14 has the same code, so this is not new in 2.x.
Expected
An operand that has _distinct set should be wrapped in a subquery, as an aggregation or a union already is. That means wrapping it in join() for either operand, which covers *, extend, and aggr with the expression as the group or as the grouping side. dj.U.aggr needs the same wrapping for its group. Then SELECT DISTINCT … runs before the join or the GROUP BY. With this wrapping tried locally, every case in the table is correct on both backends, and the existing tests for dj.U, joins, aggr and grouping still pass.
#1657 adds the same kind of check for dj.Top at these places in join() and dj.U.aggr. It does not cover _distinct, and this reproduction fails the same way on that branch. The _distinct check belongs next to the _top one.
Activity
fix(query): emit DISTINCT only where rows can repeat; keep it through…