Skip to main content

linkrs_storage/client/
traits_reader.rs

1//! Read-only graph, catalog and schema queries.
2
3use crate::cursor::{EdgeCursor, IndexCursor, IndexRow, IndexScanPlan, ScanOptions, VertexCursor};
4use crate::schema::{LabelVersionHistory, PropertyChange};
5use linkrs_core::types::{EdgeTypeInfo, Index, SpaceInfo, TagInfo, VertexId};
6use linkrs_core::{Edge, EdgeDirection, StorageError, Value, Vertex};
7use std::sync::Arc;
8
9/// Read-only data and schema operations.
10pub trait StorageReader: Send + Sync + std::fmt::Debug {
11    fn get_vertex(
12        &self,
13        space: &str,
14        tag: &str,
15        id: &VertexId,
16    ) -> Result<Option<Vertex>, StorageError>;
17
18    /// Monotonic physical layout version of the vertex/edge layout.
19    ///
20    /// Bumped on compaction, restore, and remap. `0` means the
21    /// implementation does not track a layout version (default) —
22    /// consumers then cannot use it to invalidate cached plans.
23    fn layout_version(&self) -> u64 {
24        0
25    }
26
27    /// Self-proven vertex-id domain covering a whole space.
28    ///
29    /// Returns `Some(min..max)` only when the storage can prove that every
30    /// vertex id written to the space is a non-negative i64 within that
31    /// range. `None` means no proof exists (mixed or string ids, or no
32    /// writes) and partition planning must not guess a range.
33    fn vertex_id_domain(&self, space: &str) -> Option<std::ops::Range<i64>> {
34        let _ = space;
35        None
36    }
37
38    /// Fetch a vertex with only the requested properties.
39    ///
40    /// The default implementation calls [`get_vertex`] and filters the
41    /// property map.  Storage engines that natively support column projection
42    /// should override this to avoid reading unneeded columns.
43    fn get_vertex_projected(
44        &self,
45        space: &str,
46        tag: &str,
47        id: &VertexId,
48        projection: &[std::sync::Arc<str>],
49    ) -> Result<Option<Vertex>, StorageError> {
50        let vertex = self.get_vertex(space, tag, id)?;
51        if projection.is_empty() {
52            return Ok(vertex);
53        }
54        Ok(vertex.map(|mut v| {
55            v.tag.properties.retain(|k, _| projection.contains(k));
56            v
57        }))
58    }
59
60    fn scan_vertices(&self, space: &str) -> Result<Vec<Vertex>, StorageError>;
61    fn scan_vertices_by_tag(&self, space: &str, tag: &str) -> Result<Vec<Vertex>, StorageError>;
62    fn scan_vertices_by_tag_paginated(
63        &self,
64        space: &str,
65        tag: &str,
66        offset: usize,
67        limit: usize,
68    ) -> Result<Vec<Vertex>, StorageError> {
69        let _ = (space, tag, offset, limit);
70        Err(StorageError::not_supported(
71            "Native vertex pagination is not supported by this storage implementation",
72        ))
73    }
74    fn scan_vertices_by_prop(
75        &self,
76        space: &str,
77        tag: &str,
78        prop: &str,
79        value: &Value,
80    ) -> Result<Vec<Vertex>, StorageError>;
81
82    fn get_edge(
83        &self,
84        space: &str,
85        src: &VertexId,
86        dst: &VertexId,
87        edge_type: &str,
88        rank: i64,
89    ) -> Result<Option<Edge>, StorageError>;
90
91    /// Fetch an edge with only the requested properties.
92    ///
93    /// The default implementation calls [`get_edge`] and filters the
94    /// property map.  Storage engines that natively support column projection
95    /// should override this to avoid decoding unneeded columns.
96    fn get_edge_projected(
97        &self,
98        space: &str,
99        src: &VertexId,
100        dst: &VertexId,
101        edge_type: &str,
102        rank: i64,
103        projection: &[Arc<str>],
104    ) -> Result<Option<Edge>, StorageError> {
105        let edge = self.get_edge(space, src, dst, edge_type, rank)?;
106        if projection.is_empty() {
107            return Ok(edge);
108        }
109        Ok(edge.map(|mut e| {
110            e.props.retain(|k, _| projection.contains(k));
111            e
112        }))
113    }
114    fn get_node_edges(
115        &self,
116        space: &str,
117        node_id: &VertexId,
118        direction: EdgeDirection,
119        edge_types: &[String],
120    ) -> Result<Vec<Edge>, StorageError>;
121
122    /// Filtered and projected per-node edge fanout.
123    ///
124    /// `edge_types` is pushed to storage: empty means all types, otherwise
125    /// only matching tables are visited. `projection` trims decoded columns
126    /// (`None` means all columns, `Some(&[])` means topology only).
127    /// `limit` caps emitted edges per direction branch.
128    /// The default implementation filters and trims in memory for adapters;
129    /// the native engine overrides with CSR-level pushdown.
130    fn get_node_edges_projected(
131        &self,
132        space: &str,
133        node_id: &VertexId,
134        direction: EdgeDirection,
135        edge_types: &[String],
136        projection: Option<&[Arc<str>]>,
137        limit: Option<usize>,
138    ) -> Result<Vec<Edge>, StorageError> {
139        let mut edges = self.get_node_edges(space, node_id, direction, edge_types)?;
140        if let Some(projection) = projection {
141            if !projection.is_empty() {
142                for edge in &mut edges {
143                    edge.props.retain(|k, _| projection.contains(k));
144                }
145            } else {
146                for edge in &mut edges {
147                    edge.props.clear();
148                }
149            }
150        }
151        if let Some(limit) = limit {
152            edges.truncate(limit);
153        }
154        Ok(edges)
155    }
156
157    /// Batch vertex point lookups with the schema resolved once.
158    ///
159    /// Returns one entry per input id in input order. The default
160    /// implementation loops over `get_vertex` for adapters; the native
161    /// engine overrides with a single-timestamp batched path.
162    fn get_vertices_batch(
163        &self,
164        space: &str,
165        tag: &str,
166        ids: &[VertexId],
167    ) -> Result<Vec<Option<Vertex>>, StorageError> {
168        let mut out = Vec::with_capacity(ids.len());
169        for id in ids {
170            out.push(self.get_vertex(space, tag, id)?);
171        }
172        Ok(out)
173    }
174
175    /// Batch vertex lookups decoding only the requested properties.
176    ///
177    /// Same ordering, timestamp and visibility semantics as
178    /// [`get_vertices_batch`](Self::get_vertices_batch): one entry per input
179    /// id in input order, `None` for missing vertices. An empty projection
180    /// reads the full vertex, matching [`get_vertex_projected`](Self::get_vertex_projected).
181    /// The default implementation loops over `get_vertex_projected` for
182    /// adapters; the native engine overrides with a single-timestamp batched
183    /// path. Query expand hops use this for destination bypass columns so a
184    /// property projection never pays full-attribute decoding per chunk.
185    fn get_vertices_projected_batch(
186        &self,
187        space: &str,
188        tag: &str,
189        ids: &[VertexId],
190        projection: &[std::sync::Arc<str>],
191    ) -> Result<Vec<Option<Vertex>>, StorageError> {
192        let mut out = Vec::with_capacity(ids.len());
193        for id in ids {
194            out.push(self.get_vertex_projected(space, tag, id, projection)?);
195        }
196        Ok(out)
197    }
198
199    /// Whether any edge type reached by a topology fast path has TTL
200    /// configured.
201    ///
202    /// Fast paths that read raw CSR adjacency (`neighbor_dst_ids_batch`,
203    /// `out_degree_batch`, multi-hop intermediate hops, GetNeighbors source)
204    /// never materialize edge properties, so they cannot evaluate TTL. Before
205    /// taking such a path a caller checks this predicate and, when it is true,
206    /// degrades to the materializing path so results match the "expired means
207    /// absent" contract.
208    ///
209    /// `edge_types` empty means "every edge type in the space". Returns `true`
210    /// when at least one of the relevant edge types has a valid TTL.
211    fn edge_types_ttl_active(
212        &self,
213        space: &str,
214        edge_types: &[String],
215    ) -> Result<bool, StorageError> {
216        let is_active = |info: &EdgeTypeInfo| {
217            linkrs_core::types::ttl::has_valid_ttl(info.ttl_duration, info.ttl_col.as_deref())
218        };
219        if edge_types.is_empty() {
220            Ok(self.list_edge_types(space)?.iter().any(is_active))
221        } else {
222            for name in edge_types {
223                if let Some(info) = self.get_edge_type(space, name)? {
224                    if is_active(&info) {
225                        return Ok(true);
226                    }
227                }
228            }
229            Ok(false)
230        }
231    }
232
233    /// Lightweight batch neighbor read used by de-materialized expand hops
234    /// (`id_only`/`count_only`).  Resolves the edge-type schema once for the
235    /// batch and reads MVCC neighbors directly from the CSR, skipping
236    /// `EdgeRecord` materialization and per-edge property decoding.
237    ///
238    /// Returns the external neighbor `VertexId`s per input source id, in input
239    /// order.
240    fn neighbor_dst_ids_batch(
241        &self,
242        space: &str,
243        src_ids: &[VertexId],
244        direction: EdgeDirection,
245        edge_types: &[String],
246    ) -> Result<Vec<Vec<VertexId>>, StorageError>;
247
248    /// Batch out-degree read for count-only expand tails.  Counts distinct
249    /// edges per source id, in input order.
250    fn out_degree_batch(
251        &self,
252        space: &str,
253        src_ids: &[VertexId],
254        direction: EdgeDirection,
255        edge_types: &[String],
256    ) -> Result<Vec<usize>, StorageError>;
257
258    fn scan_edges_by_type(&self, space: &str, edge_type: &str) -> Result<Vec<Edge>, StorageError>;
259    /// Table-sharded parallel variant of [`Self::scan_edges_by_type`]: the
260    /// type's edge tables fan out across `shards` rayon shards sharing one
261    /// read timestamp, concatenated in key order. Single-table types fall
262    /// back to the serial scan internally. The default body is the serial
263    /// scan, so adapters and test doubles inherit it unchanged.
264    fn scan_edges_by_type_parallel(
265        &self,
266        space: &str,
267        edge_type: &str,
268        shards: usize,
269    ) -> Result<Vec<Edge>, StorageError> {
270        let _ = shards;
271        self.scan_edges_by_type(space, edge_type)
272    }
273    fn scan_all_edges(&self, space: &str) -> Result<Vec<Edge>, StorageError>;
274    fn count_vertices_by_tag(&self, space: &str, tag: &str) -> Result<u64, StorageError>;
275    fn count_edges_by_type(&self, space: &str, edge_type: &str) -> Result<u64, StorageError>;
276
277    /// Scan edges of a specific type with pagination support.
278    /// Returns at most `limit` edges starting from `offset`.
279    /// The `offset` parameter is 0-based.
280    /// The `limit` parameter controls the page size.
281    fn scan_edges_by_type_paginated(
282        &self,
283        space: &str,
284        edge_type: &str,
285        offset: usize,
286        limit: usize,
287    ) -> Result<Vec<Edge>, StorageError> {
288        let _ = (space, edge_type, offset, limit);
289        Err(StorageError::not_supported(
290            "Native edge pagination is not supported by this storage implementation",
291        ))
292    }
293
294    fn lookup_index(
295        &self,
296        space: &str,
297        index: &str,
298        value: &Value,
299    ) -> Result<Vec<Value>, StorageError>;
300
301    /// Enable the per-table edge property index for `edge_type`, building it
302    /// from existing edge data. Returns `true` if the index was enabled.
303    fn enable_edge_property_index(
304        &self,
305        space: &str,
306        edge_type: &str,
307        pool_capacity: u64,
308    ) -> Result<bool, StorageError> {
309        let _ = (space, edge_type, pool_capacity);
310        Err(StorageError::not_supported(
311            "Edge property index management is not supported by this storage implementation",
312        ))
313    }
314
315    /// Whether the per-table edge property index is enabled for `edge_type`.
316    fn has_edge_property_index(&self, space: &str, edge_type: &str) -> Result<bool, StorageError> {
317        let _ = (space, edge_type);
318        Err(StorageError::not_supported(
319            "Edge property index management is not supported by this storage implementation",
320        ))
321    }
322
323    /// Drop the per-table edge property index for `edge_type`.
324    fn disable_edge_property_index(
325        &self,
326        space: &str,
327        edge_type: &str,
328    ) -> Result<(), StorageError> {
329        let _ = (space, edge_type);
330        Err(StorageError::not_supported(
331            "Edge property index management is not supported by this storage implementation",
332        ))
333    }
334
335    /// Look up edges of `edge_type` whose `prop_name` value falls within
336    /// `[lower, upper)` using the per-table edge property index.
337    ///
338    /// Bounds are `Value`-typed; the storage layer encodes them with the
339    /// ordered codec and applies the inclusion flags. Unbounded side = `None`.
340    #[allow(clippy::too_many_arguments)]
341    fn lookup_edges_by_property_range(
342        &self,
343        space: &str,
344        edge_type: &str,
345        prop_name: &str,
346        lower: Option<&Value>,
347        upper: Option<&Value>,
348        include_lower: bool,
349        include_upper: bool,
350    ) -> Result<Vec<Edge>, StorageError> {
351        let _ = (
352            space,
353            edge_type,
354            prop_name,
355            lower,
356            upper,
357            include_lower,
358            include_upper,
359        );
360        Err(StorageError::not_supported(
361            "Edge property range lookup is not supported by this storage implementation",
362        ))
363    }
364
365    fn get_vertex_with_schema(
366        &self,
367        space: &str,
368        tag: &str,
369        id: &Value,
370    ) -> Result<Option<(TagInfo, Vec<u8>)>, StorageError>;
371    fn get_edge_with_schema(
372        &self,
373        space: &str,
374        edge_type: &str,
375        src: &Value,
376        dst: &Value,
377    ) -> Result<Option<(EdgeTypeInfo, Vec<u8>)>, StorageError>;
378    fn scan_vertices_with_schema(
379        &self,
380        space: &str,
381        tag: &str,
382    ) -> Result<Vec<(TagInfo, Vec<u8>)>, StorageError>;
383    fn scan_edges_with_schema(
384        &self,
385        space: &str,
386        edge_type: &str,
387    ) -> Result<Vec<(EdgeTypeInfo, Vec<u8>)>, StorageError>;
388
389    fn get_space(&self, space: &str) -> Result<Option<SpaceInfo>, StorageError>;
390    fn get_space_by_id(&self, space_id: u64) -> Result<Option<SpaceInfo>, StorageError>;
391    fn list_spaces(&self) -> Result<Vec<SpaceInfo>, StorageError>;
392    fn get_space_id(&self, space: &str) -> Result<u64, StorageError>;
393    fn space_exists(&self, space: &str) -> bool;
394
395    fn get_tag(&self, space: &str, tag: &str) -> Result<Option<TagInfo>, StorageError>;
396    fn list_tags(&self, space: &str) -> Result<Vec<TagInfo>, StorageError>;
397
398    fn get_edge_type(
399        &self,
400        space: &str,
401        edge_type: &str,
402    ) -> Result<Option<EdgeTypeInfo>, StorageError>;
403    fn list_edge_types(&self, space: &str) -> Result<Vec<EdgeTypeInfo>, StorageError>;
404
405    /// Resolve an edge type name from the storage-level edge type hash.
406    ///
407    /// Edge index rows carry the edge type as a truncated FNV-1a hash of the
408    /// type name (see `edge_entity_ref` in `index/helpers.rs`).  This default
409    /// implementation enumerates the space's edge types and matches the hash
410    /// using the same shared FNV-1a implementation as the index write path so
411    /// the two sides stay consistent.
412    fn resolve_edge_type_name(
413        &self,
414        space: &str,
415        hash: u32,
416    ) -> Result<Option<String>, StorageError> {
417        let edge_types = self.list_edge_types(space)?;
418        Ok(edge_types.into_iter().find_map(|edge_type| {
419            if crate::index::helpers::stable_hash(edge_type.edge_type_name.as_bytes()) as u32
420                == hash
421            {
422                Some(edge_type.edge_type_name)
423            } else {
424                None
425            }
426        }))
427    }
428
429    fn get_tag_index(&self, space: &str, index: &str) -> Result<Option<Index>, StorageError>;
430    fn list_tag_indexes(&self, space: &str) -> Result<Vec<Index>, StorageError>;
431
432    fn get_edge_index(&self, space: &str, index: &str) -> Result<Option<Index>, StorageError>;
433    fn list_edge_indexes(&self, space: &str) -> Result<Vec<Index>, StorageError>;
434
435    /// Schema version history queries
436    /// Query version history for a specific vertex tag
437    fn get_vertex_version_history(
438        &self,
439        space: &str,
440        tag: &str,
441    ) -> Result<Option<LabelVersionHistory>, StorageError>;
442
443    /// Query version history for a specific edge type
444    fn get_edge_version_history(
445        &self,
446        space: &str,
447        edge_type: &str,
448    ) -> Result<Option<LabelVersionHistory>, StorageError>;
449
450    /// Get schema changes between two versions for a vertex tag
451    fn get_vertex_schema_changes(
452        &self,
453        space: &str,
454        tag: &str,
455        from_version: u64,
456        to_version: u64,
457    ) -> Result<Vec<PropertyChange>, StorageError>;
458
459    /// Get schema changes between two versions for an edge type
460    fn get_edge_schema_changes(
461        &self,
462        space: &str,
463        edge_type: &str,
464        from_version: u64,
465        to_version: u64,
466    ) -> Result<Vec<PropertyChange>, StorageError>;
467
468    /// Detect breaking changes between versions for a vertex tag
469    fn detect_vertex_breaking_changes(
470        &self,
471        space: &str,
472        tag: &str,
473        from_version: u64,
474        to_version: u64,
475    ) -> Result<Vec<PropertyChange>, StorageError>;
476
477    /// Detect breaking changes between versions for an edge type
478    fn detect_edge_breaking_changes(
479        &self,
480        space: &str,
481        edge_type: &str,
482        from_version: u64,
483        to_version: u64,
484    ) -> Result<Vec<PropertyChange>, StorageError>;
485
486    // ── Cursor-based scan methods ──
487
488    /// Create a lazy vertex scan cursor.
489    ///
490    /// Implementations must provide a native lazy cursor.
491    fn create_vertex_cursor(
492        &self,
493        _space: &str,
494        _options: &ScanOptions,
495    ) -> Result<Box<dyn VertexCursor>, StorageError> {
496        Err(StorageError::not_supported(
497            "Native vertex cursor is not supported by this storage implementation",
498        ))
499    }
500
501    /// Create a lazy edge scan cursor.
502    ///
503    /// Implementations must provide a native lazy cursor.
504    fn create_edge_cursor(
505        &self,
506        _space: &str,
507        _options: &ScanOptions,
508    ) -> Result<Box<dyn EdgeCursor>, StorageError> {
509        Err(StorageError::not_supported(
510            "Native edge cursor is not supported by this storage implementation",
511        ))
512    }
513
514    /// Create an index cursor for the given index and predicate.
515    ///
516    /// The default implementation returns a capability error.  Storage
517    /// engines with native index cursor support should override this
518    /// to return a lazy cursor.
519    fn create_index_cursor(
520        &self,
521        _plan: &IndexScanPlan,
522    ) -> Result<Box<dyn IndexCursor<Row = IndexRow>>, StorageError> {
523        Err(StorageError::not_supported(
524            "Native index cursor is not supported by this storage engine",
525        ))
526    }
527
528    // ── Migration history ──
529
530    fn list_migration_history(
531        &self,
532        _space: &str,
533        _label: &str,
534        _is_edge: bool,
535    ) -> Result<Vec<crate::MigrationHistoryRecord>, StorageError> {
536        Err(StorageError::not_supported(
537            "Migration history is not supported by this storage implementation",
538        ))
539    }
540
541    fn get_applied_versions(
542        &self,
543        _space: &str,
544        _label: &str,
545        _is_edge: bool,
546    ) -> Result<Vec<u64>, StorageError> {
547        Err(StorageError::not_supported(
548            "Migration history is not supported by this storage implementation",
549        ))
550    }
551
552    fn record_migration_history(
553        &self,
554        _record: crate::MigrationHistoryRecord,
555    ) -> Result<(), StorageError> {
556        Err(StorageError::not_supported(
557            "Migration history is not supported by this storage implementation",
558        ))
559    }
560
561    fn list_all_migration_history(
562        &self,
563    ) -> Result<Vec<crate::MigrationHistoryRecord>, StorageError> {
564        Err(StorageError::not_supported(
565            "Migration history is not supported by this storage implementation",
566        ))
567    }
568}