Skip to content

Commit 2d81534

Browse files
test(#1423): add diamond OR + cross-schema trace tests; fix Rule 3 docstring
Addresses the before-merge coverage @ttngu207 and @MilagrosMarin flagged on #1471 (the two they singled out as worth adding before merge): - test_trace_multi_hop_diamond_or_convergence: an ancestor reached via two MULTI-HOP arms (Leaf -> {Left, Right} -> Root), asserting the OR-union {1, 2}. Guards the multi-pass accumulation in the reverse-topo walk, where a regression would silently drop an arm and yield a subset. - test_trace_cross_schema_ancestor: seed and ancestor in different schemas, exercising load_all_upstream's unloaded-ancestor-schema discovery. Runs on both MySQL and PostgreSQL via schema_by_backend. Also fixes the Backward Rule 3 docstring drift (diagram.py): it described child.proj() but the code projects child.proj(*attr_map.keys()) to carry the FK columns. Upward Part-of-Part chains and an isolated non-aliased secondary-FK test remain as noted follow-ups.
1 parent 52ff2ea commit 2d81534

2 files changed

Lines changed: 101 additions & 1 deletion

File tree

src/datajoint/diagram.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,9 @@ def _apply_propagation_rule_upward(self, child_ft, child_attrs, parent_node, att
863863
``child.proj(**{parent: child for child, parent in attr_map.items()})``
864864
— reverses the renaming so the result has parent's column names.
865865
3. Non-aliased AND child restriction attrs ⊄ parent PK:
866-
``child.proj()`` — project child to parent's PK columns.
866+
``child.proj(*attr_map.keys())`` — project child onto its FK columns
867+
(which, being non-aliased, share names with parent's PK columns) so
868+
the subsequent restriction on the parent joins on the right columns.
867869
"""
868870
parent_pk = self.nodes[parent_node].get("primary_key", set())
869871

tests/integration/test_trace.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,104 @@ class Child(dj.Manual):
274274
assert counts[Child.full_table_name] == 2
275275

276276

277+
def test_trace_multi_hop_diamond_or_convergence(schema_by_backend):
278+
"""Diamond: an ancestor reached via two MULTI-HOP paths → OR-union across
279+
both arms. Unlike test_trace_or_convergence_two_paths (adjacent two-edge
280+
case), this forces the reverse-topo walk to accumulate a contributor for the
281+
same ancestor across separate multi-pass arms. A regression that dropped an
282+
OR arm would yield a subset here."""
283+
284+
@schema_by_backend
285+
class Root(dj.Manual):
286+
definition = """
287+
root_id : int32
288+
"""
289+
290+
@schema_by_backend
291+
class Left(dj.Manual):
292+
definition = """
293+
-> Root
294+
left_id : int32
295+
"""
296+
297+
@schema_by_backend
298+
class Right(dj.Manual):
299+
# renamed FK avoids the root_id name collision when Leaf reconverges
300+
definition = """
301+
-> Root.proj(root_id2='root_id')
302+
right_id : int32
303+
"""
304+
305+
@schema_by_backend
306+
class Leaf(dj.Manual):
307+
definition = """
308+
-> Left
309+
-> Right
310+
leaf_id : int32
311+
"""
312+
313+
Root.insert([(1,), (2,), (3,)])
314+
Left.insert([(1, 10)]) # Left row → Root 1
315+
Right.insert([(2, 20)]) # Right row (root_id2=2) → Root 2
316+
# Leaf PK order: root_id, left_id, root_id2, right_id, leaf_id
317+
Leaf.insert([(1, 10, 2, 20, 100)])
318+
319+
trace = dj.Diagram.trace(Leaf & {"leaf_id": 100})
320+
321+
# Root reached via Leaf→Left→Root (root_id=1) OR Leaf→Right→Root
322+
# (root_id2=2 reversed to root_id=2). Union = {1, 2}; Root 3 excluded.
323+
contributing = set(trace[Root].fetch("root_id"))
324+
assert contributing == {1, 2}
325+
326+
327+
def test_trace_cross_schema_ancestor(schema_by_backend, connection_by_backend):
328+
"""Ancestor in a DIFFERENT schema than the seed → load_all_upstream must
329+
discover the unloaded ancestor schema via reverse FK-schema lookup."""
330+
import time
331+
332+
backend = connection_by_backend.adapter
333+
other_name = f"djtest_trace_other_{str(int(time.time() * 1000))[-8:]}"[:64]
334+
if connection_by_backend.is_connected:
335+
try:
336+
connection_by_backend.query(f"DROP DATABASE IF EXISTS {backend.quote_identifier(other_name)}")
337+
except Exception:
338+
pass
339+
other = dj.Schema(other_name, connection=connection_by_backend)
340+
341+
try:
342+
343+
@schema_by_backend
344+
class Upstream(dj.Manual):
345+
definition = """
346+
up_id : int32
347+
---
348+
label : varchar(32)
349+
"""
350+
351+
@other
352+
class Downstream(dj.Manual):
353+
# cross-schema FK: Downstream lives in `other`, Upstream in schema_by_backend
354+
definition = """
355+
-> Upstream
356+
down_id : int32
357+
"""
358+
359+
Upstream.insert([(1, "a"), (2, "b")])
360+
Downstream.insert([(1, 10), (2, 20)])
361+
362+
trace = dj.Diagram.trace(Downstream & {"up_id": 1, "down_id": 10})
363+
364+
assert len(trace[Upstream]) == 1
365+
assert trace[Upstream].fetch1("up_id") == 1
366+
assert trace[Upstream].fetch1("label") == "a"
367+
finally:
368+
if connection_by_backend.is_connected:
369+
try:
370+
connection_by_backend.query(f"DROP DATABASE IF EXISTS {backend.quote_identifier(other_name)}")
371+
except Exception:
372+
pass
373+
374+
277375
def test_trace_seed_with_no_ancestors(schema_by_backend):
278376
"""Tracing from a table with no FK parents → trace contains only the seed."""
279377

0 commit comments

Comments
 (0)