@@ -323,6 +323,85 @@ yields legible per-table keys, and is delete-safe — this is the "update the ke
323323names as we traverse" behavior. A live seed can be materialized up front to get
324324all of that.
325325
326+ ## 7. The group rule and the relabel fast-path — how a key crosses master↔part
327+
328+ Section 6 handled ordinary foreign keys. Part tables are the other complication.
329+ This section works out how a materialized key crosses a master↔part boundary,
330+ and shows that the group rule (R2) needs ** no new key machinery** — it is the
331+ existing relabel fast-path, run twice, with the part-specific attribute
332+ deliberately lost in between.
333+
334+ ** Setup.** Take a master ` Session ` with primary key ` session_id ` , and its part
335+ ` Session.Trial ` with primary key ` (session_id, trial_id) ` . A part always carries
336+ its master's full primary key inside its own — the ` -> master ` reference is a
337+ * primary, non-renamed* foreign key, so on that edge the relabel is the identity
338+ (names are shared). The part adds its own key attribute (` trial_id ` ) on top. R2:
339+ a master and its parts are one entity — a restriction touching any part lifts to
340+ the master, and the master brings in * all* its parts.
341+
342+ ### master to part (downstream): nothing new
343+
344+ ` Session & {'session_id': 5} ` crosses down to ` Session.Trial ` by the ordinary
345+ downstream relabel: keep ` session_id ` , leave ` trial_id ` unconstrained. The result
346+ is the partial key ` {'session_id': 5} ` , which selects * every* trial of session 5.
347+ "All parts follow the master" falls straight out of the down rule.
348+
349+ ### part to master (upstream): the existential lift is relabel-drop
350+
351+ ` Session.Trial & {'session_id': 5, 'trial_id': 2} ` going up: the master's full
352+ primary key (` session_id ` ) is present, so the fast-path fires — ** relabel-drop** :
353+ keep ` session_id ` , drop the part-specific ` trial_id ` (absent on the master) →
354+ ` {'session_id': 5} ` .
355+
356+ The "existential" needs no extra mechanism: a * sequence* of part keys spanning
357+ several trials of session 5 all drop to the same ` {'session_id': 5} ` , and the
358+ master key-set ** de-duplicates** . Dropping the part-specific attribute collapses
359+ sibling-part keys onto one master key — the OR-over-siblings is free.
360+
361+ ### master to all parts (re-expansion): not a relabel of the seed
362+
363+ R2's atomicity requires that once the master is in, * all* its parts come —
364+ including trials the seed never named. That is not obtainable by relabelling the
365+ seed key; it is a ** fresh downstream step from the recovered master key** :
366+
367+ ` {'session_id': 5} ` on ` Session ` → (down relabel) → ` {'session_id': 5} ` on
368+ ` Session.Trial ` , which drops the ` trial_id ` constraint and so * widens* from
369+ "trial 2" to all trials of session 5.
370+
371+ ### The signature: the round trip is lossy, and the loss is atomicity
372+
373+ Follow ` trial_id ` around the loop:
374+
375+ 1 . seed part key ` {'session_id': 5, 'trial_id': 2} `
376+ 2 . lift (up, drop) → master ` {'session_id': 5} ` — ` trial_id ` gone
377+ 3 . re-expand (down) → part ` {'session_id': 5} ` — ` trial_id ` cannot be restored
378+
379+ The part-specific constraint is destroyed by the lift and cannot be recovered on
380+ the way back down. Stated as a rule: ** a key that reaches a part via its master
381+ carries no part-specific constraint.** That loss * is* compositional atomicity in
382+ key terms — the whole part-group comes along precisely because the returning key
383+ no longer distinguishes trial 2 from its siblings. The lift * narrows* the key (to
384+ the master); the re-expansion * widens* it (to all parts); the asymmetry is the
385+ point.
386+
387+ ### Two corollaries
388+
389+ - ** Materialized stays materialized.** Relabel-drop of literal values yields
390+ literal values, and the re-expansion is a relabel of that literal master key.
391+ So a materialized-key seed stays materialized through the entire
392+ part→master→parts round trip, and is therefore ** delete-safe for free** — the
393+ delete-order materialization (#1496 ) only ever has to fire for * live*
394+ (subquery/string) seeds.
395+ - ** One mechanism, three call sites.** The same round trip fires in cascade-down
396+ (` part_integrity="cascade" ` , #1429 ), in trace-up (#1481 , a master drags in its
397+ parts), and in ` restrict ` (#1501 , where a part * predicate* is promoted to its
398+ master and the whole group is kept — not carved part-by-part). R2 is
399+ direction-agnostic; only what * triggers* it differs.
400+
401+ So R2 adds no new key machinery: relabel-drop (lift) plus a downstream relabel
402+ (re-expand) over the existing fast-path. The only non-relabel act is recognizing
403+ that the re-expansion is new growth from the master, not a transform of the seed.
404+
326405## Summary
327406
328407| | additive (grow) | subtractive (carve) |
0 commit comments