Skip to main content

ripbi_core/graph/
reachability.rs

1//! Reachability: the two-pass traversal that separates live objects from dead.
2//!
3//! Two plain passes encode the relationship policy without any special-cased
4//! machinery (the policy itself is documented in [`super`]):
5//!
6//! 1. **Strong pass** — from the roots (report bindings and roles), over every
7//!    edge *except* relationship endpoints and the inactive-relationship
8//!    edges. Containment fires: a used column keeps its table alive, a used
9//!    table keeps its partitions and active relationships alive.
10//! 2. **Weak pass** — extends the strong set over every edge *except*
11//!    containment and the inactive-relationship edges. A live table pulls in
12//!    its relationships and both their key columns, but a key column that is
13//!    only alive this way can no longer keep its own table alive — so a table
14//!    referenced by nothing but a relationship is still reported unused. An
15//!    inactive relationship joins only through a live `USERELATIONSHIP`
16//!    reference (an ordinary Dax edge); its table references never confer
17//!    liveness, so an unactivated one is a finding.
18//!
19//! A consumer annotation therefore never lies: for any unused object, every
20//! referencing object is either itself unused, live only through a
21//! relationship endpoint (its `also_unused` flag is `false`), or the table of
22//! an inactive relationship the relationship cannot keep alive.
23
24use std::collections::HashSet;
25
26use super::{DependencyGraph, ObjectId, Provenance};
27
28/// The liveness verdict for one graph: the set of live objects.
29pub(super) struct Reachability {
30    live: HashSet<ObjectId>,
31}
32
33impl Reachability {
34    /// Runs both passes over the finished graph.
35    pub(super) fn compute(graph: &DependencyGraph) -> Self {
36        // Pass 1 reaches everything except relationship-endpoint targets; pass 2
37        // extends that set without letting containment fire again, so weakly
38        // alive key columns never drag their tables along — and inactive
39        // relationships never confer liveness at all.
40        let strong = graph.reach(graph.seed_indices(), Provenance::is_strong_pass_edge);
41        let live = graph.reach(strong.iter().copied(), Provenance::is_weak_pass_edge);
42        Self {
43            live: live
44                .into_iter()
45                .map(|idx| graph.object_at(idx).clone())
46                .collect(),
47        }
48    }
49
50    /// True when the object is live at all.
51    pub(super) fn is_live(&self, id: &ObjectId) -> bool {
52        self.live.contains(id)
53    }
54}
55
56/// One object reachability never reached — a `scan` finding.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct UnusedObject {
59    /// The unused object.
60    pub id: ObjectId,
61    /// Every graph edge pointing at it. Empty means nothing references the
62    /// object at all: the root cause of its dead chain, deletable outright.
63    pub used_by: Vec<UsedBy>,
64    /// The M expressions that name this object — its Power Query supply
65    /// chain, carried beside the graph because it is deliberately not edges.
66    /// Unloading the object cannot break these; removing it from the script
67    /// entirely means editing each of them. Non-empty only ever for columns.
68    pub named_by_m: Vec<ObjectId>,
69}
70
71/// One referencing object behind an [`UnusedObject`].
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct UsedBy {
74    /// The referencing object.
75    pub id: ObjectId,
76    /// What kind of use the edge records.
77    pub provenance: Provenance,
78    /// True when the referencing object is itself unused — the "also unused"
79    /// of the `← only used by X (also unused)` annotation. False means the
80    /// referencing object is live but its use could not keep this one alive:
81    /// a key column kept alive only as an active relationship endpoint,
82    /// holding its table's only reference, or the live table of an inactive
83    /// relationship nothing activates.
84    pub also_unused: bool,
85}