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 Exists(String),
261 NotExists(String),
263 And(Vec<MetadataFilter>),
265 Or(Vec<MetadataFilter>),
267 Not(Box<MetadataFilter>),
269}
270
271impl MetadataFilter {
272 pub fn eq(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
274 MetadataFilter::Eq(key.into(), value.into())
275 }
276
277 pub fn ne(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
279 MetadataFilter::Ne(key.into(), value.into())
280 }
281
282 pub fn gt(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
284 MetadataFilter::Gt(key.into(), value.into())
285 }
286
287 pub fn gte(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
289 MetadataFilter::Gte(key.into(), value.into())
290 }
291
292 pub fn lt(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
294 MetadataFilter::Lt(key.into(), value.into())
295 }
296
297 pub fn lte(key: impl Into<String>, value: impl Into<MetadataValue>) -> Self {
299 MetadataFilter::Lte(key.into(), value.into())
300 }
301
302 pub fn and(filters: Vec<MetadataFilter>) -> Self {
304 MetadataFilter::And(filters)
305 }
306
307 pub fn or(filters: Vec<MetadataFilter>) -> Self {
309 MetadataFilter::Or(filters)
310 }
311
312 #[allow(clippy::should_implement_trait)]
316 pub fn not(filter: MetadataFilter) -> Self {
317 MetadataFilter::Not(Box::new(filter))
318 }
319
320 pub fn matches(&self, entry: &MetadataEntry) -> bool {
322 match self {
323 MetadataFilter::Eq(key, value) => {
324 entry.get(key).map(|v| v.matches_eq(value)).unwrap_or(false)
325 }
326 MetadataFilter::Ne(key, value) => {
327 entry.get(key).map(|v| !v.matches_eq(value)).unwrap_or(true)
328 }
329 MetadataFilter::Gt(key, value) => entry
330 .get(key)
331 .and_then(|v| v.compare(value))
332 .map(|ord| ord == std::cmp::Ordering::Greater)
333 .unwrap_or(false),
334 MetadataFilter::Gte(key, value) => entry
335 .get(key)
336 .and_then(|v| v.compare(value))
337 .map(|ord| ord != std::cmp::Ordering::Less)
338 .unwrap_or(false),
339 MetadataFilter::Lt(key, value) => entry
340 .get(key)
341 .and_then(|v| v.compare(value))
342 .map(|ord| ord == std::cmp::Ordering::Less)
343 .unwrap_or(false),
344 MetadataFilter::Lte(key, value) => entry
345 .get(key)
346 .and_then(|v| v.compare(value))
347 .map(|ord| ord != std::cmp::Ordering::Greater)
348 .unwrap_or(false),
349 MetadataFilter::In(key, values) => entry
350 .get(key)
351 .map(|v| values.iter().any(|val| v.matches_eq(val)))
352 .unwrap_or(false),
353 MetadataFilter::NotIn(key, values) => entry
354 .get(key)
355 .map(|v| !values.iter().any(|val| v.matches_eq(val)))
356 .unwrap_or(true),
357 MetadataFilter::Contains(key, needle) => entry
358 .get(key)
359 .map(|v| v.contains_str(needle))
360 .unwrap_or(false),
361 MetadataFilter::StartsWith(key, prefix) => entry
362 .get(key)
363 .map(|v| v.starts_with(prefix))
364 .unwrap_or(false),
365 MetadataFilter::EndsWith(key, suffix) => {
366 entry.get(key).map(|v| v.ends_with(suffix)).unwrap_or(false)
367 }
368 MetadataFilter::Exists(key) => entry.contains_key(key),
369 MetadataFilter::NotExists(key) => !entry.contains_key(key),
370 MetadataFilter::And(filters) => filters.iter().all(|f| f.matches(entry)),
371 MetadataFilter::Or(filters) => filters.iter().any(|f| f.matches(entry)),
372 MetadataFilter::Not(filter) => !filter.matches(entry),
373 }
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use std::cmp::Ordering;
381
382 #[test]
383 fn metadata_values_compare_and_match_by_type() {
384 assert!(MetadataValue::from("red database").contains_str("data"));
385 assert!(MetadataValue::from("red database").starts_with("red"));
386 assert!(MetadataValue::from("red database").ends_with("base"));
387 assert!(!MetadataValue::from(42_i64).contains_str("42"));
388
389 assert!(MetadataValue::from(10_i64).matches_eq(&MetadataValue::from(10_i64)));
390 assert!(!MetadataValue::from(10_i64).matches_eq(&MetadataValue::from(11_i64)));
391 assert_eq!(
392 MetadataValue::from(10_i64).compare(&MetadataValue::from(11_i64)),
393 Some(Ordering::Less)
394 );
395 assert_eq!(
396 MetadataValue::from(true).compare(&MetadataValue::from("true")),
397 None
398 );
399 }
400
401 #[test]
402 fn metadata_values_convert_to_canonical_keys() {
403 assert!(metadata_value_to_canonical_key(&MetadataValue::from("alpha")).is_some());
404 assert!(metadata_value_to_canonical_key(&MetadataValue::from(7_i64)).is_some());
405 assert!(metadata_value_to_canonical_key(&MetadataValue::from(1.5_f64)).is_some());
406 assert!(metadata_value_to_canonical_key(&MetadataValue::from(true)).is_some());
407 }
408
409 #[test]
410 fn metadata_entry_inserts_gets_keys_and_removes_nulls() {
411 let mut entry = MetadataEntry::new();
412 assert!(entry.is_empty());
413
414 entry.insert("title", MetadataValue::from("Graph Guide"));
415 entry.insert("pages", MetadataValue::from(100_i64));
416 entry.insert("score", MetadataValue::from(0.75_f64));
417 entry.insert("published", MetadataValue::from(true));
418
419 assert_eq!(entry.get("title"), Some(MetadataValue::from("Graph Guide")));
420 assert_eq!(entry.get("pages"), Some(MetadataValue::from(100_i64)));
421 assert_eq!(entry.get("score"), Some(MetadataValue::from(0.75_f64)));
422 assert_eq!(entry.get("published"), Some(MetadataValue::from(true)));
423 assert!(entry.contains_key("title"));
424 assert!(!entry.is_empty());
425
426 let mut keys = entry.keys();
427 keys.sort();
428 assert_eq!(keys, vec!["pages", "published", "score", "title"]);
429
430 entry.insert("title", MetadataValue::Null);
431 assert_eq!(entry.get("title"), None);
432 assert!(!entry.contains_key("title"));
433 }
434
435 #[test]
436 fn metadata_filters_cover_comparison_membership_and_strings() {
437 let mut entry = MetadataEntry::new();
438 entry.insert("title", MetadataValue::from("Graph Guide"));
439 entry.insert("pages", MetadataValue::from(100_i64));
440
441 assert!(MetadataFilter::eq("title", "Graph Guide").matches(&entry));
442 assert!(!MetadataFilter::eq("title", "Other").matches(&entry));
443 assert!(MetadataFilter::ne("title", "Other").matches(&entry));
444 assert!(MetadataFilter::ne("missing", "anything").matches(&entry));
445
446 assert!(MetadataFilter::gt("pages", 99_i64).matches(&entry));
447 assert!(MetadataFilter::gte("pages", 100_i64).matches(&entry));
448 assert!(MetadataFilter::lt("pages", 101_i64).matches(&entry));
449 assert!(MetadataFilter::lte("pages", 100_i64).matches(&entry));
450 assert!(!MetadataFilter::gt("missing", 1_i64).matches(&entry));
451
452 assert!(MetadataFilter::In(
453 "pages".to_string(),
454 vec![MetadataValue::from(1_i64), MetadataValue::from(100_i64)]
455 )
456 .matches(&entry));
457 assert!(MetadataFilter::NotIn(
458 "pages".to_string(),
459 vec![MetadataValue::from(1_i64), MetadataValue::from(2_i64)]
460 )
461 .matches(&entry));
462 assert!(
463 MetadataFilter::NotIn("missing".to_string(), vec![MetadataValue::from(1_i64)])
464 .matches(&entry)
465 );
466
467 assert!(MetadataFilter::Contains("title".to_string(), "Guide".to_string()).matches(&entry));
468 assert!(
469 MetadataFilter::StartsWith("title".to_string(), "Graph".to_string()).matches(&entry)
470 );
471 assert!(MetadataFilter::EndsWith("title".to_string(), "Guide".to_string()).matches(&entry));
472 }
473
474 #[test]
475 fn metadata_filters_cover_existence_and_boolean_composition() {
476 let mut entry = MetadataEntry::new();
477 entry.insert("title", MetadataValue::from("Graph Guide"));
478 entry.insert("pages", MetadataValue::from(100_i64));
479
480 assert!(MetadataFilter::Exists("title".to_string()).matches(&entry));
481 assert!(MetadataFilter::NotExists("missing".to_string()).matches(&entry));
482 assert!(MetadataFilter::and(vec![
483 MetadataFilter::eq("title", "Graph Guide"),
484 MetadataFilter::gte("pages", 100_i64),
485 ])
486 .matches(&entry));
487 assert!(MetadataFilter::or(vec![
488 MetadataFilter::eq("title", "Other"),
489 MetadataFilter::eq("pages", 100_i64),
490 ])
491 .matches(&entry));
492 assert!(MetadataFilter::not(MetadataFilter::eq("title", "Other")).matches(&entry));
493 }
494}