vertep_store/
projector.rs1use std::collections::HashSet;
4
5use oxrdf::vocab::xsd;
6use oxrdf::{GraphName, Literal, NamedNode, Quad, Term, Triple};
7
8use crate::error::StoreError;
9use crate::graph::GraphStore;
10use crate::journal::{JournalStore, Position};
11use crate::vocab;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct ProjectionReport {
16 pub actor: NamedNode,
18 pub from: Position,
20 pub to: Position,
22 pub entries: usize,
24}
25
26pub struct Projector<'a> {
40 journal: &'a dyn JournalStore,
41 target: &'a dyn GraphStore,
42 state_source: Option<&'a dyn GraphStore>,
43}
44
45impl std::fmt::Debug for Projector<'_> {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("Projector")
48 .field("state_source", &self.state_source.is_some())
49 .finish_non_exhaustive()
50 }
51}
52
53impl<'a> Projector<'a> {
54 pub fn new(journal: &'a dyn JournalStore, target: &'a dyn GraphStore) -> Self {
56 Projector {
57 journal,
58 target,
59 state_source: None,
60 }
61 }
62
63 pub fn with_state_source(mut self, source: &'a dyn GraphStore) -> Self {
65 self.state_source = Some(source);
66 self
67 }
68
69 pub fn projected_position(&self, actor: &NamedNode) -> Result<Position, StoreError> {
75 let q = format!(
76 "SELECT ?n WHERE {{ GRAPH <{g}> {{ {actor} <{p}> ?n }} }}",
77 g = vocab::PROJECTION_GRAPH,
78 p = vocab::PROJECTED_POSITION,
79 );
80 let solutions = self.target.query(&q)?.into_solutions()?;
81 Ok(Position::new(
82 solutions
83 .values("n")
84 .iter()
85 .filter_map(|t| match t {
86 Term::Literal(l) => l.value().parse::<u64>().ok(),
87 _ => None,
88 })
89 .max()
90 .unwrap_or(0),
91 ))
92 }
93
94 pub fn project_actor(&self, actor: &NamedNode) -> Result<ProjectionReport, StoreError> {
102 let from = self.projected_position(actor)?;
103 let records = self.journal.read(actor, from.next())?;
104 let mut to = from;
105 for record in &records {
106 debug_assert_eq!(record.position, to.next(), "journal reads are gap-free");
107 self.target.insert_quads(&record.entry.quads())?;
108 to = record.position;
109 }
110 if records.is_empty() {
111 return Ok(ProjectionReport {
112 actor: actor.clone(),
113 from,
114 to,
115 entries: 0,
116 });
117 }
118 if let Some(source) = self.state_source {
119 self.copy_state(source, actor)?;
120 }
121 self.record_position(actor, from, to)?;
122 Ok(ProjectionReport {
123 actor: actor.clone(),
124 from,
125 to,
126 entries: records.len(),
127 })
128 }
129
130 pub fn project_all(&self) -> Result<Vec<ProjectionReport>, StoreError> {
136 self.journal
137 .actors()?
138 .iter()
139 .map(|a| self.project_actor(a))
140 .collect()
141 }
142
143 fn graph_triples(store: &dyn GraphStore, graph: &NamedNode) -> Result<Vec<Triple>, StoreError> {
144 store
145 .query(&format!(
146 "CONSTRUCT {{ ?s ?p ?o }} WHERE {{ GRAPH {graph} {{ ?s ?p ?o }} }}"
147 ))?
148 .into_graph()
149 }
150
151 fn copy_state(&self, source: &dyn GraphStore, actor: &NamedNode) -> Result<(), StoreError> {
152 let want: HashSet<Triple> = Self::graph_triples(source, actor)?.into_iter().collect();
153 let have: HashSet<Triple> = Self::graph_triples(self.target, actor)?
154 .into_iter()
155 .collect();
156 let g = GraphName::NamedNode(actor.clone());
157 let add: Vec<Quad> = want
158 .difference(&have)
159 .map(|t| t.clone().in_graph(g.clone()))
160 .collect();
161 let remove: Vec<Quad> = have
162 .difference(&want)
163 .map(|t| t.clone().in_graph(g.clone()))
164 .collect();
165 if !add.is_empty() {
166 self.target.insert_quads(&add)?;
167 }
168 if !remove.is_empty() {
169 self.target.remove_quads(&remove)?;
170 }
171 Ok(())
172 }
173
174 fn record_position(
175 &self,
176 actor: &NamedNode,
177 old: Position,
178 new: Position,
179 ) -> Result<(), StoreError> {
180 let quad = |p: Position| {
181 Quad::new(
182 actor.clone(),
183 vocab::node(vocab::PROJECTED_POSITION),
184 Literal::new_typed_literal(p.get().to_string(), xsd::INTEGER),
185 vocab::node(vocab::PROJECTION_GRAPH),
186 )
187 };
188 self.target.insert_quads(&[quad(new)])?;
191 if old != Position::ZERO && old != new {
192 self.target.remove_quads(&[quad(old)])?;
193 }
194 Ok(())
195 }
196}