You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: update design docs to reflect actual implementation
- Add prune() method to both spec and design docs
- Rename _propagate_to_children → _propagate_restrictions + _apply_propagation_rule
- Fix delete() part_integrity: post-check with rollback, not pre-check
- Add _part_integrity instance attribute
- Update files affected, verification, and implementation phases
- Mark open questions as resolved with actual decisions
- Mark export/restore as future work
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Internal. Propagates restriction from one node to its children.
111
+
Internal. Propagates restrictions from `start_node`to all its descendants in topological order. Only processes descendants of `start_node` to avoid duplicate propagation when chaining `restrict()`.
111
112
112
-
For each `out_edge(parent_node)`:
113
+
Uses multiple passes (up to 10) to handle `part_integrity="cascade"` upward propagation, which can add new restricted nodes that need further propagation.
113
114
114
-
1. Get `child_name, edge_props` from edge
115
-
2. If child is an alias node (`.isdigit()`), follow through to the real child
116
-
3. Get `attr_map`, `aliased` from `edge_props`
117
-
4. Build parent `FreeTable` with current restriction
118
-
5. Compute child restriction using propagation rules:
115
+
For each restricted node, iterates over `out_edges(node)`:
116
+
117
+
1. If target is an alias node (`.isdigit()`), follow through to real child via `out_edges(alias_node)`
118
+
2. Delegate to `_apply_propagation_rule()` for the actual restriction computation
119
+
3. Track propagated edges to avoid duplicate work
120
+
4. Handle `part_integrity="cascade"`: if child is a part table and its master is not already restricted, propagate upward from part to master using `make_condition(master, (master.proj() & part.proj()).to_arrays(), ...)`, expand the allowed node set, and continue to next pass
| Aliased FK (`attr_map` renames columns) |`parent_ft.proj(**{fk: pk for fk, pk in attr_map.items()})`|
124
130
| Non-aliased AND `parent_restriction_attrs ⊄ child.primary_key`|`parent_ft.proj()`|
125
131
126
-
6. Accumulate on child:
127
-
-`cascade` mode: `_cascade_restrictions[child].extend(child_restr)` — list = OR
128
-
-`restrict` mode: `_restrict_conditions[child].extend(child_restr)` — AndList = AND
132
+
Accumulates on child:
133
+
-`cascade` mode: `restrictions.setdefault(child, []).extend(...)` — list = OR
134
+
-`restrict` mode: `restrictions.setdefault(child, AndList()).extend(...)` — AndList = AND
129
135
130
-
7. Handle `part_integrity="cascade"`: if child is a part table and its master is not already restricted, propagate upward from part to master using `make_condition(master, (master.proj() & part.proj()).to_arrays(), ...)`, then re-propagate from the master.
136
+
Updates `_restriction_attrs` for the child with the relevant attribute names.
1. Pre-check `part_integrity="enforce"`: for each node in `_cascade_restrictions`, if it's a part table and its master is not restricted, raise `DataJointError`
144
-
2. Get nodes with restrictions in topological order
145
-
3. If `prompt`: show preview (table name + row count for each)
146
-
4. Start transaction (if `transaction=True`)
147
-
5. Iterate in **reverse** topological order (leaves first):
149
+
1. Get non-alias nodes with restrictions in topological order
150
+
2. If `prompt`: show preview (table name + row count for each)
151
+
3. Start transaction (if `transaction=True`)
152
+
4. Iterate in **reverse** topological order (leaves first):
2. If restrictions exist (`_cascade_restrictions` or `_restrict_conditions`):
206
+
- For each restricted node, build `FreeTable` with restriction applied
207
+
- If `len(ft) == 0`: remove node from restrictions dict, `_restriction_attrs`, and `nodes_to_show`
208
+
3. If no restrictions (unrestricted diagram):
209
+
- For each node in `nodes_to_show`, check `len(FreeTable(conn, node))`
210
+
- If 0: remove from `nodes_to_show`
211
+
4. Return `result`
212
+
213
+
**Properties:**
214
+
- Idempotent — pruning twice yields the same result
215
+
- Chainable — `restrict()` can be called after `prune()`
216
+
- Skips alias nodes (`.isdigit()`)
217
+
183
218
### Visualization methods (gated)
184
219
185
220
All existing visualization methods (`draw`, `make_dot`, `make_svg`, `make_png`, `make_image`, `make_mermaid`, `save`, `_repr_svg_`) raise `DataJointError("Install matplotlib and pygraphviz...")` when `diagram_active is False`. When active, they work as before.
@@ -307,23 +342,24 @@ For `_restrict_conditions`: values are `AndList` (AND). Each `.restrict()` call
307
342
308
343
| File | Change |
309
344
|------|--------|
310
-
|`src/datajoint/diagram.py`| Restructure: single `Diagram(nx.DiGraph)` class, gate only visualization. Add `_connection`, restriction dicts, `cascade()`, `restrict()`, `_propagate_to_children()`, `delete()`, `drop()`, `preview()`, `_from_table()`|
| Inspectability | Opaque recursive cascade | Preview affected data before executing |
315
322
316
-
## Implementation plan
323
+
## Implementation status
317
324
318
-
### Phase 1: RestrictedDiagram core
325
+
### Phase 1: Diagram restructure and restriction propagation ✓
319
326
320
-
1. Add `_cascade_restrictions` and `_restrict_conditions` to `Diagram` — per-node restriction storage
321
-
2. Implement `_propagate_downstream(mode)` — walk edges in topo order, compute child restrictions via `attr_map`, accumulate as OR (cascade) or AND (restrict)
322
-
3. Implement `cascade(table_expr)` — OR propagation entry point
323
-
4. Implement `restrict(table_expr)` — AND propagation entry point
324
-
5. Handle alias nodes during propagation (always OR for multiple FK paths from same parent)
325
-
6. Handle `part_integrity` during cascade propagation (upward cascade from part to master)
327
+
Single `Diagram(nx.DiGraph)` class with `_cascade_restrictions`, `_restrict_conditions`, `_restriction_attrs`, `_part_integrity`. `cascade()`, `restrict()`, `_propagate_restrictions()`, `_apply_propagation_rule()`. Alias node handling, `part_integrity="cascade"` upward propagation.
326
328
327
-
### Phase 2: Graph-driven delete and drop
329
+
### Phase 2: Graph-driven operations ✓
328
330
329
-
1. Implement `Diagram.delete()` — reverse topo order, `delete_quick()` at each cascade-restricted node
330
-
2. Implement `Diagram.drop()` — reverse topo order, `drop_quick()` at each node (no restrictions)
4. Migrate `Table.delete()` to construct a diagram + `cascade()` internally
333
-
5. Migrate `Table.drop()` to construct a diagram + `drop()` internally
334
-
6. Preserve `Part.delete()` and `Part.drop()` behavior with diagram-based `part_integrity`
335
-
7. Remove error-message parsing from the delete critical path (retain as diagnostic fallback)
331
+
`delete()`, `drop()`, `preview()`, `prune()`, `_from_table()`. Unloaded-schema fallback error handling. `Table.delete()` and `Table.drop()` rewritten to delegate to `Diagram`. Dead cascade code removed.
336
332
337
-
### Phase 3: Preview and visualization
333
+
### Phase 3: Tests ✓
338
334
339
-
1.`Diagram.preview()` — show restricted nodes with row counts
340
-
2.`Diagram.draw()` — highlight restricted nodes, show restriction labels
335
+
All existing tests pass. 5 prune integration tests added to `test_erd.py`.
341
336
342
337
### Phase 4: Export and backup (future, #864/#560)
343
338
344
-
1.`Diagram.export(path)` — forward topo order, fetch + write at each restrict-restricted node
345
-
2. Upward pass to include referenced parent rows (referential context)
346
-
3.`Diagram.restore(path)` — forward topo order, insert at each node
|`tests/integration/test_diagram.py`| New tests for restricted diagram |
345
+
|`src/datajoint/diagram.py`| Single `Diagram(nx.DiGraph)` class with `cascade()`, `restrict()`, `_propagate_restrictions()`, `_apply_propagation_rule()`, `delete()`, `drop()`, `preview()`, `prune()`, `_from_table()`|
346
+
|`src/datajoint/table.py`|`Table.delete()` (~200 → ~10 lines) and `Table.drop()` (~35 → ~10 lines) rewritten to delegate to `Diagram`. Dead cascade code removed |
347
+
|`src/datajoint/user_tables.py`|`Part.drop()`: pass `part_integrity` through to `super().drop()`|
1.**Should `cascade()`/`restrict()` return a new object or mutate in place?**
362
-
Returning a new object enables chaining (`diagram.restrict(A).restrict(B)`) and keeps the original diagram reusable. Mutating in place is simpler but prevents reuse.
352
+
| Question | Resolution |
353
+
|----------|------------|
354
+
| Return new or mutate? | Return new `Diagram` — enables chaining and keeps original reusable |
355
+
| Upward propagation scope? | Master's restriction propagates to all its descendants (natural from re-running `_propagate_restrictions`) |
356
+
| Transaction boundaries? | Build diagram (read-only), preview, confirm, execute all deletes in one transaction |
357
+
| Lazy vs eager propagation? | Eager — propagate when `cascade()`/`restrict()` is called. Restrictions are `QueryExpression` objects, not executed until `preview()`/`delete()`|
358
+
| Export upward context? | Deferred to future work (Phase 4) |
363
359
364
-
2.**Upward propagation scope for `part_integrity="cascade"`:**
365
-
When a restriction propagates up from part to master, should the master's restriction then propagate to the master's *other* parts and descendants? The current implementation does this (lines 1098–1108 of `table.py`). The diagram approach would naturally do the same — restricting the master triggers downstream propagation to all its children.
360
+
## Future work
366
361
367
-
3.**Transaction boundaries:**
368
-
The current `Table.delete()` wraps everything in a single transaction with user confirmation. The diagram-based delete should preserve this: build the restricted diagram (read-only), show preview, get confirmation, then execute all deletes in one transaction.
362
+
### Export and backup (#864/#560)
369
363
370
-
4.**Lazy vs eager restriction propagation:**
371
-
Eager: propagate all restrictions when `restrict()` is called (computes row counts immediately).
372
-
Lazy: store parent restrictions and propagate during `delete()`/`export()` (defers queries).
373
-
Eager is better for preview but may issue many queries upfront. Lazy is more efficient when the user just wants to delete without preview.
364
+
Not yet implemented. Planned:
374
365
375
-
5.**Export: upward context scope.**
376
-
When exporting, non-downstream tables should be included for referential integrity. How far upstream? Options: (a) all ancestors of restricted nodes, (b) only directly referenced parents, (c) full referential closure. Full closure is safest but may pull in large amounts of unrestricted data.
366
+
-`Diagram.export(path)` — forward topo order, fetch + write at each restrict-restricted node
367
+
- Upward pass to include referenced parent rows (referential context)
368
+
-`Diagram.restore(path)` — forward topo order, insert at each node
0 commit comments