@@ -183,7 +183,8 @@ changes names across an edge, so it is the only place this needs care. The shape
183183of ` r ` decides how. A restriction comes in one of three kinds, and they cross a
184184renamed edge differently:
185185
186- - ** materialized** — a dict, or a sequence of dicts (literal ` attr: value ` rows);
186+ - ** materialized** — a dict of primary-key values, or a sequence of them
187+ (literal ` attr: value ` rows, e.g. ` A.keys() ` );
187188- ** subquery** — a query expression (another table, possibly restricted);
188189- ** string** — a raw SQL predicate over attribute names, e.g. ` 'weight > 10' ` .
189190
@@ -224,20 +225,24 @@ deletions elsewhere, so the delete-order hazard that forces `cascade` to
224225materialize (see below) does not arise here. The whole walk stays symbolic — no
225226subqueries, and the per-table keys stay human-legible.
226227
227- ** When the relabelling is exact.** Only for key attributes the foreign key
228- actually carries across (the referenced attributes — typically the primary key).
229- Two caveats:
230-
231- 1 . Dropping is exact when the removed field is an ** identity attribute the
232- neighbor simply lacks** (the upstream case above — the child's own key). It is
233- lossy only when the removed field was an extra ** constraint** — a secondary
234- attribute that narrows which ` A ` rows exist (e.g. `{'subject_id': 5,
235- 'weight': 10}` ): dropping ` weight` would keep neighbor rows for ` subject_id 5`
236- even if that row's weight is not 10. Resolve this by ** materializing first** —
237- ` (A & key).keys() ` reduces the seed to referenced-key values with the
238- constraint already applied, after which relabel-and-drop is exact.
239- 2 . If the foreign key carries only part of ` A ` 's identity, the relabelled dict is
240- a partial-key restriction on the neighbor — still exact, just not a full key.
228+ ** A key must be primary-key-only to qualify.** A foreign key references the
229+ parent's ** primary key** , so primary-key attributes are exactly the ones that
230+ cross edges — they relabel and propagate. A ** secondary** attribute (e.g.
231+ ` weight ` ) is referenced by no foreign key, so it cannot cross any edge; it exists
232+ only on ` A ` . A ` key ` that mixes in a secondary attribute is therefore ** not a
233+ materialized-key restriction at all** — the secondary part is an opaque
234+ value-condition, which makes the whole seed an ` A & cond ` case (Kind 2 / Kind 3
235+ below). It can be reduced back to Kind 1 by materializing to primary keys first:
236+ ` (A & cond).keys() ` .
237+
238+ (Edge nuance: a secondary attribute that is * itself* a foreign key does propagate
239+ — but only along that one edge. The clean, safe rule is still "primary-key-only →
240+ Kind 1; anything else → opaque condition," since non-primary attributes do not
241+ propagate uniformly.)
242+
243+ Even for a primary-key-only key: if a foreign key carries only part of ` A ` 's
244+ identity, the relabelled dict is a partial-key restriction on the neighbor —
245+ still exact, just not a full key.
241246
242247This is the common, cheap case: "give me everything for this entity"
243248(` A & {'subject_id': 5} ` , or a set of such rows via ` A.keys() ` ).
0 commit comments