Skip to main content

tsift_core/
convex.rs

1use anyhow::{Result, bail};
2use serde::{Deserialize, Serialize};
3use std::cell::RefCell;
4use std::collections::BTreeMap;
5
6use crate::store::{GraphStore, shortest_path_using_outgoing};
7use crate::types::*;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
10#[serde(rename_all = "camelCase")]
11pub struct ConvexProjectionRows {
12    pub nodes: Vec<ConvexNodeRow>,
13    pub edges: Vec<ConvexEdgeRow>,
14}
15
16impl From<&GraphProjection> for ConvexProjectionRows {
17    fn from(projection: &GraphProjection) -> Self {
18        Self {
19            nodes: projection.nodes.iter().map(ConvexNodeRow::from).collect(),
20            edges: projection.edges.iter().map(ConvexEdgeRow::from).collect(),
21        }
22    }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct ConvexNodeRow {
28    pub external_id: String,
29    pub kind: String,
30    pub label: String,
31    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
32    pub properties: BTreeMap<String, String>,
33    #[serde(default, skip_serializing_if = "Vec::is_empty")]
34    pub provenance: Vec<GraphProvenance>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub freshness: Option<GraphFreshness>,
37}
38
39impl From<&GraphNode> for ConvexNodeRow {
40    fn from(node: &GraphNode) -> Self {
41        Self {
42            external_id: node.id.clone(),
43            kind: node.kind.clone(),
44            label: node.label.clone(),
45            properties: node.properties.clone(),
46            provenance: node.provenance.clone(),
47            freshness: node.freshness.clone(),
48        }
49    }
50}
51
52impl From<ConvexNodeRow> for GraphNode {
53    fn from(row: ConvexNodeRow) -> Self {
54        Self {
55            id: row.external_id,
56            kind: row.kind,
57            label: row.label,
58            properties: row.properties,
59            provenance: row.provenance,
60            freshness: row.freshness,
61        }
62    }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct ConvexEdgeRow {
68    pub edge_key: String,
69    pub from_external_id: String,
70    pub to_external_id: String,
71    pub kind: String,
72    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
73    pub properties: BTreeMap<String, String>,
74    #[serde(default, skip_serializing_if = "Vec::is_empty")]
75    pub provenance: Vec<GraphProvenance>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub freshness: Option<GraphFreshness>,
78}
79
80impl ConvexEdgeRow {
81    pub fn stable_key(from_id: &str, to_id: &str, kind: &str) -> String {
82        stable_graph_edge_id(from_id, to_id, kind)
83    }
84}
85
86impl From<&GraphEdge> for ConvexEdgeRow {
87    fn from(edge: &GraphEdge) -> Self {
88        Self {
89            edge_key: graph_edge_id(edge),
90            from_external_id: edge.from_id.clone(),
91            to_external_id: edge.to_id.clone(),
92            kind: edge.kind.clone(),
93            properties: edge.properties.clone(),
94            provenance: edge.provenance.clone(),
95            freshness: edge.freshness.clone(),
96        }
97    }
98}
99
100impl From<ConvexEdgeRow> for GraphEdge {
101    fn from(row: ConvexEdgeRow) -> Self {
102        Self {
103            id: row.edge_key,
104            from_id: row.from_external_id,
105            to_id: row.to_external_id,
106            kind: row.kind,
107            properties: row.properties,
108            provenance: row.provenance,
109            freshness: row.freshness,
110        }
111    }
112}
113
114pub trait ConvexGraphClient {
115    fn upsert_node_row(&self, row: &ConvexNodeRow) -> Result<()>;
116    fn upsert_edge_row(&self, row: &ConvexEdgeRow) -> Result<()>;
117    fn delete_node_row(&self, external_id: &str) -> Result<usize>;
118    fn delete_edge_row(&self, edge_key: &str) -> Result<usize>;
119    fn node_row(&self, external_id: &str) -> Result<Option<ConvexNodeRow>>;
120    fn node_rows(&self) -> Result<Vec<ConvexNodeRow>>;
121    fn edge_rows(&self) -> Result<Vec<ConvexEdgeRow>>;
122    fn node_rows_by_kind(&self, kind: &str) -> Result<Vec<ConvexNodeRow>>;
123    fn outgoing_edge_rows(
124        &self,
125        from_external_id: &str,
126        kind: Option<&str>,
127    ) -> Result<Vec<ConvexEdgeRow>>;
128}
129
130#[derive(Default)]
131pub struct ConvexRowsGraphClient {
132    nodes: RefCell<BTreeMap<String, ConvexNodeRow>>,
133    edges: RefCell<BTreeMap<String, ConvexEdgeRow>>,
134}
135
136impl ConvexRowsGraphClient {
137    pub fn from_rows(rows: ConvexProjectionRows) -> Self {
138        Self {
139            nodes: RefCell::new(
140                rows.nodes
141                    .into_iter()
142                    .map(|row| (row.external_id.clone(), row))
143                    .collect(),
144            ),
145            edges: RefCell::new(
146                rows.edges
147                    .into_iter()
148                    .map(|row| (row.edge_key.clone(), row))
149                    .collect(),
150            ),
151        }
152    }
153
154    pub fn to_rows(&self) -> ConvexProjectionRows {
155        ConvexProjectionRows {
156            nodes: self.nodes.borrow().values().cloned().collect(),
157            edges: self.edges.borrow().values().cloned().collect(),
158        }
159    }
160}
161
162impl ConvexGraphClient for ConvexRowsGraphClient {
163    fn upsert_node_row(&self, row: &ConvexNodeRow) -> Result<()> {
164        self.nodes
165            .borrow_mut()
166            .insert(row.external_id.clone(), row.clone());
167        Ok(())
168    }
169
170    fn upsert_edge_row(&self, row: &ConvexEdgeRow) -> Result<()> {
171        self.edges
172            .borrow_mut()
173            .insert(row.edge_key.clone(), row.clone());
174        Ok(())
175    }
176
177    fn delete_node_row(&self, external_id: &str) -> Result<usize> {
178        let mut edges = self.edges.borrow_mut();
179        let incident = edges
180            .values()
181            .filter(|row| row.from_external_id == external_id || row.to_external_id == external_id)
182            .map(|row| row.edge_key.clone())
183            .collect::<Vec<_>>();
184        for edge_key in incident {
185            edges.remove(&edge_key);
186        }
187        Ok(usize::from(
188            self.nodes.borrow_mut().remove(external_id).is_some(),
189        ))
190    }
191
192    fn delete_edge_row(&self, edge_key: &str) -> Result<usize> {
193        Ok(usize::from(
194            self.edges.borrow_mut().remove(edge_key).is_some(),
195        ))
196    }
197
198    fn node_row(&self, external_id: &str) -> Result<Option<ConvexNodeRow>> {
199        Ok(self.nodes.borrow().get(external_id).cloned())
200    }
201
202    fn node_rows(&self) -> Result<Vec<ConvexNodeRow>> {
203        Ok(self.nodes.borrow().values().cloned().collect())
204    }
205
206    fn edge_rows(&self) -> Result<Vec<ConvexEdgeRow>> {
207        Ok(self.edges.borrow().values().cloned().collect())
208    }
209
210    fn node_rows_by_kind(&self, kind: &str) -> Result<Vec<ConvexNodeRow>> {
211        Ok(self
212            .nodes
213            .borrow()
214            .values()
215            .filter(|row| row.kind == kind)
216            .cloned()
217            .collect())
218    }
219
220    fn outgoing_edge_rows(
221        &self,
222        from_external_id: &str,
223        kind: Option<&str>,
224    ) -> Result<Vec<ConvexEdgeRow>> {
225        Ok(self
226            .edges
227            .borrow()
228            .values()
229            .filter(|row| row.from_external_id == from_external_id)
230            .filter(|row| kind.is_none_or(|kind| row.kind == kind))
231            .cloned()
232            .collect())
233    }
234}
235
236pub struct ConvexGraphStore<C> {
237    client: C,
238}
239
240impl<C> ConvexGraphStore<C> {
241    pub fn new(client: C) -> Self {
242        Self { client }
243    }
244
245    pub fn client(&self) -> &C {
246        &self.client
247    }
248
249    pub fn into_inner(self) -> C {
250        self.client
251    }
252}
253
254impl<C: ConvexGraphClient> GraphStore for ConvexGraphStore<C> {
255    fn upsert_node(&self, node: &GraphNode) -> Result<()> {
256        self.client.upsert_node_row(&ConvexNodeRow::from(node))
257    }
258
259    fn upsert_edge(&self, edge: &GraphEdge) -> Result<()> {
260        if self.client.node_row(&edge.from_id)?.is_none() {
261            bail!(
262                "convex graph edge {} -> {} ({}) references missing from node",
263                edge.from_id,
264                edge.to_id,
265                edge.kind
266            );
267        }
268        if self.client.node_row(&edge.to_id)?.is_none() {
269            bail!(
270                "convex graph edge {} -> {} ({}) references missing to node",
271                edge.from_id,
272                edge.to_id,
273                edge.kind
274            );
275        }
276        self.client.upsert_edge_row(&ConvexEdgeRow::from(edge))
277    }
278
279    fn delete_node(&self, id: &str) -> Result<usize> {
280        let incident = self
281            .client
282            .edge_rows()?
283            .into_iter()
284            .filter(|row| row.from_external_id == id || row.to_external_id == id)
285            .map(|row| row.edge_key)
286            .collect::<Vec<_>>();
287        for edge_key in incident {
288            self.client.delete_edge_row(&edge_key)?;
289        }
290        self.client.delete_node_row(id)
291    }
292
293    fn delete_edge(&self, from_id: &str, to_id: &str, kind: &str) -> Result<usize> {
294        self.client
295            .delete_edge_row(&ConvexEdgeRow::stable_key(from_id, to_id, kind))
296    }
297
298    fn node(&self, id: &str) -> Result<Option<GraphNode>> {
299        Ok(self.client.node_row(id)?.map(GraphNode::from))
300    }
301
302    fn all_nodes(&self) -> Result<Vec<GraphNode>> {
303        let mut nodes: Vec<GraphNode> = self
304            .client
305            .node_rows()?
306            .into_iter()
307            .map(GraphNode::from)
308            .collect();
309        nodes.sort_by(|left, right| left.id.cmp(&right.id));
310        Ok(nodes)
311    }
312
313    fn all_edges(&self) -> Result<Vec<GraphEdge>> {
314        let mut edges: Vec<GraphEdge> = self
315            .client
316            .edge_rows()?
317            .into_iter()
318            .map(GraphEdge::from)
319            .collect();
320        edges.sort_by(|left, right| {
321            left.from_id
322                .cmp(&right.from_id)
323                .then(left.kind.cmp(&right.kind))
324                .then(left.to_id.cmp(&right.to_id))
325        });
326        Ok(edges)
327    }
328
329    fn graph_counts(&self) -> Result<(usize, usize)> {
330        Ok((
331            self.client.node_rows()?.len(),
332            self.client.edge_rows()?.len(),
333        ))
334    }
335
336    fn nodes_by_kind(&self, kind: &str) -> Result<Vec<GraphNode>> {
337        let mut nodes: Vec<GraphNode> = self
338            .client
339            .node_rows_by_kind(kind)?
340            .into_iter()
341            .map(GraphNode::from)
342            .collect();
343        nodes.sort_by(|left, right| left.id.cmp(&right.id));
344        Ok(nodes)
345    }
346
347    fn outgoing_edges(&self, from_id: &str, kind: Option<&str>) -> Result<Vec<GraphEdge>> {
348        let mut edges: Vec<GraphEdge> = self
349            .client
350            .outgoing_edge_rows(from_id, kind)?
351            .into_iter()
352            .map(GraphEdge::from)
353            .collect();
354        edges.sort_by(|left, right| {
355            left.to_id
356                .cmp(&right.to_id)
357                .then(left.kind.cmp(&right.kind))
358        });
359        Ok(edges)
360    }
361
362    fn shortest_path(
363        &self,
364        from_id: &str,
365        to_id: &str,
366        kind: Option<&str>,
367    ) -> Result<Option<GraphPath>> {
368        shortest_path_using_outgoing(from_id, to_id, kind, None, |current, kind| {
369            self.outgoing_edges(current, kind)
370        })
371    }
372}