Skip to main content

nodedb_query/
metadata_filter.rs

1//! Bridge between `MetadataFilter` (nodedb-types) and document evaluation.
2//!
3//! Converts the typed `MetadataFilter` enum into runtime evaluation against
4//! JSON documents. Used by both Origin (vector search pre-filter) and Lite
5//! (vector search post-filter against CRDT state).
6
7use nodedb_types::filter::MetadataFilter;
8
9use crate::json_ops::{coerced_eq, compare_json_optional};
10
11/// Evaluate a `MetadataFilter` against a JSON document.
12///
13/// Returns `true` if the document matches the filter.
14pub fn matches_metadata_filter(doc: &serde_json::Value, filter: &MetadataFilter) -> bool {
15    match filter {
16        MetadataFilter::Eq { field, value } => {
17            let field_val = doc.get(field.as_str());
18            let filter_val = value_to_json(value);
19            match field_val {
20                Some(fv) => coerced_eq(fv, &filter_val),
21                None => filter_val.is_null(),
22            }
23        }
24        MetadataFilter::Ne { field, value } => {
25            let field_val = doc.get(field.as_str());
26            let filter_val = value_to_json(value);
27            match field_val {
28                Some(fv) => !coerced_eq(fv, &filter_val),
29                None => !filter_val.is_null(),
30            }
31        }
32        MetadataFilter::Gt { field, value } => {
33            let field_val = doc.get(field.as_str());
34            let filter_val = value_to_json(value);
35            compare_json_optional(field_val, Some(&filter_val)) == std::cmp::Ordering::Greater
36        }
37        MetadataFilter::Gte { field, value } => {
38            let field_val = doc.get(field.as_str());
39            let filter_val = value_to_json(value);
40            let cmp = compare_json_optional(field_val, Some(&filter_val));
41            cmp == std::cmp::Ordering::Greater || cmp == std::cmp::Ordering::Equal
42        }
43        MetadataFilter::Lt { field, value } => {
44            let field_val = doc.get(field.as_str());
45            let filter_val = value_to_json(value);
46            compare_json_optional(field_val, Some(&filter_val)) == std::cmp::Ordering::Less
47        }
48        MetadataFilter::Lte { field, value } => {
49            let field_val = doc.get(field.as_str());
50            let filter_val = value_to_json(value);
51            let cmp = compare_json_optional(field_val, Some(&filter_val));
52            cmp == std::cmp::Ordering::Less || cmp == std::cmp::Ordering::Equal
53        }
54        MetadataFilter::In { field, values } => {
55            let field_val = match doc.get(field.as_str()) {
56                Some(v) => v,
57                None => return false,
58            };
59            values
60                .iter()
61                .any(|v| coerced_eq(field_val, &value_to_json(v)))
62        }
63        MetadataFilter::NotIn { field, values } => {
64            let field_val = match doc.get(field.as_str()) {
65                Some(v) => v,
66                None => return true,
67            };
68            !values
69                .iter()
70                .any(|v| coerced_eq(field_val, &value_to_json(v)))
71        }
72        MetadataFilter::And(filters) => filters.iter().all(|f| matches_metadata_filter(doc, f)),
73        MetadataFilter::Or(filters) => filters.iter().any(|f| matches_metadata_filter(doc, f)),
74        MetadataFilter::Not(inner) => !matches_metadata_filter(doc, inner),
75    }
76}
77
78/// Convert a `nodedb_types::Value` to `serde_json::Value` for comparison.
79fn value_to_json(value: &nodedb_types::value::Value) -> serde_json::Value {
80    match value {
81        nodedb_types::value::Value::Null => serde_json::Value::Null,
82        nodedb_types::value::Value::Bool(b) => serde_json::Value::Bool(*b),
83        nodedb_types::value::Value::Integer(i) => serde_json::json!(i),
84        nodedb_types::value::Value::Float(f) => serde_json::Number::from_f64(*f)
85            .map(serde_json::Value::Number)
86            .unwrap_or(serde_json::Value::Null),
87        nodedb_types::value::Value::String(s) => serde_json::Value::String(s.clone()),
88        _ => serde_json::to_value(value).unwrap_or(serde_json::Value::Null),
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use nodedb_types::filter::MetadataFilter;
96    use nodedb_types::value::Value;
97    use serde_json::json;
98
99    #[test]
100    fn eq_match() {
101        let doc = json!({"status": "active", "age": 25});
102        let filter = MetadataFilter::eq("status", "active");
103        assert!(matches_metadata_filter(&doc, &filter));
104    }
105
106    #[test]
107    fn eq_no_match() {
108        let doc = json!({"status": "inactive"});
109        let filter = MetadataFilter::eq("status", "active");
110        assert!(!matches_metadata_filter(&doc, &filter));
111    }
112
113    #[test]
114    fn gt_numeric() {
115        let doc = json!({"age": 30});
116        let filter = MetadataFilter::Gt {
117            field: "age".into(),
118            value: Value::Integer(25),
119        };
120        assert!(matches_metadata_filter(&doc, &filter));
121    }
122
123    #[test]
124    fn and_filter() {
125        let doc = json!({"status": "active", "age": 30});
126        let filter = MetadataFilter::and(vec![
127            MetadataFilter::eq("status", "active"),
128            MetadataFilter::Gt {
129                field: "age".into(),
130                value: Value::Integer(25),
131            },
132        ]);
133        assert!(matches_metadata_filter(&doc, &filter));
134    }
135
136    #[test]
137    fn or_filter() {
138        let doc = json!({"status": "inactive", "age": 30});
139        let filter = MetadataFilter::or(vec![
140            MetadataFilter::eq("status", "active"),
141            MetadataFilter::Gt {
142                field: "age".into(),
143                value: Value::Integer(25),
144            },
145        ]);
146        assert!(matches_metadata_filter(&doc, &filter));
147    }
148
149    #[test]
150    fn not_filter() {
151        let doc = json!({"status": "active"});
152        let filter = MetadataFilter::Not(Box::new(MetadataFilter::eq("status", "inactive")));
153        assert!(matches_metadata_filter(&doc, &filter));
154    }
155
156    #[test]
157    fn in_filter() {
158        let doc = json!({"role": "admin"});
159        let filter = MetadataFilter::In {
160            field: "role".into(),
161            values: vec![Value::from("admin"), Value::from("superadmin")],
162        };
163        assert!(matches_metadata_filter(&doc, &filter));
164    }
165
166    #[test]
167    fn missing_field() {
168        let doc = json!({"name": "Alice"});
169        let filter = MetadataFilter::eq("status", "active");
170        assert!(!matches_metadata_filter(&doc, &filter));
171    }
172}