1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! Declarative index configuration (`IndexSpec`) plus the hashable subset of
//! `PropValue` (`IndexValue`) used as the equality-index key. `IndexSpec` is
//! supplied at `Db::open_with` time and carried in `Storage`; `storage.rs`'s
//! `apply_batch`/`rebuild_state_from_ops` are what read it to maintain the
//! on-disk PROP_INDEX table (see `prop_index.rs`).
use crate::error::TopoError;
use crate::props::PropValue;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::collections::HashSet;
/// A single declared `(label, prop)` pair to index.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PropIndex {
pub label: SmolStr,
pub prop: String,
}
/// Declares which `(label, prop)` pairs get indexed, and how.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexSpec {
/// Hash-indexed for equality lookup: `Db::nodes_by_prop`.
pub equality: Vec<PropIndex>,
/// Tokenized + BM25-indexed: `search_text` (wired in Task 6).
pub text: Vec<PropIndex>,
}
impl IndexSpec {
/// Rejects duplicate `(label, prop)` declarations within `equality` or
/// within `text` (checked independently — declaring the same key in both
/// lists is a valid "index this both ways" request, not a duplicate).
///
/// Deliberately does *not* try to reject an equality declaration whose
/// values will turn out to be `Float`: `IndexSpec` carries no value-type
/// info, so that can't be known at open time without scanning existing
/// data. Instead, Float is rejected per-VALUE: `IndexValue::of` returns
/// `None` for it (so such values are simply never entered into the
/// index), and `Db::nodes_by_prop` rejects a Float query value outright.
pub(crate) fn validate(&self) -> Result<(), TopoError> {
validate_unique(&self.equality)?;
validate_unique(&self.text)?;
Ok(())
}
}
fn validate_unique(entries: &[PropIndex]) -> Result<(), TopoError> {
let mut seen: HashSet<(&SmolStr, &str)> = HashSet::new();
for p in entries {
if !seen.insert((&p.label, p.prop.as_str())) {
return Err(TopoError::Rejected(format!(
"duplicate index declaration for ({}, {})",
p.label, p.prop
)));
}
}
Ok(())
}
/// Hashable subset of `PropValue`, used as the equality-index key. `Float` is
/// deliberately absent — floats are not equality-indexable (see
/// `nodes_by_float_range` for the intended access pattern instead).
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) enum IndexValue {
Str(String),
Int(i64),
Bool(bool),
Bytes(Vec<u8>),
DateTime(i64),
}
impl IndexValue {
/// `None` for `PropValue::Float` — every other variant converts 1:1.
pub(crate) fn of(v: &PropValue) -> Option<IndexValue> {
match v {
PropValue::Str(s) => Some(IndexValue::Str(s.clone())),
PropValue::Int(i) => Some(IndexValue::Int(*i)),
PropValue::Bool(b) => Some(IndexValue::Bool(*b)),
PropValue::Bytes(b) => Some(IndexValue::Bytes(b.clone())),
PropValue::DateTime(dt) => Some(IndexValue::DateTime(*dt)),
PropValue::Float(_) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_rejects_duplicates_within_a_list_but_not_across_lists() {
let dup_within = IndexSpec {
equality: vec![
PropIndex {
label: "M".into(),
prop: "x".into(),
},
PropIndex {
label: "M".into(),
prop: "x".into(),
},
],
text: vec![],
};
assert!(matches!(dup_within.validate(), Err(TopoError::Rejected(_))));
let same_key_both_lists = IndexSpec {
equality: vec![PropIndex {
label: "M".into(),
prop: "x".into(),
}],
text: vec![PropIndex {
label: "M".into(),
prop: "x".into(),
}],
};
assert!(same_key_both_lists.validate().is_ok());
}
#[test]
fn index_value_of_is_none_for_float_and_some_for_everything_else() {
assert_eq!(IndexValue::of(&PropValue::Float(1.0)), None);
assert_eq!(
IndexValue::of(&PropValue::Str("a".into())),
Some(IndexValue::Str("a".into()))
);
assert_eq!(IndexValue::of(&PropValue::Int(1)), Some(IndexValue::Int(1)));
assert_eq!(
IndexValue::of(&PropValue::Bool(true)),
Some(IndexValue::Bool(true))
);
assert_eq!(
IndexValue::of(&PropValue::Bytes(vec![1, 2])),
Some(IndexValue::Bytes(vec![1, 2]))
);
assert_eq!(
IndexValue::of(&PropValue::DateTime(5)),
Some(IndexValue::DateTime(5))
);
}
}