Skip to main content

velesdb_core/collection/graph/
range_index.rs

1//! Range index for ordered property lookups using BTreeMap.
2//!
3//! Provides O(log n) range queries (>, <, >=, <=, BETWEEN) instead of O(n) scans.
4
5// Reason: Numeric casts in range index are intentional:
6// - i64->f64 for mixed-type comparisons: precision loss acceptable for ordering
7// - Values represent property values, exact precision not required for comparisons
8#![allow(clippy::cast_precision_loss)]
9
10use super::helpers::{make_label_prop_key, safe_bitmap_id, PostcardPersistence};
11use roaring::RoaringBitmap;
12use serde::{Deserialize, Serialize};
13use std::collections::{BTreeMap, HashMap};
14use std::ops::Bound;
15
16/// Wrapper for comparable numeric values in BTreeMap.
17///
18/// JSON values are converted to this for ordered comparison.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub(crate) enum OrderedValue {
21    /// Null value (sorts first)
22    Null,
23    /// Integer value
24    Integer(i64),
25    /// Float value
26    Float(OrderedFloat),
27    /// String value (lexicographic order)
28    String(String),
29}
30
31/// Wrapper for f64 that implements Ord (NaN sorts last).
32#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
33pub struct OrderedFloat(pub f64);
34
35impl PartialOrd for OrderedFloat {
36    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
37        Some(self.cmp(other))
38    }
39}
40
41impl Eq for OrderedFloat {}
42
43impl Ord for OrderedFloat {
44    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
45        // total_cmp provides the same NaN-last semantics as the manual match
46        // above but without the unreachable branch.
47        self.0.total_cmp(&other.0)
48    }
49}
50
51impl PartialOrd for OrderedValue {
52    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
53        Some(self.cmp(other))
54    }
55}
56
57impl Ord for OrderedValue {
58    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
59        match (self, other) {
60            (Self::Null, Self::Null) => std::cmp::Ordering::Equal,
61            (Self::Null, _) | (Self::Integer(_) | Self::Float(_), Self::String(_)) => {
62                std::cmp::Ordering::Less
63            }
64            (_, Self::Null) | (Self::String(_), Self::Integer(_) | Self::Float(_)) => {
65                std::cmp::Ordering::Greater
66            }
67            (Self::Integer(a), Self::Integer(b)) => a.cmp(b),
68            (Self::Float(a), Self::Float(b)) => a.cmp(b),
69            (Self::Integer(a), Self::Float(b)) => OrderedFloat(*a as f64).cmp(b),
70            (Self::Float(a), Self::Integer(b)) => a.cmp(&OrderedFloat(*b as f64)),
71            (Self::String(a), Self::String(b)) => a.cmp(b),
72        }
73    }
74}
75
76impl OrderedValue {
77    /// Convert a JSON value to an OrderedValue for comparison.
78    #[must_use]
79    pub fn from_json(value: &serde_json::Value) -> Option<Self> {
80        match value {
81            serde_json::Value::Null => Some(Self::Null),
82            serde_json::Value::Number(n) => {
83                if let Some(i) = n.as_i64() {
84                    Some(Self::Integer(i))
85                } else {
86                    n.as_f64().map(|f| Self::Float(OrderedFloat(f)))
87                }
88            }
89            serde_json::Value::String(s) => Some(Self::String(s.clone())),
90            _ => None, // Arrays and objects are not comparable
91        }
92    }
93}
94
95/// Current schema version for RangeIndex serialization.
96/// Increment this when making breaking changes to the index format.
97pub const RANGE_INDEX_VERSION: u32 = 1;
98
99/// Range index for ordered property lookups.
100///
101/// Uses BTreeMap for O(log n) range queries on numeric/string properties.
102///
103/// # Example
104///
105/// ```rust,ignore
106/// let mut index = RangeIndex::new();
107/// index.create_index("Event", "timestamp");
108/// index.insert("Event", "timestamp", &json!(1704067200), 1);
109/// index.insert("Event", "timestamp", &json!(1704153600), 2);
110///
111/// // Range query: timestamp > 1704067200
112/// let nodes = index.range_greater_than("Event", "timestamp", &json!(1704067200));
113/// ```
114#[derive(Debug, Default, Serialize, Deserialize)]
115pub struct RangeIndex {
116    /// Schema version for forward compatibility.
117    #[serde(default = "default_range_version")]
118    version: u32,
119    /// (label, property_name) -> (ordered_value -> node_ids)
120    indexes: HashMap<(String, String), BTreeMap<OrderedValue, RoaringBitmap>>,
121}
122
123fn default_range_version() -> u32 {
124    RANGE_INDEX_VERSION
125}
126
127impl RangeIndex {
128    /// Create a new empty range index.
129    #[must_use]
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Create a range index for a (label, property) pair.
135    pub fn create_index(&mut self, label: &str, property: &str) {
136        self.indexes
137            .entry(make_label_prop_key(label, property))
138            .or_default();
139    }
140
141    /// Check if a range index exists for this (label, property) pair.
142    #[must_use]
143    pub fn has_index(&self, label: &str, property: &str) -> bool {
144        self.indexes
145            .contains_key(&make_label_prop_key(label, property))
146    }
147
148    /// Insert a node into the range index.
149    ///
150    /// Returns `true` if the index exists and the value is comparable.
151    /// Returns `false` if `node_id > u32::MAX` to prevent data corruption.
152    pub fn insert(
153        &mut self,
154        label: &str,
155        property: &str,
156        value: &serde_json::Value,
157        node_id: u64,
158    ) -> bool {
159        let Some(safe_id) = safe_bitmap_id(node_id) else {
160            return false;
161        };
162
163        let key = make_label_prop_key(label, property);
164        if let Some(btree) = self.indexes.get_mut(&key) {
165            if let Some(ordered) = OrderedValue::from_json(value) {
166                btree.entry(ordered).or_default().insert(safe_id);
167                return true;
168            }
169        }
170        false
171    }
172
173    /// Remove a node from the range index.
174    ///
175    /// Returns `false` if `node_id > u32::MAX` (cannot exist in index).
176    pub fn remove(
177        &mut self,
178        label: &str,
179        property: &str,
180        value: &serde_json::Value,
181        node_id: u64,
182    ) -> bool {
183        let Some(safe_id) = safe_bitmap_id(node_id) else {
184            return false;
185        };
186
187        let key = make_label_prop_key(label, property);
188        if let Some(btree) = self.indexes.get_mut(&key) {
189            if let Some(ordered) = OrderedValue::from_json(value) {
190                if let Some(bitmap) = btree.get_mut(&ordered) {
191                    let removed = bitmap.remove(safe_id);
192                    if bitmap.is_empty() {
193                        btree.remove(&ordered);
194                    }
195                    return removed;
196                }
197            }
198        }
199        false
200    }
201
202    /// Range query: value > threshold
203    #[must_use]
204    pub fn range_greater_than(
205        &self,
206        label: &str,
207        property: &str,
208        threshold: &serde_json::Value,
209    ) -> RoaringBitmap {
210        self.range_query(
211            label,
212            property,
213            Bound::Excluded(threshold),
214            Bound::Unbounded,
215        )
216    }
217
218    /// Range query: value >= threshold
219    #[must_use]
220    pub fn range_greater_or_equal(
221        &self,
222        label: &str,
223        property: &str,
224        threshold: &serde_json::Value,
225    ) -> RoaringBitmap {
226        self.range_query(
227            label,
228            property,
229            Bound::Included(threshold),
230            Bound::Unbounded,
231        )
232    }
233
234    /// Range query: value < threshold
235    #[must_use]
236    pub fn range_less_than(
237        &self,
238        label: &str,
239        property: &str,
240        threshold: &serde_json::Value,
241    ) -> RoaringBitmap {
242        self.range_query(
243            label,
244            property,
245            Bound::Unbounded,
246            Bound::Excluded(threshold),
247        )
248    }
249
250    /// Range query: value <= threshold
251    #[must_use]
252    pub fn range_less_or_equal(
253        &self,
254        label: &str,
255        property: &str,
256        threshold: &serde_json::Value,
257    ) -> RoaringBitmap {
258        self.range_query(
259            label,
260            property,
261            Bound::Unbounded,
262            Bound::Included(threshold),
263        )
264    }
265
266    /// Range query: start <= value <= end (BETWEEN)
267    #[must_use]
268    pub fn range_between(
269        &self,
270        label: &str,
271        property: &str,
272        start: &serde_json::Value,
273        end: &serde_json::Value,
274    ) -> RoaringBitmap {
275        self.range_query(
276            label,
277            property,
278            Bound::Included(start),
279            Bound::Included(end),
280        )
281    }
282
283    /// Converts a `Bound<&serde_json::Value>` to `Bound<OrderedValue>`.
284    ///
285    /// Returns `None` when the JSON value is not convertible to an ordered type.
286    fn convert_bound(bound: Bound<&serde_json::Value>) -> Option<Bound<OrderedValue>> {
287        match bound {
288            Bound::Included(v) => OrderedValue::from_json(v).map(Bound::Included),
289            Bound::Excluded(v) => OrderedValue::from_json(v).map(Bound::Excluded),
290            Bound::Unbounded => Some(Bound::Unbounded),
291        }
292    }
293
294    /// Internal range query using `BTreeMap::range()`.
295    fn range_query(
296        &self,
297        label: &str,
298        property: &str,
299        start: Bound<&serde_json::Value>,
300        end: Bound<&serde_json::Value>,
301    ) -> RoaringBitmap {
302        let mut result = RoaringBitmap::new();
303
304        let key = make_label_prop_key(label, property);
305        let Some(btree) = self.indexes.get(&key) else {
306            return result;
307        };
308
309        let (Some(start_ord), Some(end_ord)) =
310            (Self::convert_bound(start), Self::convert_bound(end))
311        else {
312            return result;
313        };
314
315        // Use BTreeMap::range() for O(log n) access
316        for bitmap in btree.range((start_ord, end_ord)).map(|(_k, v)| v) {
317            result |= bitmap;
318        }
319
320        result
321    }
322
323    /// Drop a range index.
324    pub fn drop_index(&mut self, label: &str, property: &str) -> bool {
325        self.indexes
326            .remove(&make_label_prop_key(label, property))
327            .is_some()
328    }
329
330    /// Clear all range indexes.
331    pub fn clear(&mut self) {
332        self.indexes.clear();
333    }
334
335    /// Get memory usage estimate.
336    #[must_use]
337    pub fn memory_usage(&self) -> usize {
338        let mut total = std::mem::size_of::<Self>();
339        for ((label, prop), btree) in &self.indexes {
340            total += label.len() + prop.len();
341            for bitmap in btree.values() {
342                total += std::mem::size_of::<OrderedValue>();
343                total += bitmap.serialized_size();
344            }
345        }
346        total
347    }
348
349    /// Get all indexed (label, property) pairs.
350    #[must_use]
351    pub fn indexed_properties(&self) -> Vec<(String, String)> {
352        self.indexes.keys().cloned().collect()
353    }
354}
355
356impl PostcardPersistence for RangeIndex {}
357
358// Inherent persistence methods that delegate to `PostcardPersistence`.
359// Required so callers can use `RangeIndex::load_from_file` without
360// importing the trait.
361impl RangeIndex {
362    /// Serialize the index to bytes using postcard.
363    ///
364    /// # Errors
365    /// Returns an error if serialization fails.
366    pub fn to_bytes(&self) -> Result<Vec<u8>, postcard::Error> {
367        <Self as PostcardPersistence>::to_bytes(self)
368    }
369
370    /// Deserialize an index from bytes.
371    ///
372    /// # Errors
373    /// Returns an error if deserialization fails (corrupted data).
374    pub fn from_bytes(bytes: &[u8]) -> Result<Self, postcard::Error> {
375        <Self as PostcardPersistence>::from_bytes(bytes)
376    }
377
378    /// Save the index to a file.
379    ///
380    /// # Errors
381    /// Returns an error if serialization or file I/O fails.
382    pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
383        <Self as PostcardPersistence>::save_to_file(self, path)
384    }
385
386    /// Load an index from a file.
387    ///
388    /// # Errors
389    /// Returns an error if file I/O or deserialization fails.
390    pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
391        <Self as PostcardPersistence>::load_from_file(path)
392    }
393}