use std::collections::HashSet;
use oxrdf::vocab::xsd;
use oxrdf::{GraphName, Literal, NamedNode, Quad, Term, Triple};
use crate::error::StoreError;
use crate::graph::GraphStore;
use crate::journal::{JournalStore, Position};
use crate::vocab;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProjectionReport {
pub actor: NamedNode,
pub from: Position,
pub to: Position,
pub entries: usize,
}
pub struct Projector<'a> {
journal: &'a dyn JournalStore,
target: &'a dyn GraphStore,
state_source: Option<&'a dyn GraphStore>,
}
impl std::fmt::Debug for Projector<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Projector")
.field("state_source", &self.state_source.is_some())
.finish_non_exhaustive()
}
}
impl<'a> Projector<'a> {
pub fn new(journal: &'a dyn JournalStore, target: &'a dyn GraphStore) -> Self {
Projector {
journal,
target,
state_source: None,
}
}
pub fn with_state_source(mut self, source: &'a dyn GraphStore) -> Self {
self.state_source = Some(source);
self
}
pub fn projected_position(&self, actor: &NamedNode) -> Result<Position, StoreError> {
let q = format!(
"SELECT ?n WHERE {{ GRAPH <{g}> {{ {actor} <{p}> ?n }} }}",
g = vocab::PROJECTION_GRAPH,
p = vocab::PROJECTED_POSITION,
);
let solutions = self.target.query(&q)?.into_solutions()?;
Ok(Position::new(
solutions
.values("n")
.iter()
.filter_map(|t| match t {
Term::Literal(l) => l.value().parse::<u64>().ok(),
_ => None,
})
.max()
.unwrap_or(0),
))
}
pub fn project_actor(&self, actor: &NamedNode) -> Result<ProjectionReport, StoreError> {
let from = self.projected_position(actor)?;
let records = self.journal.read(actor, from.next())?;
let mut to = from;
for record in &records {
debug_assert_eq!(record.position, to.next(), "journal reads are gap-free");
self.target.insert_quads(&record.entry.quads())?;
to = record.position;
}
if records.is_empty() {
return Ok(ProjectionReport {
actor: actor.clone(),
from,
to,
entries: 0,
});
}
if let Some(source) = self.state_source {
self.copy_state(source, actor)?;
}
self.record_position(actor, from, to)?;
Ok(ProjectionReport {
actor: actor.clone(),
from,
to,
entries: records.len(),
})
}
pub fn project_all(&self) -> Result<Vec<ProjectionReport>, StoreError> {
self.journal
.actors()?
.iter()
.map(|a| self.project_actor(a))
.collect()
}
fn graph_triples(store: &dyn GraphStore, graph: &NamedNode) -> Result<Vec<Triple>, StoreError> {
store
.query(&format!(
"CONSTRUCT {{ ?s ?p ?o }} WHERE {{ GRAPH {graph} {{ ?s ?p ?o }} }}"
))?
.into_graph()
}
fn copy_state(&self, source: &dyn GraphStore, actor: &NamedNode) -> Result<(), StoreError> {
let want: HashSet<Triple> = Self::graph_triples(source, actor)?.into_iter().collect();
let have: HashSet<Triple> = Self::graph_triples(self.target, actor)?
.into_iter()
.collect();
let g = GraphName::NamedNode(actor.clone());
let add: Vec<Quad> = want
.difference(&have)
.map(|t| t.clone().in_graph(g.clone()))
.collect();
let remove: Vec<Quad> = have
.difference(&want)
.map(|t| t.clone().in_graph(g.clone()))
.collect();
if !add.is_empty() {
self.target.insert_quads(&add)?;
}
if !remove.is_empty() {
self.target.remove_quads(&remove)?;
}
Ok(())
}
fn record_position(
&self,
actor: &NamedNode,
old: Position,
new: Position,
) -> Result<(), StoreError> {
let quad = |p: Position| {
Quad::new(
actor.clone(),
vocab::node(vocab::PROJECTED_POSITION),
Literal::new_typed_literal(p.get().to_string(), xsd::INTEGER),
vocab::node(vocab::PROJECTION_GRAPH),
)
};
self.target.insert_quads(&[quad(new)])?;
if old != Position::ZERO && old != new {
self.target.remove_quads(&[quad(old)])?;
}
Ok(())
}
}