///|
struct Cached {
reads : Array[Read]
results : Array[ValidationResult]
}
///|
/// A session keeps validation state for one immutable compiled plan.
pub struct Session {
plan : Plan
mut graph : Graph
mut cache : Map[(Term, Term), Cached]
}
///|
pub(all) struct Update {
committed : Bool
report : Report
}
///|
fn copy_result(r : ValidationResult) -> ValidationResult {
{
..r,
path: r.path.map(copy_path),
messages: r.messages.copy(),
details: r.details.map(copy_result),
}
}
///|
fn read_affected(read : Read, changes : Array[Triple]) -> Bool {
changes.any(t => {
match read {
Objects(s, p) => t.subject == s && t.predicate == p
Subjects(p, o) => t.predicate == p && t.object == o
Outgoing(s) => t.subject == s
}
})
}
///|
fn cached_validate(
plan : Plan,
graph : Graph,
cache : Map[(Term, Term), Cached],
changes : Array[Triple],
) -> (Report, Map[(Term, Term), Cached]) {
let next : Map[(Term, Term), Cached] = Map([])
let results = []
let mut checked = 0
let mut reused = 0
// Targets are always recomputed. Newly added targets and disappeared targets
// cannot be inferred solely from dependencies of previous focus nodes.
for id in plan.shape_order {
for focus in target_nodes(plan.shapes[id], graph) {
let key = (id, focus)
let entry = match cache.get(key) {
Some(old) if !old.reads.any(r => read_affected(r, changes)) => {
reused = reused + 1
old
}
_ => {
let reader : Reader = { graph, reads: [], }
let results = evaluate_shape(plan, id, focus, reader)
checked = checked + 1
{ reads: reader.reads, results, }
}
}
next[key] = entry
results.append(entry.results.map(copy_result))
}
}
({ conforms: results.is_empty(), results, checked, reused, }, next)
}
///|
/// The initial graph may be invalid; only conforming updates can be committed.
pub fn Session::new(plan : Plan, graph : Graph) -> Session {
let (_, cache) = cached_validate(plan, graph, Map([]), [])
{ plan, graph, cache, }
}
///|
pub fn Session::graph(self : Session) -> Graph {
self.graph
}
///|
pub fn Session::report(self : Session) -> Report {
let (report, _) = cached_validate(self.plan, self.graph, self.cache, [])
report
}
///|
fn Session::candidate(
self : Session,
additions : Array[Triple],
removals : Array[Triple],
) -> (Graph, Report, Map[(Term, Term), Cached]) {
let triples = self.graph.triples.filter(t => !removals.contains(t))
triples.append(additions)
let graph = Graph::new(triples)
let before : Map[Triple, Bool] = Map([])
let after : Map[Triple, Bool] = Map([])
for t in self.graph.triples {
before[t] = true
}
for t in graph.triples {
after[t] = true
}
let changes = []
for t in self.graph.triples {
if !after.contains(t) {
changes.push(t)
}
}
for t in graph.triples {
if !before.contains(t) {
changes.push(t)
}
}
let (report, cache) = cached_validate(self.plan, graph, self.cache, changes)
(graph, report, cache)
}
///|
/// Inspect a proposed graph without changing graph or cache, including on success.
pub fn Session::preview(
self : Session,
additions : Array[Triple],
removals : Array[Triple],
) -> Report {
let (_, report, _) = self.candidate(additions, removals)
report
}
///|
/// Deletes are applied before additions. A rejected update changes neither graph nor cache.
pub fn Session::apply(
self : Session,
additions : Array[Triple],
removals : Array[Triple],
) -> Update {
let (graph, report, cache) = self.candidate(additions, removals)
let committed = report.conforms
if committed {
self.graph = graph
self.cache = cache
}
{ committed, report, }
}