Skip to content

Commit f4b727e

Browse files
docs(design): a materialized key must be primary-key-only
Clarify that expand(A & key) uses the relabel fast-path only when key contains solely primary-key attributes of A. A secondary attribute is referenced by no foreign key, so it cannot propagate — a key mixing one in is really an opaque A & cond (Kind 2/3), reducible to Kind 1 via (A & cond).keys(). Per Dimitri's correction.
1 parent fbe11c3 commit f4b727e

1 file changed

Lines changed: 20 additions & 15 deletions

File tree

‎DESIGN-expand-restrict.md‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ changes names across an edge, so it is the only place this needs care. The shape
183183
of `r` decides how. A restriction comes in one of three kinds, and they cross a
184184
renamed 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
224225
materialize (see below) does not arise here. The whole walk stays symbolic — no
225226
subqueries, 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

242247
This 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

Comments
 (0)