1use crate::canonical_key::{value_to_canonical_key, CanonicalKey};
16use crate::types::Value;
17use crate::value_compare::partial_compare_values;
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum MetadataValue {
22 String(String),
24 Integer(i64),
26 Float(f64),
28 Bool(bool),
30 Null,
32}
33
34impl MetadataValue {
35 pub fn matches_eq(&self, other: &MetadataValue) -> bool {
37 compare_metadata_values(self, other)
38 .map(|ord| ord == std::cmp::Ordering::Equal)
39 .unwrap_or(false)
40 }
41
42 pub fn compare(&self, other: &MetadataValue) -> Option<std::cmp::Ordering> {
44 compare_metadata_values(self, other)
45 }
46
47 pub fn contains_str(&self, needle: &str) -> bool {
49 match self {
50 MetadataValue::String(s) => s.contains(needle),
51 _ => false,
52 }
53 }
54
55 pub fn starts_with(&self, prefix: &str) -> bool {
57 match self {
58 MetadataValue::String(s) => s.starts_with(prefix),
59 _ => false,
60 }
61 }
62
63 pub fn ends_with(&self, suffix: &str) -> bool {
65 match self {
66 MetadataValue::String(s) => s.ends_with(suffix),
67 _ => false,
68 }
69 }
70}
71
72impl From<String> for MetadataValue {
73 fn from(s: String) -> Self {
74 MetadataValue::String(s)
75 }
76}
77
78impl From<&str> for MetadataValue {
79 fn from(s: &str) -> Self {
80 MetadataValue::String(s.to_string())
81 }
82}
83
84impl From<i64> for MetadataValue {
85 fn from(i: i64) -> Self {
86 MetadataValue::Integer(i)
87 }
88}
89
90impl From<i32> for MetadataValue {
91 fn from(i: i32) -> Self {
92 MetadataValue::Integer(i as i64)
93 }
94}
95
96impl From<f64> for MetadataValue {
97 fn from(f: f64) -> Self {
98 MetadataValue::Float(f)
99 }
100}
101
102impl From<f32> for MetadataValue {
103 fn from(f: f32) -> Self {
104 MetadataValue::Float(f as f64)
105 }
106}
107
108impl From<bool> for MetadataValue {
109 fn from(b: bool) -> Self {
110 MetadataValue::Bool(b)
111 }
112}
113
114fn metadata_value_to_storage_value(value: &MetadataValue) -> Value {
115 match value {
116 MetadataValue::String(s) => Value::text(s.clone()),
117 MetadataValue::Integer(i) => Value::Integer(*i),
118 MetadataValue::Float(f) => Value::Float(*f),
119 MetadataValue::Bool(b) => Value::Boolean(*b),
120 MetadataValue::Null => Value::Null,
121 }
122}
123
124pub fn metadata_value_to_canonical_key(value: &MetadataValue) -> Option<CanonicalKey> {
128 let storage_value = metadata_value_to_storage_value(value);
129 value_to_canonical_key(&storage_value)
130}
131
132fn compare_metadata_values(
133 left: &MetadataValue,
134 right: &MetadataValue,
135) -> Option<std::cmp::Ordering> {
136 let left_value = metadata_value_to_storage_value(left);
137 let right_value = metadata_value_to_storage_value(right);
138 partial_compare_values(&left_value, &right_value).or_else(|| {
139 let left_key = value_to_canonical_key(&left_value)?;
140 let right_key = value_to_canonical_key(&right_value)?;
141 (left_key.family() == right_key.family()).then(|| left_key.cmp(&right_key))
142 })
143}
144
145#[derive(Debug, Clone, Default)]
147pub struct MetadataEntry {
148 pub strings: std::collections::HashMap<String, String>,
150 pub integers: std::collections::HashMap<String, i64>,
152 pub floats: std::collections::HashMap<String, f64>,
154 pub bools: std::collections::HashMap<String, bool>,
156}
157
158impl MetadataEntry {
159 pub fn new() -> Self {
161 Self::default()
162 }
163
164 pub fn insert(&mut self, key: impl Into<String>, value: MetadataValue) {
166 let key = key.into();
167 match value {
168 MetadataValue::String(s) => {
169 self.strings.insert(key, s);
170 }
171 MetadataValue::Integer(i) => {
172 self.integers.insert(key, i);
173 }
174 MetadataValue::Float(f) => {
175 self.floats.insert(key, f);
176 }
177 MetadataValue::Bool(b) => {
178 self.bools.insert(key, b);
179 }
180 MetadataValue::Null => {
181 self.strings.remove(&key);
183 self.integers.remove(&key);
184 self.floats.remove(&key);
185 self.bools.remove(&key);
186 }
187 }
188 }
189
190 pub fn get(&self, key: &str) -> Option<MetadataValue> {
192 if let Some(s) = self.strings.get(key) {
193 return Some(MetadataValue::String(s.clone()));
194 }
195 if let Some(i) = self.integers.get(key) {
196 return Some(MetadataValue::Integer(*i));
197 }
198 if let Some(f) = self.floats.get(key) {
199 return Some(MetadataValue::Float(*f));
200 }
201 if let Some(b) = self.bools.get(key) {
202 return Some(MetadataValue::Bool(*b));
203 }
204 None
205 }
206
207 pub fn contains_key(&self, key: &str) -> bool {
209 self.strings.contains_key(key)
210 || self.integers.contains_key(key)
211 || self.floats.contains_key(key)
212 || self.bools.contains_key(key)
213 }
214
215 pub fn keys(&self) -> Vec<String> {
217 let mut keys: Vec<String> = Vec::new();
218 keys.extend(self.strings.keys().cloned());
219 keys.extend(self.integers.keys().cloned());
220 keys.extend(self.floats.keys().cloned());
221 keys.extend(self.bools.keys().cloned());
222 keys
223 }
224
225 pub fn is_empty(&self) -> bool {
227 self.strings.is_empty()
228 && self.integers.is_empty()
229 && self.floats.is_empty()
230 && self.bools.is_empty()
231 }
232}
233
234#[derive(Debug, Clone)]
236pub enum MetadataFilter {
237 Eq(String, MetadataValue),
239 Ne(String, MetadataValue),
241 Gt(String, MetadataValue),
243 Gte(String, MetadataValue),
245 Lt(String, MetadataValue),
247 Lte(String, MetadataValue),
249 In(String, Vec<MetadataValue>),
251 NotIn(String, Vec<MetadataValue>),
253 Contains(String, String),
255 StartsWith(String, String),
257 EndsWith(String, String),
259 GeoRadius {
261 key: String,
262 center_lat: f64,
263 center_lon: f64,
264 radius_km: f64,
265 },
266 Exists(String),
268 NotExists(String),
270 And(Vec<MetadataFilter>),
272 Or(Vec<MetadataFilter>),
274 Not(Box<MetadataFilter>),
276}
277
278impl MetadataFilter {
279 pub fn eq(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
281 MetadataFilter::Eq(key.into(), value.into())
282 }
283
284 pub fn ne(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
286 MetadataFilter::Ne(key.into(), value.into())
287 }
288
289 pub fn gt(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
291 MetadataFilter::Gt(key.into(), value.into())
292 }
293
294 pub fn gte(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
296 MetadataFilter::Gte(key.into(), value.into())
297 }
298
299 pub fn lt(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
301 MetadataFilter::Lt(key.into(), value.into())
302 }
303
304 pub fn lte(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
306 MetadataFilter::Lte(key.into(), value.into())
307 }
308
309 pub fn and(filters: Vec<MetadataFilter>) -> Self {
311 MetadataFilter::And(filters)
312 }
313
314 pub fn or(filters: Vec<MetadataFilter>) -> Self {
316 MetadataFilter::Or(filters)
317 }
318
319 #[allow(clippy::should_implement_trait)]
323 pub fn not(filter: MetadataFilter) -> Self {
324 MetadataFilter::Not(Box::new(filter))
325 }
326
327 pub fn matches(&self, entry: &MetadataEntry) -> bool {
329 match self {
330 MetadataFilter::Eq(key, value) => {
331 entry.get(key).map(|v| v.matches_eq(value)).unwrap_or(false)
332 }
333 MetadataFilter::Ne(key, value) => {
334 entry.get(key).map(|v| !v.matches_eq(value)).unwrap_or(true)
335 }
336 MetadataFilter::Gt(key, value) => entry
337 .get(key)
338 .and_then(|v| v.compare(value))
339 .map(|ord| ord == std::cmp::Ordering::Greater)
340 .unwrap_or(false),
341 MetadataFilter::Gte(key, value) => entry
342 .get(key)
343 .and_then(|v| v.compare(value))
344 .map(|ord| ord != std::cmp::Ordering::Less)
345 .unwrap_or(false),
346 MetadataFilter::Lt(key, value) => entry
347 .get(key)
348 .and_then(|v| v.compare(value))
349 .map(|ord| ord == std::cmp::Ordering::Less)
350 .unwrap_or(false),
351 MetadataFilter::Lte(key, value) => entry
352 .get(key)
353 .and_then(|v| v.compare(value))
354 .map(|ord| ord != std::cmp::Ordering::Greater)
355 .unwrap_or(false),
356 MetadataFilter::In(key, values) => entry
357 .get(key)
358 .map(|v| values.iter().any(|val| v.matches_eq(val)))
359 .unwrap_or(false),
360 MetadataFilter::NotIn(key, values) => entry
361 .get(key)
362 .map(|v| !values.iter().any(|val| v.matches_eq(val)))
363 .unwrap_or(true),
364 MetadataFilter::Contains(key, needle) => entry
365 .get(key)
366 .map(|v| v.contains_str(needle))
367 .unwrap_or(false),
368 MetadataFilter::StartsWith(key, prefix) => entry
369 .get(key)
370 .map(|v| v.starts_with(prefix))
371 .unwrap_or(false),
372 MetadataFilter::EndsWith(key, suffix) => {
373 entry.get(key).map(|v| v.ends_with(suffix)).unwrap_or(false)
374 }
375 MetadataFilter::GeoRadius { .. } => false,
376 MetadataFilter::Exists(key) => entry.contains_key(key),
377 MetadataFilter::NotExists(key) => !entry.contains_key(key),
378 MetadataFilter::And(filters) => filters.iter().all(|f| f.matches(entry)),
379 MetadataFilter::Or(filters) => filters.iter().any(|f| f.matches(entry)),
380 MetadataFilter::Not(filter) => !filter.matches(entry),
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use std::cmp::Ordering;
389
390 #[test]
391 fn metadata_values_compare_and_match_by_type() {
392 assert!(MetadataValue::from("red database").contains_str("data"));
393 assert!(MetadataValue::from("red database").starts_with("red"));
394 assert!(MetadataValue::from("red database").ends_with("base"));
395 assert!(!MetadataValue::from(42_i64).contains_str("42"));
396
397 assert!(MetadataValue::from(10_i64).matches_eq(&MetadataValue::from(10_i64)));
398 assert!(!MetadataValue::from(10_i64).matches_eq(&MetadataValue::from(11_i64)));
399 assert_eq!(
400 MetadataValue::from(10_i64).compare(&MetadataValue::from(11_i64)),
401 Some(Ordering::Less)
402 );
403 assert_eq!(
404 MetadataValue::from(true).compare(&MetadataValue::from("true")),
405 None
406 );
407 }
408
409 #[test]
410 fn metadata_values_convert_to_canonical_keys() {
411 assert!(metadata_value_to_canonical_key(&MetadataValue::from("alpha")).is_some());
412 assert!(metadata_value_to_canonical_key(&MetadataValue::from(7_i64)).is_some());
413 assert!(metadata_value_to_canonical_key(&MetadataValue::from(1.5_f64)).is_some());
414 assert!(metadata_value_to_canonical_key(&MetadataValue::from(true)).is_some());
415 }
416
417 #[test]
418 fn metadata_entry_inserts_gets_keys_and_removes_nulls() {
419 let mut entry = MetadataEntry::new();
420 assert!(entry.is_empty());
421
422 entry.insert("title", MetadataValue::from("Graph Guide"));
423 entry.insert("pages", MetadataValue::from(100_i64));
424 entry.insert("score", MetadataValue::from(0.75_f64));
425 entry.insert("published", MetadataValue::from(true));
426
427 assert_eq!(entry.get("title"), Some(MetadataValue::from("Graph Guide")));
428 assert_eq!(entry.get("pages"), Some(MetadataValue::from(100_i64)));
429 assert_eq!(entry.get("score"), Some(MetadataValue::from(0.75_f64)));
430 assert_eq!(entry.get("published"), Some(MetadataValue::from(true)));
431 assert!(entry.contains_key("title"));
432 assert!(!entry.is_empty());
433
434 let mut keys = entry.keys();
435 keys.sort();
436 assert_eq!(keys, vec!["pages", "published", "score", "title"]);
437
438 entry.insert("title", MetadataValue::Null);
439 assert_eq!(entry.get("title"), None);
440 assert!(!entry.contains_key("title"));
441 }
442
443 #[test]
444 fn metadata_filters_cover_comparison_membership_and_strings() {
445 let mut entry = MetadataEntry::new();
446 entry.insert("title", MetadataValue::from("Graph Guide"));
447 entry.insert("pages", MetadataValue::from(100_i64));
448
449 assert!(MetadataFilter::eq("title", "Graph Guide").matches(&entry));
450 assert!(!MetadataFilter::eq("title", "Other").matches(&entry));
451 assert!(MetadataFilter::ne("title", "Other").matches(&entry));
452 assert!(MetadataFilter::ne("missing", "anything").matches(&entry));
453
454 assert!(MetadataFilter::gt("pages", 99_i64).matches(&entry));
455 assert!(MetadataFilter::gte("pages", 100_i64).matches(&entry));
456 assert!(MetadataFilter::lt("pages", 101_i64).matches(&entry));
457 assert!(MetadataFilter::lte("pages", 100_i64).matches(&entry));
458 assert!(!MetadataFilter::gt("missing", 1_i64).matches(&entry));
459
460 assert!(MetadataFilter::In(
461 "pages".to_string(),
462 vec![MetadataValue::from(1_i64), MetadataValue::from(100_i64)]
463 )
464 .matches(&entry));
465 assert!(MetadataFilter::NotIn(
466 "pages".to_string(),
467 vec![MetadataValue::from(1_i64), MetadataValue::from(2_i64)]
468 )
469 .matches(&entry));
470 assert!(
471 MetadataFilter::NotIn("missing".to_string(), vec![MetadataValue::from(1_i64)])
472 .matches(&entry)
473 );
474
475 assert!(MetadataFilter::Contains("title".to_string(), "Guide".to_string()).matches(&entry));
476 assert!(
477 MetadataFilter::StartsWith("title".to_string(), "Graph".to_string()).matches(&entry)
478 );
479 assert!(MetadataFilter::EndsWith("title".to_string(), "Guide".to_string()).matches(&entry));
480 }
481
482 #[test]
483 fn metadata_filters_cover_existence_and_boolean_composition() {
484 let mut entry = MetadataEntry::new();
485 entry.insert("title", MetadataValue::from("Graph Guide"));
486 entry.insert("pages", MetadataValue::from(100_i64));
487
488 assert!(MetadataFilter::Exists("title".to_string()).matches(&entry));
489 assert!(MetadataFilter::NotExists("missing".to_string()).matches(&entry));
490 assert!(MetadataFilter::and(vec![
491 MetadataFilter::eq("title", "Graph Guide"),
492 MetadataFilter::gte("pages", 100_i64),
493 ])
494 .matches(&entry));
495 assert!(MetadataFilter::or(vec![
496 MetadataFilter::eq("title", "Other"),
497 MetadataFilter::eq("pages", 100_i64),
498 ])
499 .matches(&entry));
500 assert!(MetadataFilter::not(MetadataFilter::eq("title", "Other")).matches(&entry));
501 }
502}