use nodedb_query::SqlExpr;
use nodedb_types::value::Value;
use crate::error::LiteError;
use super::key;
#[derive(Debug, Clone, Copy, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
#[msgpack(c_enum)]
pub enum IndexEngine {
Document = 0,
Strict = 1,
KeyValue = 2,
}
#[derive(Debug, Clone, zerompk::ToMessagePack, zerompk::FromMessagePack)]
pub struct IndexPredicate {
pub sql: String,
pub expr: SqlExpr,
}
impl IndexPredicate {
pub fn parse(sql: &str) -> Result<Self, LiteError> {
let (expr, _deps) = nodedb_query::expr_parse::parse_generated_expr(sql).map_err(|e| {
LiteError::BadRequest {
detail: format!(
"CREATE INDEX: partial-index predicate '{sql}' does not parse: {e}"
),
}
})?;
Ok(Self {
sql: sql.to_string(),
expr,
})
}
pub fn matches(&self, row: &Value) -> bool {
matches!(self.expr.eval(row), Ok(Value::Bool(true)))
}
}
#[derive(Debug, Clone, zerompk::ToMessagePack, zerompk::FromMessagePack)]
pub struct IndexDef {
pub name: String,
pub collection: String,
pub path: String,
pub unique: bool,
pub case_insensitive: bool,
pub is_array: bool,
pub predicate: Option<IndexPredicate>,
pub engine: IndexEngine,
}
impl IndexDef {
pub fn field_spec(&self) -> String {
field_spec(&self.path, self.is_array)
}
pub(crate) fn entry_prefix(&self) -> Vec<u8> {
key::index_prefix(&self.collection, &self.name)
}
pub(crate) fn planner_spec(&self) -> nodedb_sql::types::IndexSpec {
nodedb_sql::types::IndexSpec {
name: self.name.clone(),
field: self.field_spec(),
unique: self.unique,
case_insensitive: self.case_insensitive,
state: nodedb_sql::types::IndexState::Ready,
predicate: self.predicate.as_ref().map(|p| p.sql.clone()),
}
}
pub(crate) fn encode(&self) -> Result<Vec<u8>, LiteError> {
zerompk::to_msgpack_vec(self).map_err(|e| LiteError::Serialization {
detail: format!("encode index definition '{}': {e}", self.name),
})
}
pub(crate) fn decode(bytes: &[u8]) -> Result<Self, LiteError> {
zerompk::from_msgpack(bytes).map_err(|e| LiteError::Corrupted {
detail: format!("stored index definition does not decode: {e}"),
})
}
}
pub fn canonical_field(field: &str) -> (String, bool) {
let (field, is_array) = match field.strip_suffix("[]") {
Some(inner) => (inner, true),
None => (field, false),
};
let path = if field.starts_with('$') {
field.to_string()
} else {
format!("$.{field}")
};
(path, is_array)
}
pub fn field_spec(path: &str, is_array: bool) -> String {
if is_array {
format!("{path}[]")
} else {
path.to_string()
}
}
pub fn default_index_name(collection: &str, field: &str) -> String {
format!("idx_{collection}_{field}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fields_canonicalize_to_json_paths() {
assert_eq!(canonical_field("email"), ("$.email".into(), false));
assert_eq!(canonical_field("$.a.b"), ("$.a.b".into(), false));
assert_eq!(canonical_field("tags[]"), ("$.tags".into(), true));
}
#[test]
fn a_definition_round_trips_with_its_predicate() {
let def = IndexDef {
name: "idx".into(),
collection: "users".into(),
path: "$.email".into(),
unique: true,
case_insensitive: true,
is_array: false,
predicate: Some(IndexPredicate::parse("active = true").expect("parse")),
engine: IndexEngine::Document,
};
let decoded = IndexDef::decode(&def.encode().expect("encode")).expect("decode");
assert_eq!(decoded.name, "idx");
assert!(decoded.unique && decoded.case_insensitive);
assert_eq!(decoded.engine, IndexEngine::Document);
let predicate = decoded.predicate.expect("predicate");
assert_eq!(predicate.sql, "active = true");
let mut row = std::collections::HashMap::new();
row.insert("active".to_string(), Value::Bool(true));
assert!(predicate.matches(&Value::Object(row)));
}
}