nodedb_lite/index/
catalog.rs1use nodedb_query::SqlExpr;
7use nodedb_types::value::Value;
8
9use crate::error::LiteError;
10
11use super::key;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
17#[msgpack(c_enum)]
18pub enum IndexEngine {
19 Document = 0,
21 Strict = 1,
23 KeyValue = 2,
25}
26
27#[derive(Debug, Clone, zerompk::ToMessagePack, zerompk::FromMessagePack)]
33pub struct IndexPredicate {
34 pub sql: String,
37 pub expr: SqlExpr,
39}
40
41impl IndexPredicate {
42 pub fn parse(sql: &str) -> Result<Self, LiteError> {
44 let (expr, _deps) = nodedb_query::expr_parse::parse_generated_expr(sql).map_err(|e| {
45 LiteError::BadRequest {
46 detail: format!(
47 "CREATE INDEX: partial-index predicate '{sql}' does not parse: {e}"
48 ),
49 }
50 })?;
51 Ok(Self {
52 sql: sql.to_string(),
53 expr,
54 })
55 }
56
57 pub fn matches(&self, row: &Value) -> bool {
59 matches!(self.expr.eval(row), Ok(Value::Bool(true)))
60 }
61}
62
63#[derive(Debug, Clone, zerompk::ToMessagePack, zerompk::FromMessagePack)]
65pub struct IndexDef {
66 pub name: String,
68 pub collection: String,
70 pub path: String,
72 pub unique: bool,
74 pub case_insensitive: bool,
76 pub is_array: bool,
78 pub predicate: Option<IndexPredicate>,
80 pub engine: IndexEngine,
82}
83
84impl IndexDef {
85 pub fn field_spec(&self) -> String {
88 field_spec(&self.path, self.is_array)
89 }
90
91 pub(crate) fn entry_prefix(&self) -> Vec<u8> {
93 key::index_prefix(&self.collection, &self.name)
94 }
95
96 pub(crate) fn planner_spec(&self) -> nodedb_sql::types::IndexSpec {
99 nodedb_sql::types::IndexSpec {
100 name: self.name.clone(),
101 field: self.field_spec(),
102 unique: self.unique,
103 case_insensitive: self.case_insensitive,
104 state: nodedb_sql::types::IndexState::Ready,
105 predicate: self.predicate.as_ref().map(|p| p.sql.clone()),
106 }
107 }
108
109 pub(crate) fn encode(&self) -> Result<Vec<u8>, LiteError> {
111 zerompk::to_msgpack_vec(self).map_err(|e| LiteError::Serialization {
112 detail: format!("encode index definition '{}': {e}", self.name),
113 })
114 }
115
116 pub(crate) fn decode(bytes: &[u8]) -> Result<Self, LiteError> {
118 zerompk::from_msgpack(bytes).map_err(|e| LiteError::Corrupted {
119 detail: format!("stored index definition does not decode: {e}"),
120 })
121 }
122}
123
124pub fn canonical_field(field: &str) -> (String, bool) {
129 let (field, is_array) = match field.strip_suffix("[]") {
130 Some(inner) => (inner, true),
131 None => (field, false),
132 };
133 let path = if field.starts_with('$') {
134 field.to_string()
135 } else {
136 format!("$.{field}")
137 };
138 (path, is_array)
139}
140
141pub fn field_spec(path: &str, is_array: bool) -> String {
144 if is_array {
145 format!("{path}[]")
146 } else {
147 path.to_string()
148 }
149}
150
151pub fn default_index_name(collection: &str, field: &str) -> String {
153 format!("idx_{collection}_{field}")
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn fields_canonicalize_to_json_paths() {
162 assert_eq!(canonical_field("email"), ("$.email".into(), false));
163 assert_eq!(canonical_field("$.a.b"), ("$.a.b".into(), false));
164 assert_eq!(canonical_field("tags[]"), ("$.tags".into(), true));
165 }
166
167 #[test]
168 fn a_definition_round_trips_with_its_predicate() {
169 let def = IndexDef {
170 name: "idx".into(),
171 collection: "users".into(),
172 path: "$.email".into(),
173 unique: true,
174 case_insensitive: true,
175 is_array: false,
176 predicate: Some(IndexPredicate::parse("active = true").expect("parse")),
177 engine: IndexEngine::Document,
178 };
179 let decoded = IndexDef::decode(&def.encode().expect("encode")).expect("decode");
180 assert_eq!(decoded.name, "idx");
181 assert!(decoded.unique && decoded.case_insensitive);
182 assert_eq!(decoded.engine, IndexEngine::Document);
183 let predicate = decoded.predicate.expect("predicate");
184 assert_eq!(predicate.sql, "active = true");
185 let mut row = std::collections::HashMap::new();
186 row.insert("active".to_string(), Value::Bool(true));
187 assert!(predicate.matches(&Value::Object(row)));
188 }
189}