|
| 1 | +import datajoint as dj |
1 | 2 | from datajoint.declare import declare |
2 | 3 |
|
3 | 4 | from tests.schema_advanced import ( |
@@ -51,3 +52,40 @@ def test_delete(schema_adv): |
51 | 52 | to_delete = len(parent & "11 in (person_id, parent)") |
52 | 53 | (person & "person_id=11").delete() |
53 | 54 | assert to_delete and len(parent) == original_len - to_delete |
| 55 | + |
| 56 | + |
| 57 | +def test_parallel_edges_to_same_parent(schema_adv): |
| 58 | + """Two foreign keys from one child to the same parent are preserved as two |
| 59 | + distinct parallel edges (``nx.MultiDiGraph`` migration, #1492 / PR #1508). |
| 60 | +
|
| 61 | + ``LocalSynapse`` references ``Cell`` twice — |
| 62 | + ``-> Cell.proj(presynaptic='cell')`` and ``-> Cell.proj(postsynaptic='cell')`` |
| 63 | + — so ``Cell`` must appear as TWO parent edges of ``LocalSynapse``. The |
| 64 | + pre-migration name-keyed dependency dict collapsed these into a single entry; |
| 65 | + this test locks the parallel-edges capability across all three layers that |
| 66 | + consume it: the ``Dependencies`` accessors, the dependency graph itself, and |
| 67 | + the rendered ``Diagram``. |
| 68 | + """ |
| 69 | + cell_name = Cell.full_table_name |
| 70 | + local_name = LocalSynapse.full_table_name |
| 71 | + |
| 72 | + # (a) Table.parents() surfaces BOTH edges to Cell, with distinct rename maps. |
| 73 | + cell_edges = [props for name, props in LocalSynapse().parents(foreign_key_info=True) if name == cell_name] |
| 74 | + assert len(cell_edges) == 2, "LocalSynapse must expose two parallel FK edges to Cell" |
| 75 | + renamed = {child for props in cell_edges for child, parent in props["attr_map"].items() if parent == "cell"} |
| 76 | + assert renamed == {"presynaptic", "postsynaptic"} |
| 77 | + # the two edges must carry distinct keys (``tuple(attr_map)``) — the uniqueness |
| 78 | + # the keyed-edge migration relies on to keep them apart. |
| 79 | + assert len({tuple(sorted(props["attr_map"].items())) for props in cell_edges}) == 2 |
| 80 | + |
| 81 | + # (b) The dependency graph keeps both edges — cascade/trace traverse per edge. |
| 82 | + deps = LocalSynapse.connection.dependencies |
| 83 | + deps.load(force=False) |
| 84 | + assert deps.number_of_edges(cell_name, local_name) == 2 |
| 85 | + # symmetric view from the parent side (children direction) |
| 86 | + local_as_child = [name for name, _ in Cell().children(foreign_key_info=True) if name == local_name] |
| 87 | + assert len(local_as_child) == 2 |
| 88 | + |
| 89 | + # (c) The rendered Diagram (a MultiDiGraph over full table names) draws both. |
| 90 | + diagram = dj.Diagram(schema_adv) |
| 91 | + assert diagram.number_of_edges(cell_name, local_name) == 2 |
0 commit comments