Skip to content

Commit a41f660

Browse files
docs(design): self-contained derivation of renamed-FK seed propagation
Adds section 6 deriving how expand(A & r) propagates across renamed foreign keys, distinguishing the dict-key seed (relabel keys through the edge, values unchanged) from the general-condition seed (restrict the neighbor by the renamed projection). Written to stand alone, in DataJoint operators.
1 parent 98c838a commit a41f660

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

DESIGN-expand-restrict.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,106 @@ descendant; `expand` + combine cannot assemble it, `restrict.restrict` does.
150150
group's keys before deleting (delete runs parts-before-masters) matters only
151151
when a traversal feeds `delete`; the read-only closures never pay for it.
152152

153+
## 6. Renamed foreign keys and the seed restriction — a self-contained derivation
154+
155+
This section stands on its own; it does not depend on the rules above.
156+
157+
**Setup.** A foreign key copies a parent table's referenced attributes into the
158+
child. A *renamed* foreign key gives those copied attributes new names in the
159+
child. Record the edge's renaming as pairs `parent_attr -> child_attr`, one per
160+
referenced attribute; a non-renamed foreign key pairs each attribute with itself.
161+
Renaming in a foreign key is a pure attribute rename — it never computes or
162+
changes a value or type. Example:
163+
164+
```python
165+
class Session(dj.Manual):
166+
definition = """
167+
subject_id : int32
168+
session_id : int32
169+
"""
170+
171+
class Analysis(dj.Manual):
172+
definition = """
173+
-> Session.proj(animal='subject_id', sess='session_id')
174+
analysis_id : int32
175+
"""
176+
# edge renaming (Session -> Analysis): subject_id -> animal, session_id -> sess
177+
```
178+
179+
**Goal.** `Diagram.expand(A & r)` seeds table `A` with restriction `r` and grows
180+
the related sub-diagram. Each time we cross a foreign key we must re-express the
181+
restriction in the neighbor's attribute names. Renaming is the only thing that
182+
changes names across an edge, so it is the only place this needs care. The shape
183+
of `r` decides how.
184+
185+
### Form 1 — `A & key`, where `key` is a dict `{attr: value, ...}`
186+
187+
A dict is a set of "attribute equals value" conditions. Crossing a renamed
188+
foreign key, the neighbor's restriction is obtained by **renaming the dict's keys
189+
through the edge, values unchanged**:
190+
191+
- **downstream** (`A` is the parent, neighbor is the child): rewrite each
192+
`subject_id: 5` to `animal: 5` using the edge's `parent_attr -> child_attr`
193+
pairs. `A & {'subject_id': 5}` induces `child & {'animal': 5}`.
194+
- **upstream** (`A` is the child, neighbor is the parent): apply the pairs the
195+
other way — `A & {'animal': 5}` induces `parent & {'subject_id': 5}`.
196+
197+
Multi-hop composes: the renamings chain, so a key is relabelled edge by edge
198+
(`subject_id: 5``animal: 5``creature: 5`). This is exact because the
199+
renaming is pure (values and types are preserved) and the attribute's identity
200+
across the edge is fixed by the edge's pairing, not by any coincidental match of
201+
names.
202+
203+
**When the shortcut is exact.** Only for key attributes the foreign key actually
204+
carries across (the referenced attributes — typically the primary key). Two
205+
caveats:
206+
207+
1. A dict entry on an attribute the edge does **not** carry (a secondary
208+
attribute of `A`, or one the foreign key doesn't reference) has no name on the
209+
neighbor, so it can't be relabelled. If that entry changes which `A` rows
210+
exist, dropping it would over-select the neighbor. So Form 1 applies when
211+
`key`'s attributes are among the edge's referenced attributes; otherwise the
212+
non-carried part must be enforced as in Form 2.
213+
2. If the foreign key carries only part of `A`'s identity, the relabelled dict is
214+
a partial-key restriction on the neighbor — still exact, just not a full key.
215+
216+
This is the common, cheap case ("give me everything for this entity",
217+
`A & {'subject_id': 5}`): the per-table restriction stays a dict, and traversal
218+
is a name-substitution walk — no subqueries, and the per-table keys stay
219+
human-legible.
220+
221+
### Form 2 — `A & cond`, where `cond` is a general condition
222+
223+
A general condition — a SQL predicate (`'weight > 10'`), a query expression, a
224+
list — is not a set of equalities on the foreign-key attributes, so there are no
225+
keys to relabel. Propagate it as a **restriction by the renamed, projected
226+
seed**: restrict `A` by `cond`, project it onto the referenced attributes under
227+
the neighbor's names, and restrict the neighbor by that.
228+
229+
- **downstream:** `child & (A & cond).proj(animal='subject_id', sess='session_id')`
230+
— project restricted `A` to the referenced attributes under the child's names,
231+
then restrict the child by it.
232+
- **upstream:** `parent & (A & cond).proj(subject_id='animal', session_id='sess')`
233+
— project under the parent's names (the renaming reversed).
234+
235+
This is always correct, including when `cond` touches attributes the foreign key
236+
doesn't carry: those simply constrain which `A` rows the projection sees.
237+
238+
### How the two relate
239+
240+
Form 1 is the special case of Form 2 where `cond` is a dict of equalities on the
241+
carried attributes: there, restricting the neighbor by
242+
`(A & key).proj(...renamed...)` selects exactly the neighbor rows the relabelled
243+
dict does — so we skip building the projection and just rename keys. Form 2 is
244+
the fallback whenever that equivalence doesn't hold.
245+
246+
**Consequence for `expand`.** Per reached table, `expand` can carry either a
247+
relabelled dict (Form 1 — when the seed is a qualifying dict and every edge on
248+
the path is a rename over carried attributes) or a relational restriction
249+
(Form 2). Prefer the dict path when available: it is symbolic, composes by
250+
chaining the edge renamings, and yields legible per-table keys — this is the
251+
"update the key names as we traverse" behavior.
252+
153253
## Summary
154254

155255
| | additive (grow) | subtractive (carve) |

0 commit comments

Comments
 (0)