Skip to main content

Module graph

Module graph 

Source
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 broken::BrokenBinding;
pub use broken::BrokenReason;
pub use provenance::BindingEdge;
pub use provenance::BindingSite;
pub use provenance::Provenance;
pub use provenance::StructuralEdge;

Modules§

broken
Broken report bindings: written field references that resolve to nothing in the model, and bindings that land on artifacts whose own expressions no longer resolve (issue #60).
provenance
Per-edge provenance: what kind of use every dependency edge records.

Structs§

AutoDateTimeVerdict
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?
DependencyGraph
The dependency graph of one semantic model and the reports sharing it.
UnusedObject
One object reachability never reached — a scan finding.
UsedBy
One referencing object behind an UnusedObject.

Enums§

AutoDateTimeStatus
The provenance-based verdict for one auto date/time table — the three states of issue #16, none of which plain reachability can produce.