Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Materialized Property Views

Materialized property views are per-node derived properties maintained incrementally as the graph changes. They behave exactly like user-written node properties in queries (scan, filter, project, group-by) but are computed automatically — no triggers, no manual refresh.

Concepts

A view binds a synthetic property name (view_prop) to a computation over a node’s neighbors:

Source typeComputes
DegreeCount of edges of a given type in a given direction
NeighborAgg / SumSum of a neighbor property
NeighborAgg / AvgAverage of a neighbor property
NeighborAgg / MinMinimum of a neighbor property
NeighborAgg / MaxMaximum of a neighbor property
NeighborAgg / CountCount of neighbors that have the property

NeighborAgg Count skips neighbors missing the named property (props.get is None). It is not Degree: an incident edge whose endpoint has no value for prop does not increment Count. Degree still counts every incident edge of that type and direction.

Views are label-scoped: only nodes with the declared label carry the synthetic property. Views cover both user-inserted edges and rule-derived edges.

Quick Start

#![allow(unused)]
fn main() {
use mushroomdb::{AggFn, Direction, GraphDb, Value, ViewDef, ViewSource};

let mut db = GraphDb::open(&dir)?;

// Track how many people live in each city.
db.create_view(ViewDef {
    name: "city_population".into(),
    label: "City".into(),
    view_prop: "pop".into(),
    source: ViewSource::Degree {
        edge_type: "LIVES_IN".into(),
        direction: Direction::In,
    },
})?;

// Average score of residents.
db.create_view(ViewDef {
    name: "city_avg_score".into(),
    label: "City".into(),
    view_prop: "avg_score".into(),
    source: ViewSource::NeighborAgg {
        edge_type: "LIVES_IN".into(),
        direction: Direction::In,
        agg: AggFn::Avg,
        prop: "score".into(),
    },
})?;

// Read like any property.
let pop = db.get_prop("london", "pop");

// Query using view props in Cypher.
let results = db.query(
    "MATCH (c:City) WHERE c.pop >= 1000 RETURN c.name, c.avg_score",
    &Default::default()
)?;
}

API

create_view(def: ViewDef) -> Result<()>

Register a new view. Backfills values for all existing nodes with the matching label. Fails if:

  • A view with the same name already exists.
  • view_prop is already owned by another view.
  • view_prop collides with an existing real node property.

delete_view(name: &str) -> Result<()>

Remove a view and delete its values from all nodes. Fails if the view does not exist.

views() -> Vec<ViewDef>

Snapshot of all registered view definitions.

Incremental Maintenance

EventViews updated
Edge insertedDegree / NeighborAgg for src and/or dst
Edge deletedSame
Derived edge fired (rule)Same
Derived edge retracted (rule)Same
Node property changedNeighborAgg views that read that property
Node deletedNeighbor views decremented; own values removed
create_viewBackfill all matching nodes
delete_viewValues removed from all matching nodes

MIN / MAX retraction cost

When a Degree or NeighborAgg Min/Max view loses the edge whose endpoint held the current extreme value, a full scan over remaining neighbors is required (O(degree)). No auxiliary sorted structure is maintained in v1. For workloads where this is a bottleneck, use Sum + Count and compute Avg / Min / Max in the query layer, or maintain a heap externally.

Write Guard

Writing directly to a view-managed property name returns GraphError::ViewPropReadOnly { view_name }:

#![allow(unused)]
fn main() {
// Assuming "pop" is managed by "city_population":
let err = db.set_prop("london", "pop", Value::Int(999));
// err == Err(ViewPropReadOnly { view_name: "city_population" })
}

Persistence

View definitions are WAL-persisted as CreateView / DeleteView records (discriminants 10 / 11). They also survive snapshots via a view_defs field in the snapshot state (format version 5).

View values are NOT stored in snapshots — they are recomputed from topo + props on every open. This matches the rebuild-on-open pattern used by candidate indexes and keeps snapshot writes simple.

Disk-space note. Snapshots store the raw edge list including all derived edges. At 100k nodes with a dense rule (10.5M derived edges) a V5 snapshot is approximately 2.2 GiB on disk. Plan accordingly if you call snapshot() on large graphs with high-fanout rules.

Subscriptions

View-value updates do not emit subscription events in v1. Only user-write WAL records and rule fire/retract deltas generate DbEvents.

DST Oracle

GraphDb::scratch_view_value(key, view_name) recomputes a view value from scratch (O(degree)) without reading the stored column. Use it to verify correctness:

#![allow(unused)]
fn main() {
let live = db.get_prop("london", "pop");
let scratch = db.scratch_view_value("london", "city_population");
assert_eq!(live, scratch.as_ref());
}