Skip to main content

meshdb_executor/
reader.rs

1use crate::error::Result;
2use meshdb_core::{Edge, EdgeId, Node, NodeId, Property};
3use meshdb_storage::{PropertyConstraintSpec, StorageEngine};
4
5/// Read-side counterpart to [`crate::GraphWriter`]. Gives the executor a
6/// uniform view of the graph regardless of whether the data behind it lives
7/// entirely in a local storage engine (single-node or full-replica Raft
8/// mode) or is sharded across cluster peers (routing mode, where a
9/// partitioned reader fans out point reads to owners and scatter-gathers
10/// bulk scans).
11///
12/// Methods are sync because the executor's iterator model is sync. Async-
13/// backed implementations (e.g. a remote reader that talks gRPC) bridge via
14/// `Handle::block_on`; callers must run the executor inside `spawn_blocking`
15/// so they don't stall the tokio runtime.
16pub trait GraphReader: Send + Sync {
17    fn get_node(&self, id: NodeId) -> Result<Option<Node>>;
18    fn get_edge(&self, id: EdgeId) -> Result<Option<Edge>>;
19    fn all_node_ids(&self) -> Result<Vec<NodeId>>;
20    fn nodes_by_label(&self, label: &str) -> Result<Vec<NodeId>>;
21    /// Equality lookup via a property index. Callers (planner) must
22    /// have verified the `(label, property)` index exists before
23    /// emitting this call — fallback implementations are free to do a
24    /// label-scan-and-filter for impls that don't maintain their own
25    /// property index, but the storage-backed reader treats a call on
26    /// a non-existent index as an empty result since no entries are
27    /// maintained.
28    fn nodes_by_property(
29        &self,
30        label: &str,
31        property: &str,
32        value: &Property,
33    ) -> Result<Vec<NodeId>>;
34    /// Composite form of [`Self::nodes_by_property`]. `properties`
35    /// and `values` are parallel slices of equal length — every
36    /// slot must be present for the call to land a match. The
37    /// default impl delegates to `nodes_by_property` for length-1
38    /// slices and returns empty otherwise, so readers that haven't
39    /// wired a native composite seek degrade to no-results rather
40    /// than mis-answering. The storage-backed blanket overrides
41    /// with a real composite lookup.
42    fn nodes_by_properties(
43        &self,
44        label: &str,
45        properties: &[String],
46        values: &[Property],
47    ) -> Result<Vec<NodeId>> {
48        if properties.len() == 1 && values.len() == 1 {
49            return self.nodes_by_property(label, &properties[0], &values[0]);
50        }
51        Ok(Vec::new())
52    }
53    /// Relationship-scope analogue of [`Self::nodes_by_property`].
54    /// The planner only emits an `EdgeSeek` after confirming a
55    /// `(edge_type, property)` index is registered via
56    /// [`Self::list_edge_property_indexes`]. Default impl returns
57    /// empty so readers that haven't wired a native seek path
58    /// degrade to no-results rather than mis-answering; the
59    /// storage-backed blanket overrides with a real lookup.
60    fn edges_by_property(
61        &self,
62        _edge_type: &str,
63        _property: &str,
64        _value: &Property,
65    ) -> Result<Vec<EdgeId>> {
66        Ok(Vec::new())
67    }
68    /// Snapshot the `(label, property)` pairs of every property
69    /// index visible through this reader. Used by `SHOW INDEXES`.
70    /// Default impl returns empty — the storage-backed reader
71    /// overrides via the blanket impl, and partitioned/overlay
72    /// readers delegate to their bases.
73    fn list_property_indexes(&self) -> Result<Vec<(String, Vec<String>)>> {
74        Ok(Vec::new())
75    }
76    /// Relationship-scope analogue of [`Self::list_property_indexes`].
77    /// Returns `(edge_type, property)` pairs for every registered
78    /// edge property index. Default impl returns empty; overlay
79    /// and partitioned readers delegate to their bases.
80    fn list_edge_property_indexes(&self) -> Result<Vec<(String, Vec<String>)>> {
81        Ok(Vec::new())
82    }
83    /// Snapshot the `(label, property)` pairs of every point /
84    /// spatial index visible through this reader. Used by `SHOW
85    /// POINT INDEXES`. Default impl returns empty; storage-backed
86    /// readers override via the blanket impl.
87    fn list_point_indexes(&self) -> Result<Vec<(String, String)>> {
88        Ok(Vec::new())
89    }
90    /// Relationship-scope analogue of [`Self::list_point_indexes`].
91    /// `(edge_type, property)` pairs. Default impl returns empty.
92    fn list_edge_point_indexes(&self) -> Result<Vec<(String, String)>> {
93        Ok(Vec::new())
94    }
95    /// Axis-aligned bounding-box range query over the point index
96    /// `(label, property)`. Returns every node carrying `label`
97    /// whose `property` is a `Property::Point` under `srid` that
98    /// falls inside `[xlo..xhi] × [ylo..yhi]`.
99    ///
100    /// Default impl does the naive scan (iterate `nodes_by_label`,
101    /// filter in memory) so that readers that don't maintain a
102    /// spatial index — or that haven't wired a native range query —
103    /// stay correct, just slow. Partitioned readers inherit this
104    /// default and get cluster-wide correctness through the
105    /// scatter-gather `nodes_by_label` underneath. The
106    /// storage-backed blanket overrides with the Z-order seek.
107    fn nodes_in_bbox(
108        &self,
109        label: &str,
110        property: &str,
111        srid: i32,
112        xlo: f64,
113        ylo: f64,
114        xhi: f64,
115        yhi: f64,
116    ) -> Result<Vec<NodeId>> {
117        let (lox, hix) = if xlo <= xhi { (xlo, xhi) } else { (xhi, xlo) };
118        let (loy, hiy) = if ylo <= yhi { (ylo, yhi) } else { (yhi, ylo) };
119        let mut result: Vec<NodeId> = Vec::new();
120        for id in self.nodes_by_label(label)? {
121            let Some(node) = self.get_node(id)? else {
122                continue;
123            };
124            if let Some(Property::Point(p)) = node.properties.get(property) {
125                if p.srid == srid && p.x >= lox && p.x <= hix && p.y >= loy && p.y <= hiy {
126                    result.push(id);
127                }
128            }
129        }
130        Ok(result)
131    }
132    /// Relationship-scope analogue of [`Self::nodes_in_bbox`].
133    /// Default impl returns empty — edge-scoped bbox queries aren't
134    /// yet part of the planner's rewrite surface, so no read path
135    /// exercises this on anything but the storage-backed blanket.
136    /// When the edge point-seek lowering lands this default should
137    /// grow a naive scan via `edges_by_type` (needs to be added to
138    /// the trait first).
139    fn edges_in_bbox(
140        &self,
141        _edge_type: &str,
142        _property: &str,
143        _srid: i32,
144        _xlo: f64,
145        _ylo: f64,
146        _xhi: f64,
147        _yhi: f64,
148    ) -> Result<Vec<EdgeId>> {
149        Ok(Vec::new())
150    }
151    /// Snapshot every registered constraint visible through this
152    /// reader, for `SHOW CONSTRAINTS` and `db.constraints()`. Default
153    /// impl returns empty; storage-backed readers override.
154    fn list_property_constraints(&self) -> Result<Vec<PropertyConstraintSpec>> {
155        Ok(Vec::new())
156    }
157    fn outgoing(&self, id: NodeId) -> Result<Vec<(EdgeId, NodeId)>>;
158    fn incoming(&self, id: NodeId) -> Result<Vec<(EdgeId, NodeId)>>;
159}
160
161/// Blanket impl: any **sized** type that implements [`StorageEngine`]
162/// is automatically a [`GraphReader`]. Covers the concrete
163/// [`meshdb_storage::RocksDbStorageEngine`] — a `&RocksDbStorageEngine`
164/// coerces to `&dyn GraphReader` via this blanket.
165///
166/// Not covered: `dyn StorageEngine` itself. Rust does not transitively
167/// coerce `&dyn StorageEngine` to `&dyn GraphReader` because trait
168/// objects of unrelated traits carry different vtables and there's no
169/// supertrait relationship connecting them. Call sites that hold a
170/// `&dyn StorageEngine` should use [`StorageReaderAdapter`] to wrap it
171/// as a `GraphReader`.
172impl<T: StorageEngine> GraphReader for T {
173    fn get_node(&self, id: NodeId) -> Result<Option<Node>> {
174        Ok(StorageEngine::get_node(self, id)?)
175    }
176
177    fn get_edge(&self, id: EdgeId) -> Result<Option<Edge>> {
178        Ok(StorageEngine::get_edge(self, id)?)
179    }
180
181    fn all_node_ids(&self) -> Result<Vec<NodeId>> {
182        Ok(StorageEngine::all_node_ids(self)?)
183    }
184
185    fn nodes_by_label(&self, label: &str) -> Result<Vec<NodeId>> {
186        Ok(StorageEngine::nodes_by_label(self, label)?)
187    }
188
189    fn nodes_by_property(
190        &self,
191        label: &str,
192        property: &str,
193        value: &Property,
194    ) -> Result<Vec<NodeId>> {
195        Ok(StorageEngine::nodes_by_property(
196            self, label, property, value,
197        )?)
198    }
199
200    fn nodes_by_properties(
201        &self,
202        label: &str,
203        properties: &[String],
204        values: &[Property],
205    ) -> Result<Vec<NodeId>> {
206        Ok(StorageEngine::nodes_by_properties(
207            self, label, properties, values,
208        )?)
209    }
210
211    fn edges_by_property(
212        &self,
213        edge_type: &str,
214        property: &str,
215        value: &Property,
216    ) -> Result<Vec<EdgeId>> {
217        Ok(StorageEngine::edges_by_property(
218            self, edge_type, property, value,
219        )?)
220    }
221
222    fn list_property_indexes(&self) -> Result<Vec<(String, Vec<String>)>> {
223        Ok(StorageEngine::list_property_indexes(self)
224            .into_iter()
225            .map(|s| (s.label, s.properties))
226            .collect())
227    }
228
229    fn list_edge_property_indexes(&self) -> Result<Vec<(String, Vec<String>)>> {
230        Ok(StorageEngine::list_edge_property_indexes(self)
231            .into_iter()
232            .map(|s| (s.edge_type, s.properties))
233            .collect())
234    }
235
236    fn list_point_indexes(&self) -> Result<Vec<(String, String)>> {
237        Ok(StorageEngine::list_point_indexes(self)
238            .into_iter()
239            .map(|s| (s.label, s.property))
240            .collect())
241    }
242
243    fn list_edge_point_indexes(&self) -> Result<Vec<(String, String)>> {
244        Ok(StorageEngine::list_edge_point_indexes(self)
245            .into_iter()
246            .map(|s| (s.edge_type, s.property))
247            .collect())
248    }
249
250    fn nodes_in_bbox(
251        &self,
252        label: &str,
253        property: &str,
254        srid: i32,
255        xlo: f64,
256        ylo: f64,
257        xhi: f64,
258        yhi: f64,
259    ) -> Result<Vec<NodeId>> {
260        // Storage-backed readers go through the Z-order seek path;
261        // skip the default naive scan.
262        Ok(StorageEngine::nodes_in_bbox(
263            self, label, property, srid, xlo, ylo, xhi, yhi,
264        )?)
265    }
266
267    fn edges_in_bbox(
268        &self,
269        edge_type: &str,
270        property: &str,
271        srid: i32,
272        xlo: f64,
273        ylo: f64,
274        xhi: f64,
275        yhi: f64,
276    ) -> Result<Vec<EdgeId>> {
277        Ok(StorageEngine::edges_in_bbox(
278            self, edge_type, property, srid, xlo, ylo, xhi, yhi,
279        )?)
280    }
281
282    fn list_property_constraints(&self) -> Result<Vec<PropertyConstraintSpec>> {
283        Ok(StorageEngine::list_property_constraints(self))
284    }
285
286    fn outgoing(&self, id: NodeId) -> Result<Vec<(EdgeId, NodeId)>> {
287        Ok(StorageEngine::outgoing(self, id)?)
288    }
289
290    fn incoming(&self, id: NodeId) -> Result<Vec<(EdgeId, NodeId)>> {
291        Ok(StorageEngine::incoming(self, id)?)
292    }
293}
294
295/// Adapter that lets a `&dyn StorageEngine` act as a `GraphReader`.
296/// Needed because trait objects of unrelated traits don't coerce —
297/// see the note on the blanket `impl<T: StorageEngine> GraphReader for T`.
298/// Wraps a trait-object reference; no heap allocation. Works for both
299/// reads and writes via the sibling [`crate::writer::StorageWriterAdapter`].
300pub struct StorageReaderAdapter<'a>(pub &'a dyn StorageEngine);
301
302impl GraphReader for StorageReaderAdapter<'_> {
303    fn get_node(&self, id: NodeId) -> Result<Option<Node>> {
304        Ok(self.0.get_node(id)?)
305    }
306
307    fn get_edge(&self, id: EdgeId) -> Result<Option<Edge>> {
308        Ok(self.0.get_edge(id)?)
309    }
310
311    fn all_node_ids(&self) -> Result<Vec<NodeId>> {
312        Ok(self.0.all_node_ids()?)
313    }
314
315    fn nodes_by_label(&self, label: &str) -> Result<Vec<NodeId>> {
316        Ok(self.0.nodes_by_label(label)?)
317    }
318
319    fn nodes_by_property(
320        &self,
321        label: &str,
322        property: &str,
323        value: &Property,
324    ) -> Result<Vec<NodeId>> {
325        Ok(self.0.nodes_by_property(label, property, value)?)
326    }
327
328    fn edges_by_property(
329        &self,
330        edge_type: &str,
331        property: &str,
332        value: &Property,
333    ) -> Result<Vec<EdgeId>> {
334        Ok(self.0.edges_by_property(edge_type, property, value)?)
335    }
336
337    fn list_property_indexes(&self) -> Result<Vec<(String, Vec<String>)>> {
338        Ok(self
339            .0
340            .list_property_indexes()
341            .into_iter()
342            .map(|s| (s.label, s.properties))
343            .collect())
344    }
345
346    fn list_edge_property_indexes(&self) -> Result<Vec<(String, Vec<String>)>> {
347        Ok(self
348            .0
349            .list_edge_property_indexes()
350            .into_iter()
351            .map(|s| (s.edge_type, s.properties))
352            .collect())
353    }
354
355    fn list_point_indexes(&self) -> Result<Vec<(String, String)>> {
356        Ok(self
357            .0
358            .list_point_indexes()
359            .into_iter()
360            .map(|s| (s.label, s.property))
361            .collect())
362    }
363
364    fn list_edge_point_indexes(&self) -> Result<Vec<(String, String)>> {
365        Ok(self
366            .0
367            .list_edge_point_indexes()
368            .into_iter()
369            .map(|s| (s.edge_type, s.property))
370            .collect())
371    }
372
373    fn nodes_in_bbox(
374        &self,
375        label: &str,
376        property: &str,
377        srid: i32,
378        xlo: f64,
379        ylo: f64,
380        xhi: f64,
381        yhi: f64,
382    ) -> Result<Vec<NodeId>> {
383        Ok(self
384            .0
385            .nodes_in_bbox(label, property, srid, xlo, ylo, xhi, yhi)?)
386    }
387
388    fn edges_in_bbox(
389        &self,
390        edge_type: &str,
391        property: &str,
392        srid: i32,
393        xlo: f64,
394        ylo: f64,
395        xhi: f64,
396        yhi: f64,
397    ) -> Result<Vec<EdgeId>> {
398        Ok(self
399            .0
400            .edges_in_bbox(edge_type, property, srid, xlo, ylo, xhi, yhi)?)
401    }
402
403    fn list_property_constraints(&self) -> Result<Vec<PropertyConstraintSpec>> {
404        Ok(self.0.list_property_constraints())
405    }
406
407    fn outgoing(&self, id: NodeId) -> Result<Vec<(EdgeId, NodeId)>> {
408        Ok(self.0.outgoing(id)?)
409    }
410
411    fn incoming(&self, id: NodeId) -> Result<Vec<(EdgeId, NodeId)>> {
412        Ok(self.0.incoming(id)?)
413    }
414}