@@ -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