Skip to main content

mongreldb_core/index/
learned_range.rs

1//! Per-column learned (PGM) range index — serves `Condition::Range` and
2//! `Condition::RangeF64` sub-linearly for numeric columns declared
3//! `IndexKind::LearnedRange`.
4//!
5//! The run is sorted by `RowId`, not by column value, so at flush we collect
6//! `(value, row_id)`, sort by value, and build a PGM over `value → position`
7//! (parallel to the sorted `row_id` array). A range query uses the PGM to land
8//! in an ε-window, then a local binary search finds the exact `[lo,hi]` slice —
9//! `O(log segments + log ε)` instead of a full column scan.
10//!
11//! Both `i64` and `f64` columns are supported via order-preserving key encodings.
12
13use super::pgm::{LearnedIndex, PgmIndex};
14use std::collections::HashSet;
15
16/// Order-preserving encoding of `i64` into `u64` (flip the sign bit), so PGM
17/// key order matches numeric order including negatives: `MIN→0`, `-1→2⁶³-1`,
18/// `0→2⁶³`, `MAX→u64::MAX`.
19#[inline]
20fn i64_key(v: i64) -> u64 {
21    (v as u64) ^ (1u64 << 63)
22}
23
24/// Order-preserving encoding of `f64` into `u64`: positive floats map to
25/// `2⁶³..u64::MAX` (sign bit flipped), negative floats map to `0..2⁶³-1`
26/// (all bits flipped) so the total order matches IEEE-754 totalOrder.
27#[inline]
28fn f64_key(v: f64) -> u64 {
29    let bits = v.to_bits();
30    if bits & (1u64 << 63) != 0 {
31        !bits
32    } else {
33        bits ^ (1u64 << 63)
34    }
35}
36
37#[derive(Debug, Clone)]
38pub struct ColumnLearnedRange {
39    keys: Vec<u64>,    // order-preserving value keys, ascending
40    row_ids: Vec<u64>, // parallel row ids (sorted by value)
41    pgm: PgmIndex,
42}
43
44impl ColumnLearnedRange {
45    /// Build from `(value, row_id)` pairs in any order.
46    pub fn build_i64(pairs: &[(i64, u64)]) -> Self {
47        let mut sorted: Vec<(u64, u64)> = pairs.iter().map(|(v, r)| (i64_key(*v), *r)).collect();
48        sorted.sort_unstable_by_key(|(k, _)| *k);
49        let keys: Vec<u64> = sorted.iter().map(|(k, _)| *k).collect();
50        let row_ids: Vec<u64> = sorted.iter().map(|(_, r)| *r).collect();
51        let points: Vec<(u64, usize)> = keys.iter().enumerate().map(|(i, k)| (*k, i)).collect();
52        let pgm = if points.is_empty() {
53            PgmIndex::build(&[], 16)
54        } else {
55            PgmIndex::build(&points, 16)
56        };
57        Self { keys, row_ids, pgm }
58    }
59
60    /// First position whose key `>= key`. Seeded by the PGM ε-window, then
61    /// gallops outward — the window alone only brackets duplicate runs up to
62    /// 2·ε, so longer runs need expansion (rare; keeps lookups sub-linear).
63    fn lower_bound(&self, key: u64) -> usize {
64        let n = self.keys.len();
65        if n == 0 {
66            return 0;
67        }
68        let (lo, hi) = self.pgm.predict(key);
69        let lo = lo.min(n);
70        let hi = hi.min(n).max(lo);
71        let mut idx = lo + self.keys[lo..hi].partition_point(|k| *k < key);
72        // Gallop left if the window began inside a run of keys >= search key.
73        if idx > 0 && self.keys[idx - 1] >= key {
74            let mut step = 1usize;
75            while idx >= step && self.keys[idx - step] >= key {
76                step <<= 1;
77            }
78            let start = idx - step.min(idx);
79            idx = start + self.keys[start..idx].partition_point(|k| *k < key);
80        }
81        // Gallop right if the window ended before the true lower bound.
82        if idx < n && self.keys[idx] < key {
83            let mut step = 1usize;
84            while idx + step <= n && self.keys[(idx + step).min(n) - 1] < key {
85                step <<= 1;
86            }
87            let end = (idx + step).min(n);
88            idx = idx + self.keys[idx..end].partition_point(|k| *k < key);
89        }
90        idx
91    }
92
93    /// First position whose key `> key` (galloping, same rationale).
94    fn upper_bound(&self, key: u64) -> usize {
95        let n = self.keys.len();
96        if n == 0 {
97            return 0;
98        }
99        let (lo, hi) = self.pgm.predict(key);
100        let lo = lo.min(n);
101        let hi = hi.min(n).max(lo);
102        let mut idx = lo + self.keys[lo..hi].partition_point(|k| *k <= key);
103        if idx > 0 && self.keys[idx - 1] > key {
104            let mut step = 1usize;
105            while idx >= step && self.keys[idx - step] > key {
106                step <<= 1;
107            }
108            let start = idx - step.min(idx);
109            idx = start + self.keys[start..idx].partition_point(|k| *k <= key);
110        }
111        if idx < n && self.keys[idx] <= key {
112            let mut step = 1usize;
113            while idx + step <= n && self.keys[(idx + step).min(n) - 1] <= key {
114                step <<= 1;
115            }
116            let end = (idx + step).min(n);
117            idx = idx + self.keys[idx..end].partition_point(|k| *k <= key);
118        }
119        idx
120    }
121
122    /// Row ids whose value is in `[lo, hi]` (inclusive).
123    pub fn range(&self, lo: i64, hi: i64) -> HashSet<u64> {
124        if hi < lo || self.keys.is_empty() {
125            return HashSet::new();
126        }
127        let start = self.lower_bound(i64_key(lo));
128        let end = self.upper_bound(i64_key(hi));
129        self.row_ids[start..end].iter().copied().collect()
130    }
131
132    /// Build from `(f64_value, row_id)` pairs (Phase 13.3).
133    pub fn build_f64(pairs: &[(f64, u64)]) -> Self {
134        let mut sorted: Vec<(u64, u64)> = pairs.iter().map(|(v, r)| (f64_key(*v), *r)).collect();
135        sorted.sort_unstable_by_key(|(k, _)| *k);
136        let keys: Vec<u64> = sorted.iter().map(|(k, _)| *k).collect();
137        let row_ids: Vec<u64> = sorted.iter().map(|(_, r)| *r).collect();
138        let points: Vec<(u64, usize)> = keys.iter().enumerate().map(|(i, k)| (*k, i)).collect();
139        let pgm = if points.is_empty() {
140            PgmIndex::build(&[], 16)
141        } else {
142            PgmIndex::build(&points, 16)
143        };
144        Self { keys, row_ids, pgm }
145    }
146
147    /// Row ids whose f64 value is in `[lo, hi]` with per-bound inclusivity
148    /// (Phase 13.3).
149    pub fn range_f64(
150        &self,
151        lo: f64,
152        lo_inclusive: bool,
153        hi: f64,
154        hi_inclusive: bool,
155    ) -> HashSet<u64> {
156        if self.keys.is_empty() {
157            return HashSet::new();
158        }
159        // Convert to key space. For inclusive bounds, use the value's key
160        // directly (it maps to the exact position). For exclusive bounds,
161        // nudge inward by ±1 in key space (which is the next representable
162        // value in the order-preserving encoding).
163        let lo_key = f64_key(lo);
164        let hi_key = f64_key(hi);
165        let (start, end) = if hi < lo {
166            return HashSet::new();
167        } else if lo_inclusive && hi_inclusive {
168            (self.lower_bound(lo_key), self.upper_bound(hi_key))
169        } else if lo_inclusive {
170            // hi exclusive
171            (self.lower_bound(lo_key), self.lower_bound(hi_key))
172        } else if hi_inclusive {
173            // lo exclusive
174            (self.upper_bound(lo_key), self.upper_bound(hi_key))
175        } else {
176            (self.upper_bound(lo_key), self.lower_bound(hi_key))
177        };
178        self.row_ids[start..end].iter().copied().collect()
179    }
180
181    /// Snapshot `(value_key, row_id, pgm segments/epsilon)` for checkpointing.
182    pub fn snapshot(&self) -> ColumnLearnedRangeSnapshot {
183        ColumnLearnedRangeSnapshot {
184            keys: self.keys.clone(),
185            row_ids: self.row_ids.clone(),
186            pgm: self.pgm.clone(),
187        }
188    }
189
190    /// Rebuild from a snapshot produced by [`ColumnLearnedRange::snapshot`].
191    pub fn from_snapshot(snap: ColumnLearnedRangeSnapshot) -> Self {
192        Self {
193            keys: snap.keys,
194            row_ids: snap.row_ids,
195            pgm: snap.pgm,
196        }
197    }
198}
199
200/// Serializable snapshot of a [`ColumnLearnedRange`] (PGM is already serde).
201#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
202pub struct ColumnLearnedRangeSnapshot {
203    pub keys: Vec<u64>,
204    pub row_ids: Vec<u64>,
205    pub pgm: PgmIndex,
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn range_returns_exact_slice() {
214        // values out of row_id order; duplicates present.
215        let pairs = vec![
216            (100i64, 0u64),
217            (-5, 1),
218            (50, 2),
219            (100, 3),
220            (1000, 4),
221            (-5, 5),
222        ];
223        let idx = ColumnLearnedRange::build_i64(&pairs);
224        // sorted by value: -5(r1,r5), 50(r2), 100(r0,r3), 1000(r4)
225        let r = idx.range(50, 100);
226        assert_eq!(r, [2, 0, 3].into_iter().collect::<HashSet<_>>());
227        let all = idx.range(i64::MIN, i64::MAX);
228        assert_eq!(all.len(), 6);
229        let none = idx.range(200, 300);
230        assert!(none.is_empty());
231        let negs = idx.range(i64::MIN, -5);
232        assert_eq!(negs, [1, 5].into_iter().collect::<HashSet<_>>());
233    }
234}