1use std::sync::Arc;
67
68use dashmap::DashMap;
69use parking_lot::RwLock;
70
71use crate::key_buffer::ArenaKeyHandle;
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum IndexPolicy {
80 WriteOptimized,
83
84 Balanced,
88
89 ScanOptimized,
93
94 AppendOnly,
98}
99
100impl Default for IndexPolicy {
101 fn default() -> Self {
102 IndexPolicy::Balanced
103 }
104}
105
106impl IndexPolicy {
107 pub fn from_str(s: &str) -> Option<Self> {
109 match s.to_lowercase().as_str() {
110 "write_optimized" | "write-optimized" | "write" => Some(IndexPolicy::WriteOptimized),
111 "balanced" | "default" => Some(IndexPolicy::Balanced),
112 "scan_optimized" | "scan-optimized" | "scan" => Some(IndexPolicy::ScanOptimized),
113 "append_only" | "append-only" | "append" => Some(IndexPolicy::AppendOnly),
114 _ => None,
115 }
116 }
117
118 pub fn write_cost(&self) -> &'static str {
120 match self {
121 IndexPolicy::WriteOptimized => "O(1)",
122 IndexPolicy::Balanced => "O(1) amortized",
123 IndexPolicy::ScanOptimized => "O(log N)",
124 IndexPolicy::AppendOnly => "O(1)",
125 }
126 }
127
128 pub fn scan_cost(&self) -> &'static str {
130 match self {
131 IndexPolicy::WriteOptimized => "O(N)",
132 IndexPolicy::Balanced => "O(output + log K)",
133 IndexPolicy::ScanOptimized => "O(log N + K)",
134 IndexPolicy::AppendOnly => "O(N)",
135 }
136 }
137
138 pub fn has_ordered_index(&self) -> bool {
140 matches!(self, IndexPolicy::ScanOptimized)
141 }
142
143 pub fn supports_efficient_scans(&self) -> bool {
145 matches!(self, IndexPolicy::ScanOptimized | IndexPolicy::Balanced)
146 }
147}
148
149#[derive(Debug, Clone)]
155pub struct TableIndexConfig {
156 pub table_name: String,
158 pub policy: IndexPolicy,
160 pub max_sorted_runs: usize,
162 pub target_run_size: usize,
164 pub enable_bloom_filter: bool,
166}
167
168impl TableIndexConfig {
169 pub fn new(table_name: impl Into<String>, policy: IndexPolicy) -> Self {
171 Self {
172 table_name: table_name.into(),
173 policy,
174 max_sorted_runs: 4,
175 target_run_size: 16 * 1024 * 1024, enable_bloom_filter: true,
177 }
178 }
179
180 pub fn with_max_sorted_runs(mut self, max: usize) -> Self {
182 self.max_sorted_runs = max;
183 self
184 }
185
186 pub fn with_target_run_size(mut self, size: usize) -> Self {
188 self.target_run_size = size;
189 self
190 }
191
192 pub fn with_bloom_filter(mut self, enable: bool) -> Self {
194 self.enable_bloom_filter = enable;
195 self
196 }
197}
198
199pub struct TableIndexRegistry {
208 configs: DashMap<String, TableIndexConfig>,
210 default_policy: RwLock<IndexPolicy>,
212}
213
214impl TableIndexRegistry {
215 pub fn new() -> Self {
217 Self {
218 configs: DashMap::new(),
219 default_policy: RwLock::new(IndexPolicy::Balanced),
220 }
221 }
222
223 pub fn with_default_policy(policy: IndexPolicy) -> Self {
225 Self {
226 configs: DashMap::new(),
227 default_policy: RwLock::new(policy),
228 }
229 }
230
231 pub fn set_default_policy(&self, policy: IndexPolicy) {
233 *self.default_policy.write() = policy;
234 }
235
236 pub fn default_policy(&self) -> IndexPolicy {
238 *self.default_policy.read()
239 }
240
241 pub fn configure_table(&self, config: TableIndexConfig) {
243 self.configs.insert(config.table_name.clone(), config);
244 }
245
246 pub fn get_policy(&self, table_name: &str) -> IndexPolicy {
248 self.configs
249 .get(table_name)
250 .map(|c| c.policy)
251 .unwrap_or_else(|| *self.default_policy.read())
252 }
253
254 pub fn get_config(&self, table_name: &str) -> TableIndexConfig {
256 self.configs
257 .get(table_name)
258 .map(|c| c.clone())
259 .unwrap_or_else(|| {
260 TableIndexConfig::new(table_name, *self.default_policy.read())
261 })
262 }
263
264 pub fn has_explicit_config(&self, table_name: &str) -> bool {
266 self.configs.contains_key(table_name)
267 }
268
269 pub fn remove_config(&self, table_name: &str) -> Option<TableIndexConfig> {
271 self.configs.remove(table_name).map(|(_, c)| c)
272 }
273
274 pub fn configured_tables(&self) -> Vec<String> {
276 self.configs.iter().map(|e| e.key().clone()).collect()
277 }
278}
279
280impl Default for TableIndexRegistry {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286#[derive(Debug)]
301pub struct SortedRun<K, V> {
302 entries: Vec<(K, V)>,
304 min_key: Option<K>,
306 max_key: Option<K>,
308 size_bytes: usize,
310 #[allow(dead_code)]
312 created_at: std::time::Instant,
313 level: usize,
315}
316
317impl<K: Ord + Clone, V: Clone> SortedRun<K, V> {
318 pub fn from_unsorted(mut entries: Vec<(K, V)>, level: usize) -> Self {
320 entries.sort_by(|a, b| a.0.cmp(&b.0));
321 let size_bytes = std::mem::size_of_val(&entries);
322 let min_key = entries.first().map(|(k, _)| k.clone());
323 let max_key = entries.last().map(|(k, _)| k.clone());
324 Self {
325 entries,
326 min_key,
327 max_key,
328 size_bytes,
329 created_at: std::time::Instant::now(),
330 level,
331 }
332 }
333
334 pub fn from_sorted(entries: Vec<(K, V)>, level: usize) -> Self {
336 let size_bytes = std::mem::size_of_val(&entries);
337 let min_key = entries.first().map(|(k, _)| k.clone());
338 let max_key = entries.last().map(|(k, _)| k.clone());
339 Self {
340 entries,
341 min_key,
342 max_key,
343 size_bytes,
344 created_at: std::time::Instant::now(),
345 level,
346 }
347 }
348
349 pub fn len(&self) -> usize {
351 self.entries.len()
352 }
353
354 pub fn is_empty(&self) -> bool {
356 self.entries.is_empty()
357 }
358
359 pub fn size_bytes(&self) -> usize {
361 self.size_bytes
362 }
363
364 pub fn level(&self) -> usize {
366 self.level
367 }
368
369 pub fn get(&self, key: &K) -> Option<&V> {
371 self.entries
372 .binary_search_by(|(k, _)| k.cmp(key))
373 .ok()
374 .map(|idx| &self.entries[idx].1)
375 }
376
377 pub fn range_from<'a>(&'a self, start: &K) -> impl Iterator<Item = &'a (K, V)> {
379 let idx = self.entries
380 .binary_search_by(|(k, _)| k.cmp(start))
381 .unwrap_or_else(|i| i);
382 self.entries[idx..].iter()
383 }
384
385 pub fn range<'a>(&'a self, start: &K, end: &K) -> impl Iterator<Item = &'a (K, V)> {
387 let start_idx = self.entries
388 .binary_search_by(|(k, _)| k.cmp(start))
389 .unwrap_or_else(|i| i);
390 let end_idx = self.entries
391 .binary_search_by(|(k, _)| k.cmp(end))
392 .unwrap_or_else(|i| i);
393 self.entries[start_idx..end_idx].iter()
394 }
395
396 pub fn iter(&self) -> impl Iterator<Item = &(K, V)> {
398 self.entries.iter()
399 }
400
401 #[inline]
406 pub fn entries(&self) -> &[(K, V)] {
407 &self.entries
408 }
409
410 #[inline]
424 pub fn overlaps_prefix(&self, prefix: &K) -> bool {
425 match &self.max_key {
426 Some(max) if max < prefix => false, _ => true, }
429 }
430
431 #[inline]
436 pub fn overlaps_range(&self, start: &K, end: &K) -> bool {
437 match (&self.min_key, &self.max_key) {
439 (Some(min), _) if min >= end => false, (_, Some(max)) if max < start => false, _ => true, }
443 }
444
445 #[inline]
447 pub fn min_key(&self) -> Option<&K> {
448 self.min_key.as_ref()
449 }
450
451 #[inline]
453 pub fn max_key(&self) -> Option<&K> {
454 self.max_key.as_ref()
455 }
456}
457
458pub struct BalancedTableIndex<V: Clone + Send + Sync + Eq + 'static> {
469 memtable: DashMap<ArenaKeyHandle, V>,
471 sorted_runs: RwLock<Vec<Arc<SortedRun<ArenaKeyHandle, V>>>>,
473 config: TableIndexConfig,
475 memtable_size: std::sync::atomic::AtomicUsize,
477}
478
479impl<V: Clone + Send + Sync + Eq + 'static> BalancedTableIndex<V> {
480 pub fn new(config: TableIndexConfig) -> Self {
482 Self {
483 memtable: DashMap::new(),
484 sorted_runs: RwLock::new(Vec::new()),
485 config,
486 memtable_size: std::sync::atomic::AtomicUsize::new(0),
487 }
488 }
489
490 pub fn insert(&self, key: ArenaKeyHandle, value: V) {
492 let key_size = key.len();
493 let value_size = std::mem::size_of::<V>();
494
495 self.memtable.insert(key, value);
496 self.memtable_size.fetch_add(key_size + value_size, std::sync::atomic::Ordering::Relaxed);
497 }
498
499 pub fn get(&self, key: &ArenaKeyHandle) -> Option<V> {
504 if let Some(v) = self.memtable.get(key) {
506 return Some(v.clone());
507 }
508
509 let runs = self.sorted_runs.read();
512 for run in runs.iter().rev() {
513 if run.overlaps_prefix(key) {
515 if let Some(v) = run.get(key) {
516 return Some(v.clone());
517 }
518 }
519 }
520
521 None
522 }
523
524 pub fn scan_prefix(&self, prefix: &ArenaKeyHandle) -> Vec<(ArenaKeyHandle, V)> {
536 use std::collections::BinaryHeap;
537 use std::cmp::Reverse;
538
539 #[derive(Eq, PartialEq)]
540 struct PrefixHeapEntry<V: Clone> {
541 key: ArenaKeyHandle,
542 value: V,
543 source_idx: usize, }
545
546 impl<V: Clone + Eq + PartialEq> Ord for PrefixHeapEntry<V> {
547 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
548 match self.key.cmp(&other.key) {
549 std::cmp::Ordering::Equal => self.source_idx.cmp(&other.source_idx),
550 other => other,
551 }
552 }
553 }
554
555 impl<V: Clone + Eq + PartialEq> PartialOrd for PrefixHeapEntry<V> {
556 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
557 Some(self.cmp(other))
558 }
559 }
560
561 let mut heap: BinaryHeap<Reverse<PrefixHeapEntry<V>>> = BinaryHeap::new();
562
563 for entry in self.memtable.iter() {
565 let key = entry.key();
566 if key.as_bytes().starts_with(prefix.as_bytes()) {
567 heap.push(Reverse(PrefixHeapEntry {
568 key: key.clone(),
569 value: entry.value().clone(),
570 source_idx: 0,
571 }));
572 }
573 }
574
575 let runs = self.sorted_runs.read();
577 for (run_idx, run) in runs.iter().enumerate() {
578 if !run.overlaps_prefix(prefix) {
580 continue; }
582
583 for (key, value) in run.range_from(prefix) {
585 if !key.as_bytes().starts_with(prefix.as_bytes()) {
586 break; }
588 heap.push(Reverse(PrefixHeapEntry {
589 key: key.clone(),
590 value: value.clone(),
591 source_idx: run_idx + 1, }));
593 }
594 }
595
596 let mut result = Vec::with_capacity(heap.len());
598 let mut last_key: Option<ArenaKeyHandle> = None;
599
600 while let Some(Reverse(entry)) = heap.pop() {
601 let is_new_key = last_key.as_ref().map(|k| k != &entry.key).unwrap_or(true);
602 if is_new_key {
603 last_key = Some(entry.key.clone());
604 result.push((entry.key, entry.value));
605 }
606 }
607
608 result
609 }
610
611 pub fn needs_compaction(&self) -> bool {
613 let memtable_size = self.memtable_size.load(std::sync::atomic::Ordering::Relaxed);
614 let runs = self.sorted_runs.read();
615
616 memtable_size >= self.config.target_run_size
617 || runs.len() >= self.config.max_sorted_runs
618 }
619
620 pub fn compact_memtable(&self) {
622 let entries: Vec<_> = self.memtable.iter()
624 .map(|e| (e.key().clone(), e.value().clone()))
625 .collect();
626
627 if entries.is_empty() {
628 return;
629 }
630
631 self.memtable.clear();
633 self.memtable_size.store(0, std::sync::atomic::Ordering::Relaxed);
634
635 let run = Arc::new(SortedRun::from_unsorted(entries, 0));
637
638 let mut runs = self.sorted_runs.write();
639 runs.push(run);
640 }
641
642 pub fn merge_runs(&self, levels_to_merge: usize) {
644 let mut runs = self.sorted_runs.write();
645
646 if runs.len() < levels_to_merge {
647 return;
648 }
649
650 let to_merge: Vec<_> = runs.drain(..levels_to_merge).collect();
652
653 let merged = self.k_way_merge(&to_merge);
655
656 let new_run = Arc::new(SortedRun::from_sorted(merged, to_merge.len()));
658 runs.insert(0, new_run);
659 }
660
661 fn k_way_merge(&self, runs: &[Arc<SortedRun<ArenaKeyHandle, V>>]) -> Vec<(ArenaKeyHandle, V)> {
669 use std::collections::BinaryHeap;
670 use std::cmp::Reverse;
671
672 #[derive(Eq, PartialEq)]
673 struct HeapEntry<V: Clone> {
674 key: ArenaKeyHandle,
675 value: V,
676 run_idx: usize,
677 entry_idx: usize,
678 }
679
680 impl<V: Clone + Eq + PartialEq> Ord for HeapEntry<V> {
681 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
682 match self.key.cmp(&other.key) {
685 std::cmp::Ordering::Equal => self.run_idx.cmp(&other.run_idx),
686 other => other,
687 }
688 }
689 }
690
691 impl<V: Clone + Eq + PartialEq> PartialOrd for HeapEntry<V> {
692 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
693 Some(self.cmp(other))
694 }
695 }
696
697 let mut heap: BinaryHeap<Reverse<HeapEntry<V>>> = BinaryHeap::new();
698
699 let mut run_positions: Vec<usize> = vec![0; runs.len()];
701
702 for (run_idx, run) in runs.iter().enumerate() {
704 let entries = run.entries();
705 if !entries.is_empty() {
706 let (key, value) = &entries[0]; heap.push(Reverse(HeapEntry {
708 key: key.clone(),
709 value: value.clone(),
710 run_idx,
711 entry_idx: 0,
712 }));
713 }
714 }
715
716 let estimated_size: usize = runs.iter().map(|r| r.len()).sum();
718 let mut result = Vec::with_capacity(estimated_size);
719 let mut last_key: Option<ArenaKeyHandle> = None;
720
721 while let Some(Reverse(entry)) = heap.pop() {
722 let is_new_key = last_key.as_ref().map(|k| k != &entry.key).unwrap_or(true);
724 if is_new_key {
725 last_key = Some(entry.key.clone());
726 result.push((entry.key.clone(), entry.value));
727 }
728
729 run_positions[entry.run_idx] += 1;
731 let next_idx = run_positions[entry.run_idx];
732
733 let run_entries = runs[entry.run_idx].entries();
735 if next_idx < run_entries.len() {
736 let (key, value) = &run_entries[next_idx]; heap.push(Reverse(HeapEntry {
738 key: key.clone(),
739 value: value.clone(),
740 run_idx: entry.run_idx,
741 entry_idx: next_idx,
742 }));
743 }
744 }
745
746 result
747 }
748
749 pub fn config(&self) -> &TableIndexConfig {
751 &self.config
752 }
753
754 pub fn memtable_size(&self) -> usize {
756 self.memtable_size.load(std::sync::atomic::Ordering::Relaxed)
757 }
758
759 pub fn run_count(&self) -> usize {
761 self.sorted_runs.read().len()
762 }
763}
764
765#[cfg(test)]
770mod tests {
771 use super::*;
772
773 #[test]
774 fn test_index_policy_from_str() {
775 assert_eq!(IndexPolicy::from_str("write_optimized"), Some(IndexPolicy::WriteOptimized));
776 assert_eq!(IndexPolicy::from_str("balanced"), Some(IndexPolicy::Balanced));
777 assert_eq!(IndexPolicy::from_str("scan-optimized"), Some(IndexPolicy::ScanOptimized));
778 assert_eq!(IndexPolicy::from_str("append_only"), Some(IndexPolicy::AppendOnly));
779 assert_eq!(IndexPolicy::from_str("invalid"), None);
780 }
781
782 #[test]
783 fn test_registry_default_policy() {
784 let registry = TableIndexRegistry::new();
785
786 assert_eq!(registry.get_policy("unknown"), IndexPolicy::Balanced);
788
789 registry.configure_table(TableIndexConfig::new("users", IndexPolicy::WriteOptimized));
791 assert_eq!(registry.get_policy("users"), IndexPolicy::WriteOptimized);
792
793 assert_eq!(registry.get_policy("orders"), IndexPolicy::Balanced);
795 }
796
797 #[test]
798 fn test_registry_change_default() {
799 let registry = TableIndexRegistry::new();
800
801 registry.set_default_policy(IndexPolicy::ScanOptimized);
802 assert_eq!(registry.get_policy("any_table"), IndexPolicy::ScanOptimized);
803 }
804
805 #[test]
806 fn test_sorted_run() {
807 let entries = vec![
808 (ArenaKeyHandle::new(b"c"), 3),
809 (ArenaKeyHandle::new(b"a"), 1),
810 (ArenaKeyHandle::new(b"b"), 2),
811 ];
812
813 let run = SortedRun::from_unsorted(entries, 0);
814
815 assert_eq!(run.len(), 3);
816 assert_eq!(run.get(&ArenaKeyHandle::new(b"a")), Some(&1));
817 assert_eq!(run.get(&ArenaKeyHandle::new(b"b")), Some(&2));
818 assert_eq!(run.get(&ArenaKeyHandle::new(b"c")), Some(&3));
819 assert_eq!(run.get(&ArenaKeyHandle::new(b"d")), None);
820 }
821
822 #[test]
823 fn test_balanced_table_index() {
824 let config = TableIndexConfig::new("test", IndexPolicy::Balanced);
825 let index: BalancedTableIndex<i32> = BalancedTableIndex::new(config);
826
827 index.insert(ArenaKeyHandle::new(b"key1"), 1);
828 index.insert(ArenaKeyHandle::new(b"key2"), 2);
829
830 assert_eq!(index.get(&ArenaKeyHandle::new(b"key1")), Some(1));
831 assert_eq!(index.get(&ArenaKeyHandle::new(b"key2")), Some(2));
832 assert_eq!(index.get(&ArenaKeyHandle::new(b"key3")), None);
833 }
834
835 #[test]
836 fn test_balanced_compaction() {
837 let config = TableIndexConfig::new("test", IndexPolicy::Balanced)
838 .with_target_run_size(100); let index: BalancedTableIndex<i32> = BalancedTableIndex::new(config);
841
842 for i in 0..10 {
843 let key = format!("key{:03}", i);
844 index.insert(ArenaKeyHandle::new(key.as_bytes()), i as i32);
845 }
846
847 index.compact_memtable();
849
850 assert_eq!(index.run_count(), 1);
851 assert_eq!(index.memtable_size(), 0);
852
853 assert_eq!(index.get(&ArenaKeyHandle::new(b"key005")), Some(5));
855 }
856
857 #[test]
858 fn test_k_way_merge_scaling() {
859 use std::time::Instant;
862
863 let sizes = [100, 500, 1000];
864 let mut times_ns: Vec<u128> = Vec::new();
865
866 for size in sizes {
867 let runs: Vec<Arc<SortedRun<ArenaKeyHandle, i32>>> = (0..5)
869 .map(|run_id| {
870 let entries: Vec<(ArenaKeyHandle, i32)> = (0..size)
871 .map(|i| {
872 let key = format!("key_{:08}_{}", i * 5 + run_id, run_id);
873 (ArenaKeyHandle::new(key.as_bytes()), (i * 5 + run_id) as i32)
874 })
875 .collect();
876 Arc::new(SortedRun::from_sorted(entries, run_id))
877 })
878 .collect();
879
880 let config = TableIndexConfig::new("test", IndexPolicy::Balanced);
881 let index: BalancedTableIndex<i32> = BalancedTableIndex::new(config);
882
883 let start = Instant::now();
884 let merged = index.k_way_merge(&runs);
885 let elapsed = start.elapsed();
886
887 times_ns.push(elapsed.as_nanos());
888
889 let total_entries = size * 5;
891 assert_eq!(merged.len(), total_entries, "Merge should produce all unique entries");
892 }
893
894 if times_ns.len() >= 2 && times_ns[0] > 0 {
898 let ratio_1_to_2 = times_ns[1] as f64 / times_ns[0] as f64;
899 let ratio_2_to_3 = times_ns[2] as f64 / times_ns[1] as f64;
900
901 assert!(ratio_1_to_2 < 15.0,
904 "Merge scaling should be sub-quadratic: ratio={:.1}x for 5x size", ratio_1_to_2);
905 assert!(ratio_2_to_3 < 10.0,
906 "Merge scaling should be sub-quadratic: ratio={:.1}x for 2x size", ratio_2_to_3);
907 }
908 }
909
910 #[test]
911 fn test_sorted_run_metadata_pruning() {
912 let entries = vec![
914 (ArenaKeyHandle::new(b"apple"), 1),
915 (ArenaKeyHandle::new(b"banana"), 2),
916 (ArenaKeyHandle::new(b"cherry"), 3),
917 ];
918 let run = SortedRun::from_sorted(entries, 0);
919
920 assert_eq!(run.min_key().map(|k| k.as_bytes()), Some(b"apple".as_slice()));
922 assert_eq!(run.max_key().map(|k| k.as_bytes()), Some(b"cherry".as_slice()));
923
924 assert!(run.overlaps_prefix(&ArenaKeyHandle::new(b"banana"))); assert!(run.overlaps_prefix(&ArenaKeyHandle::new(b"apple"))); assert!(run.overlaps_prefix(&ArenaKeyHandle::new(b"cherry"))); assert!(!run.overlaps_prefix(&ArenaKeyHandle::new(b"date"))); assert!(!run.overlaps_prefix(&ArenaKeyHandle::new(b"zebra"))); assert!(run.overlaps_range(
935 &ArenaKeyHandle::new(b"banana"),
936 &ArenaKeyHandle::new(b"cherry")
937 ));
938 assert!(!run.overlaps_range(
939 &ArenaKeyHandle::new(b"date"),
940 &ArenaKeyHandle::new(b"fig")
941 )); assert!(!run.overlaps_range(
943 &ArenaKeyHandle::new(b"aaa"),
944 &ArenaKeyHandle::new(b"aab")
945 )); }
947
948 #[test]
949 fn test_scan_prefix() {
950 let config = TableIndexConfig::new("test", IndexPolicy::Balanced)
951 .with_target_run_size(50); let index: BalancedTableIndex<i32> = BalancedTableIndex::new(config);
953
954 let prefixes = ["user:1:", "user:2:", "order:1:", "order:2:"];
956 for (i, prefix) in prefixes.iter().enumerate() {
957 for j in 0..5 {
958 let key = format!("{}{}", prefix, j);
959 index.insert(ArenaKeyHandle::new(key.as_bytes()), (i * 10 + j) as i32);
960 }
961 }
962
963 index.compact_memtable();
965
966 index.insert(ArenaKeyHandle::new(b"user:1:99"), 199);
968 index.insert(ArenaKeyHandle::new(b"order:1:99"), 299);
969
970 let results = index.scan_prefix(&ArenaKeyHandle::new(b"user:1:"));
972 assert_eq!(results.len(), 6); for (key, _value) in &results {
976 assert!(key.as_bytes().starts_with(b"user:1:"),
977 "Key {:?} should start with user:1:", String::from_utf8_lossy(key.as_bytes()));
978 }
979
980 for window in results.windows(2) {
982 assert!(window[0].0 <= window[1].0, "Results should be sorted by key");
983 }
984
985 let results = index.scan_prefix(&ArenaKeyHandle::new(b"order:"));
987 assert_eq!(results.len(), 11); }
989
990 #[test]
991 fn test_empty_sorted_run_metadata() {
992 let entries: Vec<(ArenaKeyHandle, i32)> = vec![];
994 let run = SortedRun::from_sorted(entries, 0);
995
996 assert!(run.min_key().is_none());
997 assert!(run.max_key().is_none());
998 assert!(run.overlaps_prefix(&ArenaKeyHandle::new(b"anything"))); assert!(run.overlaps_range(
1000 &ArenaKeyHandle::new(b"a"),
1001 &ArenaKeyHandle::new(b"z")
1002 )); }
1004}