1use crate::db::query::{
7 builder::FieldRef,
8 builder::{AggregateExpr, NumericProjectionExpr, RoundProjectionExpr, TextProjectionExpr},
9 plan::{
10 OrderDirection, OrderTerm as PlannedOrderTerm,
11 expr::{BinaryOp, Expr, FieldId, Function, UnaryOp},
12 },
13};
14use crate::{traits::FieldValue, value::Value};
15use candid::CandidType;
16use serde::Deserialize;
17
18#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
27#[serde(rename_all = "PascalCase")]
28pub enum FilterExpr {
29 True,
30 False,
31 And(Vec<Self>),
32 Or(Vec<Self>),
33 Not(Box<Self>),
34 Eq {
35 field: String,
36 value: Value,
37 },
38 EqCi {
39 field: String,
40 value: Value,
41 },
42 Ne {
43 field: String,
44 value: Value,
45 },
46 Lt {
47 field: String,
48 value: Value,
49 },
50 Lte {
51 field: String,
52 value: Value,
53 },
54 Gt {
55 field: String,
56 value: Value,
57 },
58 Gte {
59 field: String,
60 value: Value,
61 },
62 EqField {
63 left_field: String,
64 right_field: String,
65 },
66 NeField {
67 left_field: String,
68 right_field: String,
69 },
70 LtField {
71 left_field: String,
72 right_field: String,
73 },
74 LteField {
75 left_field: String,
76 right_field: String,
77 },
78 GtField {
79 left_field: String,
80 right_field: String,
81 },
82 GteField {
83 left_field: String,
84 right_field: String,
85 },
86 In {
87 field: String,
88 values: Vec<Value>,
89 },
90 NotIn {
91 field: String,
92 values: Vec<Value>,
93 },
94 Contains {
95 field: String,
96 value: Value,
97 },
98 TextContains {
99 field: String,
100 value: Value,
101 },
102 TextContainsCi {
103 field: String,
104 value: Value,
105 },
106 StartsWith {
107 field: String,
108 value: Value,
109 },
110 StartsWithCi {
111 field: String,
112 value: Value,
113 },
114 EndsWith {
115 field: String,
116 value: Value,
117 },
118 EndsWithCi {
119 field: String,
120 value: Value,
121 },
122 IsNull {
123 field: String,
124 },
125 IsNotNull {
126 field: String,
127 },
128 IsMissing {
129 field: String,
130 },
131 IsEmpty {
132 field: String,
133 },
134 IsNotEmpty {
135 field: String,
136 },
137}
138
139impl FilterExpr {
140 #[must_use]
142 pub(in crate::db) fn lower_bool_expr(&self) -> Expr {
143 match self {
144 Self::True => Expr::Literal(Value::Bool(true)),
145 Self::False => Expr::Literal(Value::Bool(false)),
146 Self::And(xs) => fold_filter_bool_chain(BinaryOp::And, xs),
147 Self::Or(xs) => fold_filter_bool_chain(BinaryOp::Or, xs),
148 Self::Not(x) => Expr::Unary {
149 op: UnaryOp::Not,
150 expr: Box::new(x.lower_bool_expr()),
151 },
152 Self::Eq { field, value } => field_compare_expr(BinaryOp::Eq, field, value.clone()),
153 Self::EqCi { field, value } => Expr::Binary {
154 op: BinaryOp::Eq,
155 left: Box::new(casefold_field_expr(field)),
156 right: Box::new(Expr::Literal(value.clone())),
157 },
158 Self::Ne { field, value } => field_compare_expr(BinaryOp::Ne, field, value.clone()),
159 Self::Lt { field, value } => field_compare_expr(BinaryOp::Lt, field, value.clone()),
160 Self::Lte { field, value } => field_compare_expr(BinaryOp::Lte, field, value.clone()),
161 Self::Gt { field, value } => field_compare_expr(BinaryOp::Gt, field, value.clone()),
162 Self::Gte { field, value } => field_compare_expr(BinaryOp::Gte, field, value.clone()),
163 Self::EqField {
164 left_field,
165 right_field,
166 } => field_compare_field_expr(BinaryOp::Eq, left_field, right_field),
167 Self::NeField {
168 left_field,
169 right_field,
170 } => field_compare_field_expr(BinaryOp::Ne, left_field, right_field),
171 Self::LtField {
172 left_field,
173 right_field,
174 } => field_compare_field_expr(BinaryOp::Lt, left_field, right_field),
175 Self::LteField {
176 left_field,
177 right_field,
178 } => field_compare_field_expr(BinaryOp::Lte, left_field, right_field),
179 Self::GtField {
180 left_field,
181 right_field,
182 } => field_compare_field_expr(BinaryOp::Gt, left_field, right_field),
183 Self::GteField {
184 left_field,
185 right_field,
186 } => field_compare_field_expr(BinaryOp::Gte, left_field, right_field),
187 Self::In { field, values } => membership_expr(field, values.as_slice(), false),
188 Self::NotIn { field, values } => membership_expr(field, values.as_slice(), true),
189 Self::Contains { field, value } => Expr::FunctionCall {
190 function: Function::CollectionContains,
191 args: vec![
192 Expr::Field(FieldId::new(field.clone())),
193 Expr::Literal(value.clone()),
194 ],
195 },
196 Self::TextContains { field, value } => text_function_expr(
197 Function::Contains,
198 Expr::Field(FieldId::new(field.clone())),
199 value.clone(),
200 ),
201 Self::TextContainsCi { field, value } => text_function_expr(
202 Function::Contains,
203 casefold_field_expr(field),
204 value.clone(),
205 ),
206 Self::StartsWith { field, value } => text_function_expr(
207 Function::StartsWith,
208 Expr::Field(FieldId::new(field.clone())),
209 value.clone(),
210 ),
211 Self::StartsWithCi { field, value } => text_function_expr(
212 Function::StartsWith,
213 casefold_field_expr(field),
214 value.clone(),
215 ),
216 Self::EndsWith { field, value } => text_function_expr(
217 Function::EndsWith,
218 Expr::Field(FieldId::new(field.clone())),
219 value.clone(),
220 ),
221 Self::EndsWithCi { field, value } => text_function_expr(
222 Function::EndsWith,
223 casefold_field_expr(field),
224 value.clone(),
225 ),
226 Self::IsNull { field } => field_function_expr(Function::IsNull, field),
227 Self::IsNotNull { field } => field_function_expr(Function::IsNotNull, field),
228 Self::IsMissing { field } => field_function_expr(Function::IsMissing, field),
229 Self::IsEmpty { field } => field_function_expr(Function::IsEmpty, field),
230 Self::IsNotEmpty { field } => field_function_expr(Function::IsNotEmpty, field),
231 }
232 }
233
234 #[must_use]
236 pub const fn and(exprs: Vec<Self>) -> Self {
237 Self::And(exprs)
238 }
239
240 #[must_use]
242 pub const fn or(exprs: Vec<Self>) -> Self {
243 Self::Or(exprs)
244 }
245
246 #[must_use]
248 #[expect(clippy::should_implement_trait)]
249 pub fn not(expr: Self) -> Self {
250 Self::Not(Box::new(expr))
251 }
252
253 #[must_use]
255 pub fn eq(field: impl Into<String>, value: impl FieldValue) -> Self {
256 Self::Eq {
257 field: field.into(),
258 value: value.to_value(),
259 }
260 }
261
262 #[must_use]
264 pub fn ne(field: impl Into<String>, value: impl FieldValue) -> Self {
265 Self::Ne {
266 field: field.into(),
267 value: value.to_value(),
268 }
269 }
270
271 #[must_use]
273 pub fn lt(field: impl Into<String>, value: impl FieldValue) -> Self {
274 Self::Lt {
275 field: field.into(),
276 value: value.to_value(),
277 }
278 }
279
280 #[must_use]
282 pub fn lte(field: impl Into<String>, value: impl FieldValue) -> Self {
283 Self::Lte {
284 field: field.into(),
285 value: value.to_value(),
286 }
287 }
288
289 #[must_use]
291 pub fn gt(field: impl Into<String>, value: impl FieldValue) -> Self {
292 Self::Gt {
293 field: field.into(),
294 value: value.to_value(),
295 }
296 }
297
298 #[must_use]
300 pub fn gte(field: impl Into<String>, value: impl FieldValue) -> Self {
301 Self::Gte {
302 field: field.into(),
303 value: value.to_value(),
304 }
305 }
306
307 #[must_use]
309 pub fn eq_ci(field: impl Into<String>, value: impl FieldValue) -> Self {
310 Self::EqCi {
311 field: field.into(),
312 value: value.to_value(),
313 }
314 }
315
316 #[must_use]
318 pub fn eq_field(left_field: impl Into<String>, right_field: impl Into<String>) -> Self {
319 Self::EqField {
320 left_field: left_field.into(),
321 right_field: right_field.into(),
322 }
323 }
324
325 #[must_use]
327 pub fn ne_field(left_field: impl Into<String>, right_field: impl Into<String>) -> Self {
328 Self::NeField {
329 left_field: left_field.into(),
330 right_field: right_field.into(),
331 }
332 }
333
334 #[must_use]
336 pub fn lt_field(left_field: impl Into<String>, right_field: impl Into<String>) -> Self {
337 Self::LtField {
338 left_field: left_field.into(),
339 right_field: right_field.into(),
340 }
341 }
342
343 #[must_use]
345 pub fn lte_field(left_field: impl Into<String>, right_field: impl Into<String>) -> Self {
346 Self::LteField {
347 left_field: left_field.into(),
348 right_field: right_field.into(),
349 }
350 }
351
352 #[must_use]
354 pub fn gt_field(left_field: impl Into<String>, right_field: impl Into<String>) -> Self {
355 Self::GtField {
356 left_field: left_field.into(),
357 right_field: right_field.into(),
358 }
359 }
360
361 #[must_use]
363 pub fn gte_field(left_field: impl Into<String>, right_field: impl Into<String>) -> Self {
364 Self::GteField {
365 left_field: left_field.into(),
366 right_field: right_field.into(),
367 }
368 }
369
370 #[must_use]
372 pub fn in_list(
373 field: impl Into<String>,
374 values: impl IntoIterator<Item = impl FieldValue>,
375 ) -> Self {
376 Self::In {
377 field: field.into(),
378 values: values.into_iter().map(|value| value.to_value()).collect(),
379 }
380 }
381
382 #[must_use]
384 pub fn not_in(
385 field: impl Into<String>,
386 values: impl IntoIterator<Item = impl FieldValue>,
387 ) -> Self {
388 Self::NotIn {
389 field: field.into(),
390 values: values.into_iter().map(|value| value.to_value()).collect(),
391 }
392 }
393
394 #[must_use]
396 pub fn contains(field: impl Into<String>, value: impl FieldValue) -> Self {
397 Self::Contains {
398 field: field.into(),
399 value: value.to_value(),
400 }
401 }
402
403 #[must_use]
405 pub fn text_contains(field: impl Into<String>, value: impl FieldValue) -> Self {
406 Self::TextContains {
407 field: field.into(),
408 value: value.to_value(),
409 }
410 }
411
412 #[must_use]
414 pub fn text_contains_ci(field: impl Into<String>, value: impl FieldValue) -> Self {
415 Self::TextContainsCi {
416 field: field.into(),
417 value: value.to_value(),
418 }
419 }
420
421 #[must_use]
423 pub fn starts_with(field: impl Into<String>, value: impl FieldValue) -> Self {
424 Self::StartsWith {
425 field: field.into(),
426 value: value.to_value(),
427 }
428 }
429
430 #[must_use]
432 pub fn starts_with_ci(field: impl Into<String>, value: impl FieldValue) -> Self {
433 Self::StartsWithCi {
434 field: field.into(),
435 value: value.to_value(),
436 }
437 }
438
439 #[must_use]
441 pub fn ends_with(field: impl Into<String>, value: impl FieldValue) -> Self {
442 Self::EndsWith {
443 field: field.into(),
444 value: value.to_value(),
445 }
446 }
447
448 #[must_use]
450 pub fn ends_with_ci(field: impl Into<String>, value: impl FieldValue) -> Self {
451 Self::EndsWithCi {
452 field: field.into(),
453 value: value.to_value(),
454 }
455 }
456
457 #[must_use]
459 pub fn is_null(field: impl Into<String>) -> Self {
460 Self::IsNull {
461 field: field.into(),
462 }
463 }
464
465 #[must_use]
467 pub fn is_not_null(field: impl Into<String>) -> Self {
468 Self::IsNotNull {
469 field: field.into(),
470 }
471 }
472
473 #[must_use]
475 pub fn is_missing(field: impl Into<String>) -> Self {
476 Self::IsMissing {
477 field: field.into(),
478 }
479 }
480
481 #[must_use]
483 pub fn is_empty(field: impl Into<String>) -> Self {
484 Self::IsEmpty {
485 field: field.into(),
486 }
487 }
488
489 #[must_use]
491 pub fn is_not_empty(field: impl Into<String>) -> Self {
492 Self::IsNotEmpty {
493 field: field.into(),
494 }
495 }
496}
497
498fn fold_filter_bool_chain(op: BinaryOp, exprs: &[FilterExpr]) -> Expr {
499 let mut exprs = exprs.iter();
500 let Some(first) = exprs.next() else {
501 return Expr::Literal(Value::Bool(matches!(op, BinaryOp::And)));
502 };
503
504 let first = first.lower_bool_expr();
505
506 exprs.fold(first, |left, expr| Expr::Binary {
507 op,
508 left: Box::new(left),
509 right: Box::new(expr.lower_bool_expr()),
510 })
511}
512
513fn field_compare_expr(op: BinaryOp, field: &str, value: Value) -> Expr {
514 Expr::Binary {
515 op,
516 left: Box::new(Expr::Field(FieldId::new(field.to_string()))),
517 right: Box::new(Expr::Literal(value)),
518 }
519}
520
521fn field_compare_field_expr(op: BinaryOp, left_field: &str, right_field: &str) -> Expr {
522 Expr::Binary {
523 op,
524 left: Box::new(Expr::Field(FieldId::new(left_field.to_string()))),
525 right: Box::new(Expr::Field(FieldId::new(right_field.to_string()))),
526 }
527}
528
529fn membership_expr(field: &str, values: &[Value], negated: bool) -> Expr {
530 let compare_op = if negated { BinaryOp::Ne } else { BinaryOp::Eq };
531 let join_op = if negated { BinaryOp::And } else { BinaryOp::Or };
532 let mut values = values.iter();
533 let Some(first) = values.next() else {
534 return Expr::Literal(Value::Bool(negated));
535 };
536
537 let field = Expr::Field(FieldId::new(field.to_string()));
538 let mut expr = Expr::Binary {
539 op: compare_op,
540 left: Box::new(field.clone()),
541 right: Box::new(Expr::Literal(first.clone())),
542 };
543
544 for value in values {
545 expr = Expr::Binary {
546 op: join_op,
547 left: Box::new(expr),
548 right: Box::new(Expr::Binary {
549 op: compare_op,
550 left: Box::new(field.clone()),
551 right: Box::new(Expr::Literal(value.clone())),
552 }),
553 };
554 }
555
556 expr
557}
558
559fn field_function_expr(function: Function, field: &str) -> Expr {
560 Expr::FunctionCall {
561 function,
562 args: vec![Expr::Field(FieldId::new(field.to_string()))],
563 }
564}
565
566fn text_function_expr(function: Function, left: Expr, value: Value) -> Expr {
567 Expr::FunctionCall {
568 function,
569 args: vec![left, Expr::Literal(value)],
570 }
571}
572
573fn casefold_field_expr(field: &str) -> Expr {
574 Expr::FunctionCall {
575 function: Function::Lower,
576 args: vec![Expr::Field(FieldId::new(field.to_string()))],
577 }
578}
579
580#[derive(Clone, Debug, Eq, PartialEq)]
589pub struct OrderExpr {
590 expr: Expr,
591}
592
593impl OrderExpr {
594 #[must_use]
596 pub fn field(field: impl Into<String>) -> Self {
597 let field = field.into();
598
599 Self {
600 expr: Expr::Field(FieldId::new(field)),
601 }
602 }
603
604 const fn new(expr: Expr) -> Self {
608 Self { expr }
609 }
610
611 pub(in crate::db) fn lower(&self, direction: OrderDirection) -> PlannedOrderTerm {
614 PlannedOrderTerm::new(self.expr.clone(), direction)
615 }
616}
617
618impl From<&str> for OrderExpr {
619 fn from(value: &str) -> Self {
620 Self::field(value)
621 }
622}
623
624impl From<String> for OrderExpr {
625 fn from(value: String) -> Self {
626 Self::field(value)
627 }
628}
629
630impl From<FieldRef> for OrderExpr {
631 fn from(value: FieldRef) -> Self {
632 Self::field(value.as_str())
633 }
634}
635
636impl From<TextProjectionExpr> for OrderExpr {
637 fn from(value: TextProjectionExpr) -> Self {
638 Self::new(value.expr().clone())
639 }
640}
641
642impl From<NumericProjectionExpr> for OrderExpr {
643 fn from(value: NumericProjectionExpr) -> Self {
644 Self::new(value.expr().clone())
645 }
646}
647
648impl From<RoundProjectionExpr> for OrderExpr {
649 fn from(value: RoundProjectionExpr) -> Self {
650 Self::new(value.expr().clone())
651 }
652}
653
654impl From<AggregateExpr> for OrderExpr {
655 fn from(value: AggregateExpr) -> Self {
656 Self::new(Expr::Aggregate(value))
657 }
658}
659
660#[derive(Clone, Debug, Eq, PartialEq)]
669pub struct OrderTerm {
670 expr: OrderExpr,
671 direction: OrderDirection,
672}
673
674impl OrderTerm {
675 #[must_use]
677 pub fn asc(expr: impl Into<OrderExpr>) -> Self {
678 Self {
679 expr: expr.into(),
680 direction: OrderDirection::Asc,
681 }
682 }
683
684 #[must_use]
686 pub fn desc(expr: impl Into<OrderExpr>) -> Self {
687 Self {
688 expr: expr.into(),
689 direction: OrderDirection::Desc,
690 }
691 }
692
693 pub(in crate::db) fn lower(&self) -> PlannedOrderTerm {
696 self.expr.lower(self.direction)
697 }
698}
699
700#[must_use]
702pub fn field(field: impl Into<String>) -> OrderExpr {
703 OrderExpr::field(field)
704}
705
706#[must_use]
708pub fn asc(expr: impl Into<OrderExpr>) -> OrderTerm {
709 OrderTerm::asc(expr)
710}
711
712#[must_use]
714pub fn desc(expr: impl Into<OrderExpr>) -> OrderTerm {
715 OrderTerm::desc(expr)
716}
717
718#[cfg(test)]
723mod tests {
724 use super::FilterExpr;
725 use candid::types::{CandidType, Label, Type, TypeInner};
726
727 fn expect_record_fields(ty: Type) -> Vec<String> {
728 match ty.as_ref() {
729 TypeInner::Record(fields) => fields
730 .iter()
731 .map(|field| match field.id.as_ref() {
732 Label::Named(name) => name.clone(),
733 other => panic!("expected named record field, got {other:?}"),
734 })
735 .collect(),
736 other => panic!("expected candid record, got {other:?}"),
737 }
738 }
739
740 fn expect_variant_labels(ty: Type) -> Vec<String> {
741 match ty.as_ref() {
742 TypeInner::Variant(fields) => fields
743 .iter()
744 .map(|field| match field.id.as_ref() {
745 Label::Named(name) => name.clone(),
746 other => panic!("expected named variant label, got {other:?}"),
747 })
748 .collect(),
749 other => panic!("expected candid variant, got {other:?}"),
750 }
751 }
752
753 fn expect_variant_field_type(ty: Type, variant_name: &str) -> Type {
754 match ty.as_ref() {
755 TypeInner::Variant(fields) => fields
756 .iter()
757 .find_map(|field| match field.id.as_ref() {
758 Label::Named(name) if name == variant_name => Some(field.ty.clone()),
759 _ => None,
760 })
761 .unwrap_or_else(|| panic!("expected variant label `{variant_name}`")),
762 other => panic!("expected candid variant, got {other:?}"),
763 }
764 }
765
766 #[test]
767 fn filter_expr_eq_candid_payload_shape_is_stable() {
768 let fields = expect_record_fields(expect_variant_field_type(FilterExpr::ty(), "Eq"));
769
770 for field in ["field", "value"] {
771 assert!(
772 fields.iter().any(|candidate| candidate == field),
773 "Eq payload must keep `{field}` field key in Candid shape",
774 );
775 }
776 }
777
778 #[test]
779 fn filter_expr_and_candid_payload_shape_is_stable() {
780 match expect_variant_field_type(FilterExpr::ty(), "And").as_ref() {
781 TypeInner::Vec(_) => {}
782 other => panic!("And payload must remain a Candid vec payload, got {other:?}"),
783 }
784 }
785
786 #[test]
787 fn filter_expr_text_contains_ci_candid_payload_shape_is_stable() {
788 let fields = expect_record_fields(expect_variant_field_type(
789 FilterExpr::ty(),
790 "TextContainsCi",
791 ));
792
793 for field in ["field", "value"] {
794 assert!(
795 fields.iter().any(|candidate| candidate == field),
796 "TextContainsCi payload must keep `{field}` field key in Candid shape",
797 );
798 }
799 }
800
801 #[test]
802 fn filter_expr_not_payload_shape_is_stable() {
803 match expect_variant_field_type(FilterExpr::ty(), "Not").as_ref() {
804 TypeInner::Var(_) | TypeInner::Knot(_) | TypeInner::Variant(_) => {}
805 other => panic!("Not payload must keep nested predicate payload, got {other:?}"),
806 }
807 }
808
809 #[test]
810 fn filter_expr_variant_labels_are_stable() {
811 let labels = expect_variant_labels(FilterExpr::ty());
812
813 for label in ["Eq", "And", "Not", "TextContainsCi", "IsMissing"] {
814 assert!(
815 labels.iter().any(|candidate| candidate == label),
816 "FilterExpr must keep `{label}` variant label",
817 );
818 }
819 }
820
821 #[test]
822 fn query_expr_fixture_constructors_stay_usable() {
823 let expr = FilterExpr::and(vec![
824 FilterExpr::is_null("deleted_at"),
825 FilterExpr::not(FilterExpr::is_missing("name")),
826 ]);
827
828 match expr {
829 FilterExpr::And(items) => assert_eq!(items.len(), 2),
830 other => panic!("expected And fixture, got {other:?}"),
831 }
832 }
833}