Skip to content

docs(specs): normative spec of Diagram.expand, with a traversal shape catalog - #249

Open
dimitri-yatsenko wants to merge 13 commits into
mainfrom
docs/diagram-traversal-derivation
Open

dimitri-yatsenko wants to merge 13 commits into
mainfrom
docs/diagram-traversal-derivation

Conversation

@dimitri-yatsenko

@dimitri-yatsenko dimitri-yatsenko commented Aug 10, 2026 •

Copy link
Copy Markdown
Member

This PR replaces the draft "Traversal algebra" in reference/specs/diagram.md with a normative specification of Diagram.expand, the one traversal behind cascade and trace, and adds a catalog of worked examples that the test suite will mirror one to one.

What the spec defines

The new Traversal specification section of diagram.md:

  • Base principles. P1 referential integrity, P2 compositional integrity, P3 reproducibility, P4 minimality.
  • Model. Foreign-key references ref_e, with SQL's MATCH SIMPLE rule for NULLs (A2); parallel foreign keys as separate references; owned parts (A1): a part is owned when a foreign key in its primary key references the master or an owned part of the same master, renames allowed, and its owning attributes (what that chain maps onto the master's key) define its master row and its instance; the row relations down = child ∪ inst and up = parent ∪ inst. A part that is not owned is a plain table, with a warning at declaration.
  • Operations. D(S) and U(S) as closures under down and up; expand(seed, "both") = U(D(S)), and why neither D(U(S)) nor D(S) ∪ U(S) works for export; the three part_integrity modes in terms of D and D1; trace = U; self.upstream for a row that does not exist yet.
  • Derived results T1–T8, each with a short justification: closure properties, additivity, the duality between D and U, delete safety, the read boundary, the properties of both, the enforce equivalence D1 lift-closed ⇔ D1 = D, and topo_sort succeeding exactly when the group quotient is acyclic.
  • Computation. The edge rule R1 downward (restriction, with the narrow conditions under which a condition may be copied) and upward (always a restriction); the group rule R2 (an existential, one-step lift that renames the owning attributes onto the master's key, then re-expansion); one pass over the group quotient; the exact condition under which a group needs iteration; seeds and when materialized keys may be relabeled; the plan; materialization only at delete time; concurrency.
  • The contract for expanded diagrams: node set, d[T], counts(), iteration, copying, +, the operators that raise, and prune().
  • Retirement of restrict (deprecated in 2.4, removed in 3.0), with migration recipes.

New and changed 2.4 behavior is marked with version-added / version-changed / version-deprecated admonitions.

What it removes

  • The draft's mechanism-only derivation, including the relabel discussion that did not say when it was sound.
  • restrict as an operation of the model, AND-convergence, and the "Rule 1–3" propagation section in Operational Methods.
  • In cascade.md: the multi-pass loop, the part-to-master path walk, the old worked examples built on that walk, and the internal function names. It also removes the contradiction about cost: the preview never materializes keys; materialization happens only at delete time.

Shape catalog

New page reference/specs/traversal-shapes.md (added to the nav under Query Algebra, after Diagram). One subsection per shape, each with a tiny schema, a few rows, seeds, and the exact rows expand returns down, up, and both where that is informative:

  • foreign-key shapes S1–S7 (identity, secondary, nullable with a shared column, renamed, parallel, diamond, diamond through a secondary foreign key);
  • group shapes G1–G7 (lift, part-of-part with two chains, a second renamed foreign key to the master, a renamed sibling foreign key chained across instances, merge table, a part its master does not own, and the owned part-of-part chain with renamed attributes from Cascade may fail when Part table references another Part with renamed foreign keys datajoint-python#1429 Case 1);
  • seed shapes K1–K6 (restricted by a descendant, self-referencing, seed kinds including the NOT IN NULL trap, dj.Top, empty, unrestricted);
  • graph shapes X1–X2 (unloaded downstream schema, group cycle refused at declaration).

Every result was checked against a small executable model of D and U written from the definitions, and independently against the reference model in datajoint/datajoint-python#1621, whose shape tests copy each row from this catalog by hand.

cascade.md and trace.md

  • cascade.md is now the down case: what each part_integrity mode deletes, the row-level enforce check before any DELETE, the symbolic preview, the delete steps, delete-time materialization, concurrency, and a worked example of a seed restricted by a descendant.
  • trace.md is now the up case: upward crossings are always restrictions, a trace includes the parts of ancestor masters and their ancestors (Diagram.trace: descend through master-part boundaries (Merge-table upstream tracing) datajoint-python#1481), and self.upstream is built from the parents of the row being computed. The published user-facing material is kept.

What reviewers should check

  1. The definitions. Do down, up, D, U, and D1 say what a delete, a trace, and an export should select? Do the ownership rule (A1) and its owning attributes match the declaration check and ordering in fix(declare): refuse a part reference that closes a master-part group cycle datajoint-python#1620?
  2. The derived results. Does each justification of T1–T8 hold? T7 and T8 rely on every owned part being reachable from its master along its owning chain.
  3. The catalog rows. Each expected row set should follow from the definitions alone. Please try a few by hand, especially G3, G4, G5, G7, and K3.
  4. The 2.4 behavior changes the spec implies, which the release notes will carry:
  5. Wording. Propagation is described as restriction (&) throughout. Terms that are not DataJoint canon (closure, least fixed point, group quotient, lift-closed) are defined where first used.

Decisions for reviewers to confirm

  • The enforce preview. Diagram.cascade(seed) with enforce holds D1(S) and does not raise; delete() runs the check and raises before deleting anything.

  • + on expanded diagrams. d1 + d2 takes the union of rows for two expanded diagrams of the same direction. It raises for different directions, for an expanded diagram and one that is not expanded, and for two cascade() diagrams with different part_integrity modes.

  • Owning attributes when a part has several owning chains. They come from an unrenamed -> master if the part has one, and otherwise from the first owning reference in its definition.

  • Groups that iterate. A group iterates exactly when some foreign key inside it does not map the referencing part's owning attributes onto the referenced table's owning attributes. This covers both a renamed sibling reference (G4) and a part's second, renamed reference to its own master (G3).

  • When a condition may be copied downward. The attributes a condition names are every key of a dict and the whole heading of a query expression. With that reading, a dict with keys the parent lacks, or a query expression that shares more attributes with the child than with the parent, is never copied.

  • Relabeling materialized keys downward. Every key names, among the parent's attributes, only attributes the foreign key carries; if the foreign key has a nullable column, every key names all of its attributes, none of them NULL.

  • The warning for a dict seed that names no attribute (K6). restrict() discards a dict that names none of the table's attributes before the seed reaches the traversal, so the planner (feat(diagram): Diagram.expand on a traversal plan datajoint-python#1637) cannot see it. Raising the warning needs restrict() to record that it discarded the dict. Should the spec keep the warning, or drop it?

Pages that use the old wording elsewhere (the delete how-to, the master-part spec §4, autopopulate.md §4.4, what's new) are updated in a follow-up PR once the engine changes merge.

Consolidate the expand/restrict traversal algebra (derived from first
principles: the one primitive, edge rule R1, group rule R2, the additive expand
and subtractive restrict, renamed-FK relabel fast-path, and the master-part group
rule) into diagram.md as a 'Traversal algebra' design rationale — the single
source of truth. cascade.md and trace.md now point to it as the downstream and
upstream cases (expand down/up), keeping only their operation-specific content.

Relocated from datajoint-python's DESIGN-expand-restrict.md (PR #1524), which is
closed; the derivation and spec live with the other specs in datajoint-docs.
Drop restrict and the additive/subtractive split from the diagram.md derivation.
The model is now one operation, expand(seed, direction in {up,down,both}): cascade
= down, trace = up, export = both, over the two rules R1 (edge) and R2 (group).
A filter over several independent tables is not a second operation — it either
seeds a common descendant (which inherits both keys) and expands both ways, or,
lacking one, is a union of expansions; the per-table 'AND of upstream conditions'
carving is a UI-layer composition of expands, not a core primitive.
@dimitri-yatsenko

Copy link
Copy Markdown
Member Author

Revised the derivation to expand-only: dropped restrict and the additive/subtractive framing. The model is now one operation — expand(seed, direction in {up,down,both}) (cascade = down, trace = up, export = both) — over R1 (edge) + R2 (group). Multi-condition filtering reduces to seeding a common descendant or a union of expands, so it's a UI-layer composition, not a core primitive. (Note: the Operational Methods section still documents the shipped Diagram.restrict(); it's subsumed by expand in the model and would be removed when the redesign lands.)

cascade.md: DiGraph -> MultiDiGraph; the part-to-master walk enumerates all
simple FK paths (not shortest_path) and defers the edge rule to diagram.md's R1.
trace.md: recast the convergence table as the two directions of expand (cascade
= down, trace = up), drop the restrict/AND row, and defer the rules to diagram.md
(R1 + R2). Fixes statements that were stale after the MultiDiGraph migration and
the expand-only model.
De-duplicate cascade.md's F1-3/U1-3 per-rule enumerations against the unified
edge rule R1 in diagram.md#traversal-algebra, keeping the cascade-specific
behavior on top. Rewrite worked examples to name R1's copy/rename/project cases
instead of the old rule codes; drop the subset-symbol notation.

Reframe part_integrity per the group rule R2: with cascade the master-part group
is one item — master-first pulls in all parts, part-first lifts to the master
then re-expands to all parts; ignore treats master and part as regular tables.

Update trace.md's allowed-table-set contract so an ancestor master's whole
part-group is in scope (R2 upstream), replacing the old non-descending note.
Sweep residual unicode arrows to ASCII.
Downward expansion for delete cannot leave a restriction referencing a
downstream table: reverse-topological delete empties it first, so the subquery
matches zero rows and strands what should be deleted (#1496). State this as a
general Delete-time materialization section — seed-referencing-descendant and
the part-to-master master-key freeze are its instances — applied on every
backend by delete-order reason, not as a MySQL-only (error 1093) concern.
Preview/counts pays none of it.
@dimitri-yatsenko
dimitri-yatsenko marked this pull request as draft August 12, 2026 18:54
MilagrosMarin
MilagrosMarin previously approved these changes Aug 17, 2026
Replace the draft "Traversal algebra" section of diagram.md with the
normative traversal specification: base principles P1-P4, the row
model (foreign-key references, NULLs, parallel foreign keys, the owning
reference A1), the operations D, U and U(D(S)) with the delete modes,
trace and self.upstream, derived results T1-T8 with justifications, the
computation (edge rule, group rule, one pass over the group quotient,
groups that iterate, seeds, the plan, delete-time materialization,
concurrency), the contract for expanded diagrams, and the retirement of
restrict with migration recipes.

Remove the restrict/AND-convergence text and the Rule 1-3 propagation
section from Operational Methods, which now documents expand, cascade,
trace, indexing, counts, prune, iteration and the deprecated restrict.

Add traversal-shapes.md: one worked example per schema shape (S1-S7,
G1-G6, K1-K6, X1-X2) with the exact rows expand returns.
…pand

cascade.md now specifies the delete in terms of D and D1: what each
part_integrity mode deletes, the row-level enforce check before any
DELETE, the symbolic preview, the delete steps, delete-time
materialization and concurrency. This removes the contradiction about
whether preview pays for materialization: it does not.

trace.md now defines a trace as U(S): upward crossings are always
restrictions, a trace includes the parts of ancestor masters and their
ancestors, and self.upstream is built from the parents of the row being
computed. The published user-facing material is kept.
A part belongs to its master's group when a foreign key in its primary
key references the master, or an owned part of the same master, with
renames allowed. Its owning attributes, the ones that chain maps onto
the master's key, define its master row and its instance. A part that
is not owned is a plain table in traversal, ordering and the
declaration check, and gets a warning.

- R2's lift renames the owning attributes onto the master's key.
- A group iterates exactly when a foreign key inside it does not map
  owning attributes onto owning attributes; the one-pass condition is
  stated the same way.
- R1 defines the attributes a condition names (every dict key, the
  whole heading of a query expression) and notes that 2.3 copies the
  compiled predicate.
- Relabeling materialized keys downward gets an exact condition, with
  two counterexamples to the looser wording.
- T7, T8 and X2 follow the ownership rule of the declaration check.
- The catalog's G6 is now a part with a secondary reference to its
  master, and G7 adds the owned part-of-part chain with renamed
  attributes.
- cascade.md and trace.md follow, state the enforce preview, and relate
  delete-time materialization to the subquery-seed fix.
@dimitri-yatsenko dimitri-yatsenko changed the title docs(specs): diagram-traversal algebra derivation (expand/restrict) in diagram.md docs(specs): normative spec of Diagram.expand, with a traversal shape catalog Oct 17, 2026
@dimitri-yatsenko dimitri-yatsenko added the documentation Improvements or additions to documentation label Oct 17, 2026
@dimitri-yatsenko
dimitri-yatsenko marked this pull request as ready for review October 17, 2026 22:48

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants