1use std::collections::HashSet;
68use std::fmt;
69
70use serde::{Deserialize, Serialize};
71
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub enum FilterValue {
79 String(String),
81 Int64(i64),
83 Uint64(u64),
85 Float64(f64),
87 Bool(bool),
89 Null,
91}
92
93impl FilterValue {
94 pub fn eq_match(&self, other: &FilterValue) -> bool {
96 match (self, other) {
97 (FilterValue::String(a), FilterValue::String(b)) => a == b,
98 (FilterValue::Int64(a), FilterValue::Int64(b)) => a == b,
99 (FilterValue::Uint64(a), FilterValue::Uint64(b)) => a == b,
100 (FilterValue::Float64(a), FilterValue::Float64(b)) => {
101 (a - b).abs() < f64::EPSILON
102 }
103 (FilterValue::Bool(a), FilterValue::Bool(b)) => a == b,
104 (FilterValue::Null, FilterValue::Null) => true,
105 _ => false,
106 }
107 }
108
109 pub fn partial_cmp(&self, other: &FilterValue) -> Option<std::cmp::Ordering> {
111 match (self, other) {
112 (FilterValue::Int64(a), FilterValue::Int64(b)) => Some(a.cmp(b)),
113 (FilterValue::Uint64(a), FilterValue::Uint64(b)) => Some(a.cmp(b)),
114 (FilterValue::Float64(a), FilterValue::Float64(b)) => a.partial_cmp(b),
115 (FilterValue::String(a), FilterValue::String(b)) => Some(a.cmp(b)),
116 _ => None,
117 }
118 }
119}
120
121impl fmt::Display for FilterValue {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 FilterValue::String(s) => write!(f, "'{}'", s),
125 FilterValue::Int64(i) => write!(f, "{}", i),
126 FilterValue::Uint64(u) => write!(f, "{}u64", u),
127 FilterValue::Float64(v) => write!(f, "{}", v),
128 FilterValue::Bool(b) => write!(f, "{}", b),
129 FilterValue::Null => write!(f, "NULL"),
130 }
131 }
132}
133
134impl From<&str> for FilterValue {
135 fn from(s: &str) -> Self {
136 FilterValue::String(s.to_string())
137 }
138}
139
140impl From<String> for FilterValue {
141 fn from(s: String) -> Self {
142 FilterValue::String(s)
143 }
144}
145
146impl From<i64> for FilterValue {
147 fn from(i: i64) -> Self {
148 FilterValue::Int64(i)
149 }
150}
151
152impl From<u64> for FilterValue {
153 fn from(u: u64) -> Self {
154 FilterValue::Uint64(u)
155 }
156}
157
158impl From<f64> for FilterValue {
159 fn from(f: f64) -> Self {
160 FilterValue::Float64(f)
161 }
162}
163
164impl From<bool> for FilterValue {
165 fn from(b: bool) -> Self {
166 FilterValue::Bool(b)
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub enum FilterAtom {
173 Eq {
175 field: String,
176 value: FilterValue,
177 },
178
179 Ne {
181 field: String,
182 value: FilterValue,
183 },
184
185 In {
187 field: String,
188 values: Vec<FilterValue>,
189 },
190
191 NotIn {
193 field: String,
194 values: Vec<FilterValue>,
195 },
196
197 Range {
200 field: String,
201 min: Option<FilterValue>,
202 max: Option<FilterValue>,
203 min_inclusive: bool,
204 max_inclusive: bool,
205 },
206
207 Prefix {
209 field: String,
210 prefix: String,
211 },
212
213 Contains {
215 field: String,
216 substring: String,
217 },
218
219 HasTag {
221 tag: String,
222 },
223
224 True,
226
227 False,
229}
230
231impl FilterAtom {
232 pub fn eq(field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
234 FilterAtom::Eq {
235 field: field.into(),
236 value: value.into(),
237 }
238 }
239
240 pub fn in_set(field: impl Into<String>, values: Vec<FilterValue>) -> Self {
242 FilterAtom::In {
243 field: field.into(),
244 values,
245 }
246 }
247
248 pub fn range(
250 field: impl Into<String>,
251 min: Option<FilterValue>,
252 max: Option<FilterValue>,
253 ) -> Self {
254 FilterAtom::Range {
255 field: field.into(),
256 min,
257 max,
258 min_inclusive: true,
259 max_inclusive: true,
260 }
261 }
262
263 pub fn range_exclusive(
265 field: impl Into<String>,
266 min: Option<FilterValue>,
267 max: Option<FilterValue>,
268 ) -> Self {
269 FilterAtom::Range {
270 field: field.into(),
271 min,
272 max,
273 min_inclusive: false,
274 max_inclusive: false,
275 }
276 }
277
278 pub fn field(&self) -> Option<&str> {
280 match self {
281 FilterAtom::Eq { field, .. } => Some(field),
282 FilterAtom::Ne { field, .. } => Some(field),
283 FilterAtom::In { field, .. } => Some(field),
284 FilterAtom::NotIn { field, .. } => Some(field),
285 FilterAtom::Range { field, .. } => Some(field),
286 FilterAtom::Prefix { field, .. } => Some(field),
287 FilterAtom::Contains { field, .. } => Some(field),
288 FilterAtom::HasTag { .. } => None,
289 FilterAtom::True | FilterAtom::False => None,
290 }
291 }
292
293 pub fn is_trivially_true(&self) -> bool {
295 matches!(self, FilterAtom::True)
296 }
297
298 pub fn is_trivially_false(&self) -> bool {
300 matches!(self, FilterAtom::False)
301 }
302
303 pub fn negate(&self) -> FilterAtom {
305 match self {
306 FilterAtom::Eq { field, value } => FilterAtom::Ne {
307 field: field.clone(),
308 value: value.clone(),
309 },
310 FilterAtom::Ne { field, value } => FilterAtom::Eq {
311 field: field.clone(),
312 value: value.clone(),
313 },
314 FilterAtom::In { field, values } => FilterAtom::NotIn {
315 field: field.clone(),
316 values: values.clone(),
317 },
318 FilterAtom::NotIn { field, values } => FilterAtom::In {
319 field: field.clone(),
320 values: values.clone(),
321 },
322 FilterAtom::True => FilterAtom::False,
323 FilterAtom::False => FilterAtom::True,
324 other => other.clone(), }
327 }
328}
329
330impl fmt::Display for FilterAtom {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 match self {
333 FilterAtom::Eq { field, value } => write!(f, "{} = {}", field, value),
334 FilterAtom::Ne { field, value } => write!(f, "{} != {}", field, value),
335 FilterAtom::In { field, values } => {
336 let vals: Vec<_> = values.iter().map(|v| v.to_string()).collect();
337 write!(f, "{} IN ({})", field, vals.join(", "))
338 }
339 FilterAtom::NotIn { field, values } => {
340 let vals: Vec<_> = values.iter().map(|v| v.to_string()).collect();
341 write!(f, "{} NOT IN ({})", field, vals.join(", "))
342 }
343 FilterAtom::Range { field, min, max, min_inclusive, max_inclusive } => {
344 let left = if *min_inclusive { "[" } else { "(" };
345 let right = if *max_inclusive { "]" } else { ")" };
346 let min_str = min.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "-∞".to_string());
347 let max_str = max.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "∞".to_string());
348 write!(f, "{} ∈ {}{}, {}{}", field, left, min_str, max_str, right)
349 }
350 FilterAtom::Prefix { field, prefix } => write!(f, "{} STARTS WITH '{}'", field, prefix),
351 FilterAtom::Contains { field, substring } => write!(f, "{} CONTAINS '{}'", field, substring),
352 FilterAtom::HasTag { tag } => write!(f, "HAS_TAG('{}')", tag),
353 FilterAtom::True => write!(f, "TRUE"),
354 FilterAtom::False => write!(f, "FALSE"),
355 }
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
365pub struct Disjunction {
366 pub atoms: Vec<FilterAtom>,
367}
368
369impl Disjunction {
370 pub fn new(atoms: Vec<FilterAtom>) -> Self {
372 Self { atoms }
373 }
374
375 pub fn single(atom: FilterAtom) -> Self {
377 Self { atoms: vec![atom] }
378 }
379
380 pub fn is_trivially_true(&self) -> bool {
382 self.atoms.iter().any(|a| a.is_trivially_true())
383 }
384
385 pub fn is_trivially_false(&self) -> bool {
387 self.atoms.is_empty() || self.atoms.iter().all(|a| a.is_trivially_false())
388 }
389
390 pub fn simplify(self) -> Self {
392 let atoms: Vec<_> = self.atoms.into_iter()
394 .filter(|a| !a.is_trivially_false())
395 .collect();
396
397 if atoms.iter().any(|a| a.is_trivially_true()) {
399 return Self { atoms: vec![FilterAtom::True] };
400 }
401
402 if atoms.is_empty() {
404 return Self { atoms: vec![FilterAtom::False] };
405 }
406
407 Self { atoms }
408 }
409}
410
411impl fmt::Display for Disjunction {
412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413 if self.atoms.len() == 1 {
414 write!(f, "{}", self.atoms[0])
415 } else {
416 let parts: Vec<_> = self.atoms.iter().map(|a| a.to_string()).collect();
417 write!(f, "({})", parts.join(" OR "))
418 }
419 }
420}
421
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
431pub struct FilterIR {
432 pub clauses: Vec<Disjunction>,
434}
435
436impl FilterIR {
437 pub fn all() -> Self {
439 Self { clauses: vec![] }
440 }
441
442 pub fn none() -> Self {
444 Self {
445 clauses: vec![Disjunction::single(FilterAtom::False)],
446 }
447 }
448
449 pub fn from_atom(atom: FilterAtom) -> Self {
451 Self {
452 clauses: vec![Disjunction::single(atom)],
453 }
454 }
455
456 pub fn from_disjunction(disj: Disjunction) -> Self {
458 Self { clauses: vec![disj] }
459 }
460
461 pub fn and(mut self, other: FilterIR) -> Self {
466 self.clauses.extend(other.clauses);
467 self
468 }
469
470 pub fn and_atom(mut self, atom: FilterAtom) -> Self {
472 self.clauses.push(Disjunction::single(atom));
473 self
474 }
475
476 pub fn or(self, other: FilterIR) -> Self {
480 if self.clauses.is_empty() {
481 return other;
482 }
483 if other.clauses.is_empty() {
484 return self;
485 }
486
487 let mut new_clauses = Vec::new();
490 for c1 in &self.clauses {
491 for c2 in &other.clauses {
492 let mut combined = c1.atoms.clone();
493 combined.extend(c2.atoms.clone());
494 new_clauses.push(Disjunction::new(combined));
495 }
496 }
497
498 FilterIR { clauses: new_clauses }
499 }
500
501 pub fn is_all(&self) -> bool {
503 self.clauses.is_empty() || self.clauses.iter().all(|c| c.is_trivially_true())
504 }
505
506 pub fn is_none(&self) -> bool {
508 self.clauses.iter().any(|c| c.is_trivially_false())
509 }
510
511 pub fn simplify(self) -> Self {
513 let clauses: Vec<_> = self.clauses
514 .into_iter()
515 .map(|c| c.simplify())
516 .filter(|c| !c.is_trivially_true())
517 .collect();
518
519 if clauses.iter().any(|c| c.is_trivially_false()) {
521 return Self::none();
522 }
523
524 Self { clauses }
525 }
526
527 pub fn atoms_for_field(&self, field: &str) -> Vec<&FilterAtom> {
529 self.clauses
530 .iter()
531 .flat_map(|c| c.atoms.iter())
532 .filter(|a| a.field() == Some(field))
533 .collect()
534 }
535
536 pub fn constrains_field(&self, field: &str) -> bool {
538 !self.atoms_for_field(field).is_empty()
539 }
540
541 pub fn constrained_fields(&self) -> HashSet<&str> {
543 self.clauses
544 .iter()
545 .flat_map(|c| c.atoms.iter())
546 .filter_map(|a| a.field())
547 .collect()
548 }
549}
550
551impl Default for FilterIR {
552 fn default() -> Self {
553 Self::all()
554 }
555}
556
557impl fmt::Display for FilterIR {
558 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559 if self.clauses.is_empty() {
560 return write!(f, "TRUE");
561 }
562 let parts: Vec<_> = self.clauses.iter().map(|c| c.to_string()).collect();
563 write!(f, "{}", parts.join(" AND "))
564 }
565}
566
567#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
579pub struct AuthScope {
580 pub allowed_namespaces: Vec<String>,
582
583 pub tenant_id: Option<String>,
585
586 pub project_id: Option<String>,
588
589 pub expires_at: Option<u64>,
591
592 pub capabilities: AuthCapabilities,
594
595 pub acl_tags: Vec<String>,
597}
598
599#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
601pub struct AuthCapabilities {
602 pub can_read: bool,
604 pub can_write: bool,
606 pub can_delete: bool,
608 pub can_admin: bool,
610}
611
612impl AuthScope {
613 pub fn for_namespace(namespace: impl Into<String>) -> Self {
615 Self {
616 allowed_namespaces: vec![namespace.into()],
617 tenant_id: None,
618 project_id: None,
619 expires_at: None,
620 capabilities: AuthCapabilities {
621 can_read: true,
622 can_write: false,
623 can_delete: false,
624 can_admin: false,
625 },
626 acl_tags: vec![],
627 }
628 }
629
630 pub fn full_access(namespace: impl Into<String>) -> Self {
632 Self {
633 allowed_namespaces: vec![namespace.into()],
634 tenant_id: None,
635 project_id: None,
636 expires_at: None,
637 capabilities: AuthCapabilities {
638 can_read: true,
639 can_write: true,
640 can_delete: true,
641 can_admin: false,
642 },
643 acl_tags: vec![],
644 }
645 }
646
647 pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
649 self.allowed_namespaces.push(namespace.into());
650 self
651 }
652
653 pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
655 self.tenant_id = Some(tenant_id.into());
656 self
657 }
658
659 pub fn with_project(mut self, project_id: impl Into<String>) -> Self {
661 self.project_id = Some(project_id.into());
662 self
663 }
664
665 pub fn with_expiry(mut self, expires_at: u64) -> Self {
667 self.expires_at = Some(expires_at);
668 self
669 }
670
671 pub fn with_acl_tags(mut self, tags: Vec<String>) -> Self {
673 self.acl_tags = tags;
674 self
675 }
676
677 pub fn is_expired(&self) -> bool {
679 if let Some(expires_at) = self.expires_at {
680 let now = std::time::SystemTime::now()
681 .duration_since(std::time::UNIX_EPOCH)
682 .map(|d| d.as_secs())
683 .unwrap_or(0);
684 now > expires_at
685 } else {
686 false
687 }
688 }
689
690 pub fn is_namespace_allowed(&self, namespace: &str) -> bool {
692 self.allowed_namespaces.iter().any(|ns| ns == namespace)
693 }
694
695 pub fn to_filter_ir(&self) -> FilterIR {
700 let mut filter = FilterIR::all();
701
702 if self.allowed_namespaces.len() == 1 {
704 filter = filter.and_atom(FilterAtom::eq(
705 "namespace",
706 self.allowed_namespaces[0].clone(),
707 ));
708 } else if !self.allowed_namespaces.is_empty() {
709 filter = filter.and_atom(FilterAtom::in_set(
710 "namespace",
711 self.allowed_namespaces
712 .iter()
713 .map(|ns| FilterValue::String(ns.clone()))
714 .collect(),
715 ));
716 }
717
718 if let Some(ref tenant_id) = self.tenant_id {
720 filter = filter.and_atom(FilterAtom::eq("tenant_id", tenant_id.clone()));
721 }
722
723 if let Some(ref project_id) = self.project_id {
725 filter = filter.and_atom(FilterAtom::eq("project_id", project_id.clone()));
726 }
727
728 filter
733 }
734}
735
736pub trait FilteredExecutor {
747 type QueryOp;
749
750 type Result;
752
753 type Error;
755
756 fn execute(
773 &self,
774 query: &Self::QueryOp,
775 filter_ir: &FilterIR,
776 auth_scope: &AuthScope,
777 ) -> Result<Self::Result, Self::Error>;
778
779 fn effective_filter(&self, filter_ir: &FilterIR, auth_scope: &AuthScope) -> FilterIR {
783 auth_scope.to_filter_ir().and(filter_ir.clone())
784 }
785}
786
787#[derive(Debug, Clone, Default)]
793pub struct FilterBuilder {
794 clauses: Vec<Disjunction>,
795}
796
797impl FilterBuilder {
798 pub fn new() -> Self {
800 Self::default()
801 }
802
803 pub fn eq(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
805 self.clauses.push(Disjunction::single(FilterAtom::eq(field, value)));
806 self
807 }
808
809 pub fn ne(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
811 self.clauses.push(Disjunction::single(FilterAtom::Ne {
812 field: field.to_string(),
813 value: value.into(),
814 }));
815 self
816 }
817
818 pub fn in_set(mut self, field: &str, values: Vec<FilterValue>) -> Self {
820 self.clauses.push(Disjunction::single(FilterAtom::in_set(field, values)));
821 self
822 }
823
824 pub fn range(
826 mut self,
827 field: &str,
828 min: Option<impl Into<FilterValue>>,
829 max: Option<impl Into<FilterValue>>,
830 ) -> Self {
831 self.clauses.push(Disjunction::single(FilterAtom::range(
832 field,
833 min.map(Into::into),
834 max.map(Into::into),
835 )));
836 self
837 }
838
839 pub fn gt(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
841 self.clauses.push(Disjunction::single(FilterAtom::Range {
842 field: field.to_string(),
843 min: Some(value.into()),
844 max: None,
845 min_inclusive: false,
846 max_inclusive: false,
847 }));
848 self
849 }
850
851 pub fn gte(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
853 self.clauses.push(Disjunction::single(FilterAtom::Range {
854 field: field.to_string(),
855 min: Some(value.into()),
856 max: None,
857 min_inclusive: true,
858 max_inclusive: false,
859 }));
860 self
861 }
862
863 pub fn lt(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
865 self.clauses.push(Disjunction::single(FilterAtom::Range {
866 field: field.to_string(),
867 min: None,
868 max: Some(value.into()),
869 min_inclusive: false,
870 max_inclusive: false,
871 }));
872 self
873 }
874
875 pub fn lte(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
877 self.clauses.push(Disjunction::single(FilterAtom::Range {
878 field: field.to_string(),
879 min: None,
880 max: Some(value.into()),
881 min_inclusive: false,
882 max_inclusive: true,
883 }));
884 self
885 }
886
887 pub fn prefix(mut self, field: &str, prefix: &str) -> Self {
889 self.clauses.push(Disjunction::single(FilterAtom::Prefix {
890 field: field.to_string(),
891 prefix: prefix.to_string(),
892 }));
893 self
894 }
895
896 pub fn contains(mut self, field: &str, substring: &str) -> Self {
898 self.clauses.push(Disjunction::single(FilterAtom::Contains {
899 field: field.to_string(),
900 substring: substring.to_string(),
901 }));
902 self
903 }
904
905 pub fn namespace(self, namespace: &str) -> Self {
907 self.eq("namespace", namespace)
908 }
909
910 pub fn doc_ids(self, doc_ids: &[u64]) -> Self {
912 self.in_set(
913 "doc_id",
914 doc_ids.iter().map(|&id| FilterValue::Uint64(id)).collect(),
915 )
916 }
917
918 pub fn time_range(self, field: &str, start: Option<u64>, end: Option<u64>) -> Self {
920 self.range(
921 field,
922 start.map(FilterValue::Uint64),
923 end.map(FilterValue::Uint64),
924 )
925 }
926
927 pub fn or_atoms(mut self, atoms: Vec<FilterAtom>) -> Self {
929 self.clauses.push(Disjunction::new(atoms));
930 self
931 }
932
933 pub fn build(self) -> FilterIR {
935 FilterIR { clauses: self.clauses }
936 }
937}
938
939#[macro_export]
953macro_rules! filter_ir {
954 () => {
956 $crate::filter_ir::FilterIR::all()
957 };
958
959 ($field:ident = $value:expr $(, $($rest:tt)*)?) => {{
961 let mut builder = $crate::filter_ir::FilterBuilder::new()
962 .eq(stringify!($field), $value);
963 $(
964 builder = filter_ir!(@chain builder, $($rest)*);
965 )?
966 builder.build()
967 }};
968
969 (@chain $builder:expr, $field:ident = $value:expr $(, $($rest:tt)*)?) => {{
971 let builder = $builder.eq(stringify!($field), $value);
972 $(
973 filter_ir!(@chain builder, $($rest)*)
974 )?
975 builder
976 }};
977}
978
979#[cfg(test)]
984mod tests {
985 use super::*;
986
987 #[test]
988 fn test_filter_atom_creation() {
989 let eq = FilterAtom::eq("namespace", "my_ns");
990 assert_eq!(eq.field(), Some("namespace"));
991
992 let range = FilterAtom::range("timestamp", Some(FilterValue::Uint64(1000)), Some(FilterValue::Uint64(2000)));
993 assert_eq!(range.field(), Some("timestamp"));
994 }
995
996 #[test]
997 fn test_filter_ir_conjunction() {
998 let filter1 = FilterIR::from_atom(FilterAtom::eq("namespace", "ns1"));
999 let filter2 = FilterIR::from_atom(FilterAtom::eq("project_id", "proj1"));
1000
1001 let combined = filter1.and(filter2);
1002 assert_eq!(combined.clauses.len(), 2);
1003 }
1004
1005 #[test]
1006 fn test_auth_scope_to_filter() {
1007 let scope = AuthScope::for_namespace("production")
1008 .with_tenant("acme_corp");
1009
1010 let filter = scope.to_filter_ir();
1011 assert!(filter.constrains_field("namespace"));
1012 assert!(filter.constrains_field("tenant_id"));
1013 assert!(!filter.constrains_field("project_id"));
1014 }
1015
1016 #[test]
1017 fn test_effective_filter() {
1018 let auth = AuthScope::for_namespace("production");
1019 let user_filter = FilterBuilder::new()
1020 .eq("source", "documents")
1021 .time_range("created_at", Some(1000), Some(2000))
1022 .build();
1023
1024 let effective = auth.to_filter_ir().and(user_filter);
1025
1026 assert_eq!(effective.clauses.len(), 3);
1028 assert!(effective.constrains_field("namespace"));
1029 assert!(effective.constrains_field("source"));
1030 assert!(effective.constrains_field("created_at"));
1031 }
1032
1033 #[test]
1034 fn test_filter_builder() {
1035 let filter = FilterBuilder::new()
1036 .namespace("my_namespace")
1037 .eq("project_id", "proj_123")
1038 .doc_ids(&[1, 2, 3, 4, 5])
1039 .time_range("timestamp", Some(1000), None)
1040 .build();
1041
1042 assert_eq!(filter.clauses.len(), 4);
1043 }
1044
1045 #[test]
1046 fn test_filter_simplification() {
1047 let filter = FilterIR::from_atom(FilterAtom::True)
1049 .and(FilterIR::from_atom(FilterAtom::eq("x", "y")));
1050 let simplified = filter.simplify();
1051 assert_eq!(simplified.clauses.len(), 1);
1052
1053 let filter2 = FilterIR::from_atom(FilterAtom::False)
1055 .and(FilterIR::from_atom(FilterAtom::eq("x", "y")));
1056 let simplified2 = filter2.simplify();
1057 assert!(simplified2.is_none());
1058 }
1059
1060 #[test]
1061 fn test_filter_display() {
1062 let filter = FilterBuilder::new()
1063 .eq("namespace", "prod")
1064 .range("timestamp", Some(1000i64), Some(2000i64))
1065 .build();
1066
1067 let display = filter.to_string();
1068 assert!(display.contains("namespace"));
1069 assert!(display.contains("timestamp"));
1070 }
1071}