Expand description
The dependency graph: one petgraph DAG over the semantic model and the
reports that share it, plus the reachability analysis that isolates dead
objects.
§Shape
Nodes are ObjectIds — every table, column, measure, partition,
hierarchy, relationship, role, calculation item, shared expression,
user-defined function, and report measure, whether or not anything
references them. Edges point from user to used and carry their
Provenance as first-class data, so a reverse query
(consumers_of) is a pure read and a
second view over the graph (ripbi deps) is pure rendering in the CLI.
Report sites — visuals, pages, bookmarks — are not model objects, so their
bindings live beside the graph as roots with
full provenance.
§Liveness policy (conservative — what “unused” means)
Reachability starts from the roots: every
ReportModel::bindings target and every
role (roles are security configuration, never dead weight; their filter
edges keep the referenced columns alive). From there, two passes over the
edge catalog decide liveness — the full catalog with the reasoning behind
every rule lives in docs/graph.md beside this module:
- DAX references (
dax::bind, every candidate — an unqualified[Name]keeps the measure and the home-table column alive) and their extended candidates (hierarchies, calculation items, and, for a reference matching nothing, its qualifying table). A reference that matches nothing and has no resolvable part keeps nothing alive. - M references (
m::bind, same conservatism). The pipeline is M → tables/columns → DAX → reports, so M is upstream of everything and deletion flows one way. A table or shared expression named in M — a merge source, a referenced parameter query — keeps alive: deleting it deletes the query another partition reads, which breaks refresh. A column named in M is the column’s supply chain, not a consumer: the query keeps producing it and the model just stops mapping it, so unloading cannot break refresh and there is deliberately no edge. Only Data columns qualify — an M step can only name a column it produces, so a calculated column matching an M name (the auto date/time columns named like Desktop’s date-template query) is not recorded. Instead the naming expressions ride along on the finding (UnusedObject::named_by_m) — the what-a-full-removal-must-edit context. Liveness still flows through the owner, so a dead table’s partition keeps nothing alive. - Report bindings with their provenance, report measures shadowing model measures of the same name. An unused report measure is dead like any other node — its body’s references stay alive only through it.
- Containment: a used member (column, measure, hierarchy, calculation item) keeps its table alive; a used table keeps its partitions, relationships, and engine-managed columns (calculated-table columns, calculation-group columns, calendar columns) alive.
- Relationships: live if either endpoint table is reachable. An
active relationship keeps both key columns alive — but a key column
kept alive only as a relationship endpoint does not keep its table
alive, so a table referenced by nothing but a relationship is still
unused. An inactive relationship is live only when a live DAX
reference (
USERELATIONSHIP) activates it — switching one on at query time is DAX’s job, and nothing else can. Unactivated, it is a finding itself, and its key columns are findings chained under it.
The conservatism rule from name resolution governs everything: marking an object used too many is harmless; marking one too few tells a user to delete live code. A model scanned with no reports and no roles therefore reports everything as unused — callers decide whether that is a finding or a missing report.
§Examples
use ripbi_core::{
Column, FieldTarget, FieldWell, Measure, NameKey, Page, Projection,
ReportModel, Table, TabularDatabase, Visual,
};
use ripbi_core::graph::DependencyGraph;
let db = TabularDatabase {
tables: vec![Table {
name: "Sales".to_string(),
columns: vec![
Column { name: "Amount".to_string(), ..Default::default() },
Column { name: "Legacy".to_string(), ..Default::default() },
],
measures: vec![Measure {
name: "Total".to_string(),
expression: "SUM('Sales'[Amount])".to_string(),
..Default::default()
}],
..Default::default()
}],
..Default::default()
};
// One visual projecting the Total measure keeps it — and its column — alive.
let report = ReportModel {
pages: vec![Page {
name: NameKey::new("P1"),
display_name: None,
is_hidden: false,
filters: Vec::new(),
binding: None,
visuals: vec![Visual {
name: NameKey::new("V1"),
visual_type: "card".to_string(),
wells: vec![FieldWell {
role: "Values".to_string(),
projections: vec![Projection {
target: FieldTarget::Measure {
home_table: Some(NameKey::new("Sales")),
measure: NameKey::new("Total"),
},
query_ref: None,
active: true,
}],
}],
filters: Vec::new(),
sorts: Vec::new(),
conditional_formatting: Vec::new(),
alt_text: Vec::new(),
tooltip_page: None,
}],
}],
..Default::default()
};
let graph = DependencyGraph::build(&db, &[&report]);
// The visual well is a root with provenance…
assert_eq!(graph.roots().len(), 1);
let total = ripbi_core::ObjectId::Measure {
table: NameKey::new("Sales"),
measure: NameKey::new("Total"),
};
assert_eq!(graph.roots_of(&total).len(), 1);
// …and nothing touches `Legacy`, so it is the one unused object.
let unused = graph.unused_objects();
assert_eq!(unused.len(), 1);
assert_eq!(unused[0].id.to_string(), "'Sales'[Legacy]");
assert!(unused[0].used_by.is_empty(), "nothing references it at all");Re-exports§
pub use provenance::BindingEdge;pub use provenance::BindingSite;pub use provenance::Provenance;pub use provenance::StructuralEdge;
Modules§
- provenance
- Per-edge provenance: what kind of use every dependency edge records.
Structs§
- Auto
Date Time Verdict - One auto date/time table with the second, provenance-based verdict: does a report bind the machinery, or is it kept alive only by the engine’s own relationship?
- Dependency
Graph - The dependency graph of one semantic model and the reports sharing it.
- Unused
Object - One object reachability never reached — a
scanfinding. - UsedBy
- One referencing object behind an
UnusedObject.
Enums§
- Auto
Date Time Status - The provenance-based verdict for one auto date/time table — the three states of issue #16, none of which plain reachability can produce.