1use std::borrow::Cow;
4use std::ops::RangeBounds;
5use std::sync::Arc;
6
7use imbl::HashMap;
8use roaring::RoaringBitmap;
9use rustc_hash::FxHashMap;
10use serde::{Deserialize, Serialize};
11use smallvec::SmallVec;
12
13use selene_core::{
14 DbString, EdgeId, GraphId, HnswIndexConfig, IvfIndexConfig, LabelSet, NodeId, PropertyMap,
15 Value,
16};
17
18use crate::adjacency::AdjacencyEntry;
19use crate::composite_typed_index::CompositeTypedIndex;
20use crate::graph_types::GraphTypeDef;
21use crate::id_map::{EngineIdMap, engine_id_map};
22use crate::store::{EdgeStore, NodeStore, RowIndex};
23use crate::text_index::{TextIndex, TextIndexMemoryUsage, TextIndexStats};
24use crate::typed_index::{TypedIndex, TypedIndexKind};
25use crate::vector_index::{VectorIndex, VectorIndexKind, VectorIndexMemoryUsage};
26
27#[derive(Clone, Debug)]
29pub struct PropertyIndexEntry {
30 pub index: Arc<TypedIndex>,
32 pub name: Option<DbString>,
34}
35
36impl PropertyIndexEntry {
37 #[must_use]
39 pub fn new(index: TypedIndex, name: Option<DbString>) -> Self {
40 Self {
41 index: Arc::new(index),
42 name,
43 }
44 }
45
46 #[must_use]
48 pub fn kind(&self) -> TypedIndexKind {
49 self.index.kind()
50 }
51}
52
53#[derive(Clone, Debug)]
55pub struct CompositePropertyIndexEntry {
56 pub index: Arc<CompositeTypedIndex>,
58 pub declared_properties: SmallVec<[DbString; 4]>,
60 pub name: Option<DbString>,
62}
63
64impl CompositePropertyIndexEntry {
65 #[must_use]
67 pub fn new(
68 index: CompositeTypedIndex,
69 declared_properties: SmallVec<[DbString; 4]>,
70 name: Option<DbString>,
71 ) -> Self {
72 Self {
73 index: Arc::new(index),
74 declared_properties,
75 name,
76 }
77 }
78
79 #[must_use]
81 pub fn kinds(&self) -> SmallVec<[TypedIndexKind; 4]> {
82 self.index.kinds().iter().copied().collect()
83 }
84}
85
86#[derive(Clone, Debug)]
88pub struct VectorIndexEntry {
89 pub index: Arc<VectorIndex>,
91 pub name: Option<DbString>,
93}
94
95impl VectorIndexEntry {
96 #[must_use]
98 pub fn new(index: VectorIndex, name: Option<DbString>) -> Self {
99 Self {
100 index: Arc::new(index),
101 name,
102 }
103 }
104
105 #[must_use]
107 pub fn kind(&self) -> VectorIndexKind {
108 self.index.kind()
109 }
110
111 #[must_use]
113 pub fn dimension(&self) -> u32 {
114 self.index.dimension()
115 }
116
117 #[must_use]
119 pub fn hnsw_config(&self) -> Option<HnswIndexConfig> {
120 self.index.hnsw_config()
121 }
122
123 #[must_use]
125 pub fn ivf_config(&self) -> Option<IvfIndexConfig> {
126 self.index.ivf_config()
127 }
128
129 #[must_use]
131 pub fn memory_usage(&self) -> VectorIndexMemoryUsage {
132 self.index.memory_usage()
133 }
134}
135
136#[derive(Clone, Debug)]
138pub struct TextIndexEntry {
139 pub index: Arc<TextIndex>,
141 pub name: Option<DbString>,
143}
144
145impl TextIndexEntry {
146 #[must_use]
148 pub fn new(index: TextIndex, name: Option<DbString>) -> Self {
149 Self {
150 index: Arc::new(index),
151 name,
152 }
153 }
154
155 #[must_use]
157 pub fn stats(&self) -> TextIndexStats {
158 self.index.stats()
159 }
160
161 #[must_use]
163 pub fn memory_usage(&self) -> TextIndexMemoryUsage {
164 self.index.memory_usage()
165 }
166}
167
168pub type CompositePropertyIndexEntryRow = (
170 DbString,
171 SmallVec<[DbString; 4]>,
172 SmallVec<[TypedIndexKind; 4]>,
173 Option<DbString>,
174);
175
176pub type VectorIndexEntryRow = (
178 DbString,
179 DbString,
180 VectorIndexKind,
181 u32,
182 Option<HnswIndexConfig>,
183 Option<IvfIndexConfig>,
184 Option<DbString>,
185);
186
187pub type TextIndexEntryRow = (
189 DbString,
190 DbString,
191 TextIndexStats,
192 TextIndexMemoryUsage,
193 Option<DbString>,
194);
195
196#[derive(
198 Clone,
199 Debug,
200 Deserialize,
201 PartialEq,
202 rkyv::Archive,
203 rkyv::Deserialize,
204 rkyv::Serialize,
205 Serialize,
206)]
207pub struct GraphMeta {
208 pub graph_id: GraphId,
210 pub generation: u64,
212 pub next_node_id: u64,
214 pub next_edge_id: u64,
216 pub bound_type: Option<Arc<GraphTypeDef>>,
218}
219
220#[derive(Clone, Debug)]
222pub struct SeleneGraph {
223 pub meta: GraphMeta,
225 pub node_store: NodeStore,
227 pub edge_store: EdgeStore,
229 pub adjacency_out: EngineIdMap<NodeId, AdjacencyEntry>,
231 pub adjacency_in: EngineIdMap<NodeId, AdjacencyEntry>,
233 pub idx_label: HashMap<DbString, RoaringBitmap>,
235 pub idx_edge_label: HashMap<DbString, RoaringBitmap>,
237 pub property_index: FxHashMap<(DbString, DbString), PropertyIndexEntry>,
239 pub composite_property_index:
241 FxHashMap<(DbString, SmallVec<[DbString; 4]>), CompositePropertyIndexEntry>,
242 pub vector_index: FxHashMap<(DbString, DbString), VectorIndexEntry>,
244 pub text_index: FxHashMap<(DbString, DbString), TextIndexEntry>,
246 pub node_id_to_row: EngineIdMap<NodeId, RowIndex>,
251 pub edge_id_to_row: EngineIdMap<EdgeId, RowIndex>,
253}
254
255impl SeleneGraph {
256 #[must_use]
258 pub fn new(graph_id: GraphId) -> Self {
259 Self {
260 meta: GraphMeta {
261 graph_id,
262 generation: 0,
263 next_node_id: 1,
264 next_edge_id: 1,
265 bound_type: None,
266 },
267 node_store: NodeStore::new(),
268 edge_store: EdgeStore::new(),
269 adjacency_out: engine_id_map(),
270 adjacency_in: engine_id_map(),
271 idx_label: HashMap::new(),
272 idx_edge_label: HashMap::new(),
273 property_index: FxHashMap::default(),
274 composite_property_index: FxHashMap::default(),
275 vector_index: FxHashMap::default(),
276 text_index: FxHashMap::default(),
277 node_id_to_row: engine_id_map(),
278 edge_id_to_row: engine_id_map(),
279 }
280 }
281
282 #[must_use]
284 pub const fn graph_id(&self) -> GraphId {
285 self.meta.graph_id
286 }
287
288 #[must_use]
290 pub fn node_count(&self) -> usize {
291 self.node_store.alive.len() as usize
292 }
293
294 #[must_use]
302 pub fn live_nodes(&self) -> &RoaringBitmap {
303 &self.node_store.alive
306 }
307
308 #[must_use]
310 pub fn edge_count(&self) -> usize {
311 self.edge_store.alive.len() as usize
312 }
313
314 #[must_use]
319 pub fn compaction_stats(&self) -> crate::compaction::CompactionStats {
320 crate::compaction::CompactionStats::from_graph(self)
321 }
322
323 #[must_use]
333 pub fn live_edges(&self) -> &RoaringBitmap {
334 &self.edge_store.alive
336 }
337
338 #[must_use]
346 pub fn row_for_node_id(&self, id: NodeId) -> Option<RowIndex> {
347 self.node_id_to_row.get(&id).copied()
348 }
349
350 #[must_use]
353 pub fn row_for_edge_id(&self, id: EdgeId) -> Option<RowIndex> {
354 self.edge_id_to_row.get(&id).copied()
355 }
356
357 #[must_use]
363 pub fn node_id_for_row(&self, row: RowIndex) -> Option<NodeId> {
364 self.node_store
365 .row_to_id
366 .get(row.get() as usize)
367 .copied()
368 .filter(|id| *id != NodeId::TOMBSTONE)
369 }
370
371 #[must_use]
374 pub fn edge_id_for_row(&self, row: RowIndex) -> Option<EdgeId> {
375 self.edge_store
376 .row_to_id
377 .get(row.get() as usize)
378 .copied()
379 .filter(|id| *id != EdgeId::TOMBSTONE)
380 }
381
382 #[must_use]
384 pub fn is_node_alive(&self, id: NodeId) -> bool {
385 self.live_node_row(id).is_some()
386 }
387
388 #[must_use]
390 pub fn is_edge_alive(&self, id: EdgeId) -> bool {
391 self.live_edge_row(id).is_some()
392 }
393
394 #[must_use]
396 pub fn node_labels(&self, id: NodeId) -> Option<&LabelSet> {
397 self.live_node_row(id)
398 .and_then(|row| self.node_store.labels.get(row))
399 }
400
401 #[must_use]
403 pub fn node_properties(&self, id: NodeId) -> Option<&PropertyMap> {
404 self.live_node_row(id)
405 .and_then(|row| self.node_store.properties.get(row))
406 }
407
408 #[must_use]
410 pub fn edge_label(&self, id: EdgeId) -> Option<&DbString> {
411 self.live_edge_row(id)
412 .and_then(|row| self.edge_store.label.get(row))
413 }
414
415 #[must_use]
417 pub fn edge_endpoints(&self, id: EdgeId) -> Option<(NodeId, NodeId)> {
418 self.live_edge_row(id).and_then(|row| {
419 Some((
420 *self.edge_store.source.get(row)?,
421 *self.edge_store.target.get(row)?,
422 ))
423 })
424 }
425
426 #[must_use]
428 pub fn edge_properties(&self, id: EdgeId) -> Option<&PropertyMap> {
429 self.live_edge_row(id)
430 .and_then(|row| self.edge_store.properties.get(row))
431 }
432
433 #[must_use]
435 pub fn outgoing_edges(&self, source: NodeId) -> Option<&AdjacencyEntry> {
436 self.adjacency_out.get(&source)
437 }
438
439 #[must_use]
441 pub fn incoming_edges(&self, target: NodeId) -> Option<&AdjacencyEntry> {
442 self.adjacency_in.get(&target)
443 }
444
445 #[must_use]
447 pub fn node_has_incident_edges(&self, id: NodeId) -> bool {
448 self.outgoing_edges(id)
449 .is_some_and(|entry| !entry.is_empty())
450 || self
451 .incoming_edges(id)
452 .is_some_and(|entry| !entry.is_empty())
453 }
454
455 #[must_use]
457 pub fn nodes_with_label(&self, label: &DbString) -> Option<&RoaringBitmap> {
458 self.idx_label.get(label)
459 }
460
461 #[must_use]
463 pub fn edges_with_label(&self, label: &DbString) -> Option<&RoaringBitmap> {
464 self.idx_edge_label.get(label)
465 }
466
467 #[must_use]
469 pub fn label_count(&self) -> usize {
470 self.idx_label.len()
471 }
472
473 #[must_use]
475 pub fn edge_label_count(&self) -> usize {
476 self.idx_edge_label.len()
477 }
478
479 #[must_use]
481 pub fn property_index_for(
482 &self,
483 label: &DbString,
484 property: &DbString,
485 ) -> Option<Arc<TypedIndex>> {
486 self.property_index
487 .get(&(label.clone(), property.clone()))
488 .map(|entry| Arc::clone(&entry.index))
489 }
490
491 #[must_use]
493 pub fn composite_property_index_for(
494 &self,
495 label: &DbString,
496 properties: &[DbString],
497 ) -> Option<Arc<CompositeTypedIndex>> {
498 self.composite_property_index_entry_for(label, properties)
499 .map(|entry| Arc::clone(&entry.index))
500 }
501
502 #[must_use]
504 pub fn composite_property_index_entry_for(
505 &self,
506 label: &DbString,
507 properties: &[DbString],
508 ) -> Option<&CompositePropertyIndexEntry> {
509 let key = composite_property_key(properties);
510 self.composite_property_index.get(&(label.clone(), key))
511 }
512
513 #[must_use]
515 pub fn vector_index_for(
516 &self,
517 label: &DbString,
518 property: &DbString,
519 ) -> Option<Arc<VectorIndex>> {
520 self.vector_index
521 .get(&(label.clone(), property.clone()))
522 .map(|entry| Arc::clone(&entry.index))
523 }
524
525 #[must_use]
527 pub fn text_index_for(&self, label: &DbString, property: &DbString) -> Option<Arc<TextIndex>> {
528 self.text_index
529 .get(&(label.clone(), property.clone()))
530 .map(|entry| Arc::clone(&entry.index))
531 }
532
533 #[must_use]
535 pub fn property_index_count(&self) -> usize {
536 self.property_index.len()
537 }
538
539 #[must_use]
541 pub fn composite_property_index_count(&self) -> usize {
542 self.composite_property_index.len()
543 }
544
545 #[must_use]
547 pub fn vector_index_count(&self) -> usize {
548 self.vector_index.len()
549 }
550
551 #[must_use]
553 pub fn text_index_count(&self) -> usize {
554 self.text_index.len()
555 }
556
557 pub fn iter_property_indexes(
563 &self,
564 ) -> impl Iterator<Item = (DbString, DbString, TypedIndexKind)> + '_ {
565 self.property_index
566 .iter()
567 .map(|((label, property), entry)| (label.clone(), property.clone(), entry.kind()))
568 }
569
570 pub fn iter_property_index_entries(
572 &self,
573 ) -> impl Iterator<Item = (DbString, DbString, TypedIndexKind, Option<DbString>)> + '_ {
574 self.property_index
575 .iter()
576 .map(|((label, property), entry)| {
577 (
578 label.clone(),
579 property.clone(),
580 entry.kind(),
581 entry.name.clone(),
582 )
583 })
584 }
585
586 pub fn iter_composite_property_index_entries(
588 &self,
589 ) -> impl Iterator<Item = CompositePropertyIndexEntryRow> + '_ {
590 self.composite_property_index
591 .iter()
592 .map(|((label, _), entry)| {
593 (
594 label.clone(),
595 entry.declared_properties.clone(),
596 entry.kinds(),
597 entry.name.clone(),
598 )
599 })
600 }
601
602 pub fn iter_vector_index_entries(&self) -> impl Iterator<Item = VectorIndexEntryRow> + '_ {
604 self.vector_index.iter().map(|((label, property), entry)| {
605 (
606 label.clone(),
607 property.clone(),
608 entry.kind(),
609 entry.dimension(),
610 entry.hnsw_config(),
611 entry.ivf_config(),
612 entry.name.clone(),
613 )
614 })
615 }
616
617 pub fn iter_text_index_entries(&self) -> impl Iterator<Item = TextIndexEntryRow> + '_ {
619 self.text_index.iter().map(|((label, property), entry)| {
620 (
621 label.clone(),
622 property.clone(),
623 entry.stats(),
624 entry.memory_usage(),
625 entry.name.clone(),
626 )
627 })
628 }
629
630 #[must_use]
638 pub fn nodes_with_property_eq(
639 &self,
640 label: &DbString,
641 property: &DbString,
642 value: &Value,
643 ) -> Option<Cow<'_, RoaringBitmap>> {
644 self.property_index
645 .get(&(label.clone(), property.clone()))
646 .and_then(|entry| entry.index.lookup_eq(value))
647 }
648
649 #[must_use]
655 pub fn nodes_with_property_range<R>(
656 &self,
657 label: &DbString,
658 property: &DbString,
659 range: R,
660 ) -> Option<RoaringBitmap>
661 where
662 R: RangeBounds<Value>,
663 {
664 self.property_index
665 .get(&(label.clone(), property.clone()))
666 .and_then(|entry| entry.index.lookup_range(range))
667 }
668
669 #[must_use]
674 pub fn nodes_with_property_prefix(
675 &self,
676 label: &DbString,
677 property: &DbString,
678 prefix: &str,
679 ) -> Option<RoaringBitmap> {
680 self.property_index
681 .get(&(label.clone(), property.clone()))
682 .and_then(|entry| entry.index.lookup_prefix(prefix))
683 }
684
685 fn live_node_row(&self, id: NodeId) -> Option<usize> {
686 let row = self.row_for_node_id(id)?.get();
687 ((row as usize) < self.node_store.len() && self.node_store.is_alive(row))
688 .then_some(row as usize)
689 }
690
691 fn live_edge_row(&self, id: EdgeId) -> Option<usize> {
692 let row = self.row_for_edge_id(id)?.get();
693 ((row as usize) < self.edge_store.len() && self.edge_store.is_alive(row))
694 .then_some(row as usize)
695 }
696}
697
698pub(crate) fn composite_property_key(properties: &[DbString]) -> SmallVec<[DbString; 4]> {
699 let mut key: SmallVec<[DbString; 4]> = properties.iter().cloned().collect();
700 key.sort();
701 key
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707 use selene_core::db_string;
708
709 #[test]
710 fn new_graph_is_empty() {
711 let graph = SeleneGraph::new(GraphId::new(1));
712 assert_eq!(graph.node_count(), 0);
713 assert_eq!(graph.edge_count(), 0);
714 assert_eq!(graph.label_count(), 0);
715 assert_eq!(graph.edge_label_count(), 0);
716 assert_eq!(graph.property_index_count(), 0);
717 assert_eq!(graph.composite_property_index_count(), 0);
718 assert_eq!(graph.vector_index_count(), 0);
719 assert_eq!(graph.text_index_count(), 0);
720 assert!(graph.idx_label.is_empty());
721 assert!(graph.idx_edge_label.is_empty());
722 assert!(graph.property_index.is_empty());
723 assert!(graph.composite_property_index.is_empty());
724 assert!(graph.vector_index.is_empty());
725 assert!(graph.text_index.is_empty());
726 assert_eq!(graph.meta.generation, 0);
727 assert_eq!(graph.meta.next_node_id, 1);
728 assert_eq!(graph.meta.next_edge_id, 1);
729 }
730
731 #[test]
732 fn read_accessors_return_none_for_unknown_ids() {
733 let graph = SeleneGraph::new(GraphId::new(1));
734 assert_eq!(graph.node_labels(NodeId::new(1)), None);
735 assert_eq!(graph.edge_label(EdgeId::new(1)), None);
736 assert_eq!(
737 graph.nodes_with_label(&db_string("graph.missing").unwrap()),
738 None
739 );
740 assert!(!graph.is_node_alive(NodeId::TOMBSTONE));
741 }
742
743 #[test]
744 fn node_labels_returns_some_for_alive_node() {
745 let mut graph = SeleneGraph::new(GraphId::new(1));
746 let label = db_string("graph.node").unwrap();
747 graph
748 .node_store
749 .labels
750 .push(LabelSet::single(label.clone()));
751 graph.node_store.properties.push(PropertyMap::new());
752 graph.node_store.row_to_id.push(NodeId::new(1));
755 graph
756 .node_id_to_row
757 .insert(NodeId::new(1), RowIndex::new(0));
758 graph.node_store.alive_mut().insert(0);
759 assert_eq!(
760 graph
761 .node_labels(NodeId::new(1))
762 .unwrap()
763 .iter()
764 .cloned()
765 .collect::<Vec<_>>(),
766 vec![label]
767 );
768 }
769
770 #[test]
771 fn label_count_reports_distinct_labels_only() {
772 let mut graph = SeleneGraph::new(GraphId::new(1));
773 let label = db_string("graph.same").unwrap();
774 let mut bitmap = RoaringBitmap::new();
775 bitmap.insert(0);
776 bitmap.insert(1);
777 graph.idx_label.insert(label.clone(), bitmap);
778 assert_eq!(graph.label_count(), 1);
779 assert!(graph.nodes_with_label(&label).unwrap().contains(0));
780 assert!(graph.nodes_with_label(&label).unwrap().contains(1));
781 }
782}