Skip to main content

nodedb_query/
metadata_filter.rs

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