Skip to main content

supabase_client_query/
filter.rs

1use crate::sql::{
2    ArrayRangeOperator, FilterCondition, FilterOperator, IntoSqlParam, IsValue, ParamStore,
3    PatternOperator, TextSearchType, validate_column_name,
4};
5/// Trait providing all filter methods for query builders.
6///
7/// Implementors must provide access to the internal filter list and param store.
8pub trait Filterable: Sized {
9    /// Get a mutable reference to the filter list.
10    fn filters_mut(&mut self) -> &mut Vec<FilterCondition>;
11    /// Get a mutable reference to the parameter store.
12    fn params_mut(&mut self) -> &mut ParamStore;
13
14    /// Filter: column = value
15    fn eq(mut self, column: &str, value: impl IntoSqlParam) -> Self {
16        if let Err(e) = validate_column_name(column) {
17            tracing::error!("Invalid column name in eq filter: {e}");
18            return self;
19        }
20        let idx = self.params_mut().push_value(value);
21        self.filters_mut().push(FilterCondition::Comparison {
22            column: column.to_string(),
23            operator: FilterOperator::Eq,
24            param_index: idx,
25        });
26        self
27    }
28
29    /// Filter: column != value
30    fn neq(mut self, column: &str, value: impl IntoSqlParam) -> Self {
31        if let Err(e) = validate_column_name(column) {
32            tracing::error!("Invalid column name in neq filter: {e}");
33            return self;
34        }
35        let idx = self.params_mut().push_value(value);
36        self.filters_mut().push(FilterCondition::Comparison {
37            column: column.to_string(),
38            operator: FilterOperator::Neq,
39            param_index: idx,
40        });
41        self
42    }
43
44    /// Filter: column > value
45    fn gt(mut self, column: &str, value: impl IntoSqlParam) -> Self {
46        if let Err(e) = validate_column_name(column) {
47            tracing::error!("Invalid column name in gt filter: {e}");
48            return self;
49        }
50        let idx = self.params_mut().push_value(value);
51        self.filters_mut().push(FilterCondition::Comparison {
52            column: column.to_string(),
53            operator: FilterOperator::Gt,
54            param_index: idx,
55        });
56        self
57    }
58
59    /// Filter: column >= value
60    fn gte(mut self, column: &str, value: impl IntoSqlParam) -> Self {
61        if let Err(e) = validate_column_name(column) {
62            tracing::error!("Invalid column name in gte filter: {e}");
63            return self;
64        }
65        let idx = self.params_mut().push_value(value);
66        self.filters_mut().push(FilterCondition::Comparison {
67            column: column.to_string(),
68            operator: FilterOperator::Gte,
69            param_index: idx,
70        });
71        self
72    }
73
74    /// Filter: column < value
75    fn lt(mut self, column: &str, value: impl IntoSqlParam) -> Self {
76        if let Err(e) = validate_column_name(column) {
77            tracing::error!("Invalid column name in lt filter: {e}");
78            return self;
79        }
80        let idx = self.params_mut().push_value(value);
81        self.filters_mut().push(FilterCondition::Comparison {
82            column: column.to_string(),
83            operator: FilterOperator::Lt,
84            param_index: idx,
85        });
86        self
87    }
88
89    /// Filter: column <= value
90    fn lte(mut self, column: &str, value: impl IntoSqlParam) -> Self {
91        if let Err(e) = validate_column_name(column) {
92            tracing::error!("Invalid column name in lte filter: {e}");
93            return self;
94        }
95        let idx = self.params_mut().push_value(value);
96        self.filters_mut().push(FilterCondition::Comparison {
97            column: column.to_string(),
98            operator: FilterOperator::Lte,
99            param_index: idx,
100        });
101        self
102    }
103
104    /// Filter: column LIKE pattern
105    fn like(mut self, column: &str, pattern: impl IntoSqlParam) -> Self {
106        if let Err(e) = validate_column_name(column) {
107            tracing::error!("Invalid column name in like filter: {e}");
108            return self;
109        }
110        let idx = self.params_mut().push_value(pattern);
111        self.filters_mut().push(FilterCondition::Pattern {
112            column: column.to_string(),
113            operator: PatternOperator::Like,
114            param_index: idx,
115        });
116        self
117    }
118
119    /// Filter: column ILIKE pattern (case-insensitive)
120    fn ilike(mut self, column: &str, pattern: impl IntoSqlParam) -> Self {
121        if let Err(e) = validate_column_name(column) {
122            tracing::error!("Invalid column name in ilike filter: {e}");
123            return self;
124        }
125        let idx = self.params_mut().push_value(pattern);
126        self.filters_mut().push(FilterCondition::Pattern {
127            column: column.to_string(),
128            operator: PatternOperator::ILike,
129            param_index: idx,
130        });
131        self
132    }
133
134    /// Filter: column IS NULL / IS NOT NULL / IS TRUE / IS FALSE
135    fn is(mut self, column: &str, value: IsValue) -> Self {
136        if let Err(e) = validate_column_name(column) {
137            tracing::error!("Invalid column name in is filter: {e}");
138            return self;
139        }
140        self.filters_mut().push(FilterCondition::Is {
141            column: column.to_string(),
142            value,
143        });
144        self
145    }
146
147    /// Filter: column IN (val1, val2, ...)
148    fn in_<V: IntoSqlParam>(mut self, column: &str, values: Vec<V>) -> Self {
149        if let Err(e) = validate_column_name(column) {
150            tracing::error!("Invalid column name in in_ filter: {e}");
151            return self;
152        }
153        let indices: Vec<usize> = values
154            .into_iter()
155            .map(|v| self.params_mut().push_value(v))
156            .collect();
157        self.filters_mut().push(FilterCondition::In {
158            column: column.to_string(),
159            param_indices: indices,
160        });
161        self
162    }
163
164    /// Filter: column @> value (contains)
165    fn contains(mut self, column: &str, value: impl IntoSqlParam) -> Self {
166        if let Err(e) = validate_column_name(column) {
167            tracing::error!("Invalid column name in contains filter: {e}");
168            return self;
169        }
170        let idx = self.params_mut().push_value(value);
171        self.filters_mut().push(FilterCondition::ArrayRange {
172            column: column.to_string(),
173            operator: ArrayRangeOperator::Contains,
174            param_index: idx,
175        });
176        self
177    }
178
179    /// Filter: column <@ value (contained by)
180    fn contained_by(mut self, column: &str, value: impl IntoSqlParam) -> Self {
181        if let Err(e) = validate_column_name(column) {
182            tracing::error!("Invalid column name in contained_by filter: {e}");
183            return self;
184        }
185        let idx = self.params_mut().push_value(value);
186        self.filters_mut().push(FilterCondition::ArrayRange {
187            column: column.to_string(),
188            operator: ArrayRangeOperator::ContainedBy,
189            param_index: idx,
190        });
191        self
192    }
193
194    /// Filter: column && value (overlaps)
195    fn overlaps(mut self, column: &str, value: impl IntoSqlParam) -> Self {
196        if let Err(e) = validate_column_name(column) {
197            tracing::error!("Invalid column name in overlaps filter: {e}");
198            return self;
199        }
200        let idx = self.params_mut().push_value(value);
201        self.filters_mut().push(FilterCondition::ArrayRange {
202            column: column.to_string(),
203            operator: ArrayRangeOperator::Overlaps,
204            param_index: idx,
205        });
206        self
207    }
208
209    /// Filter: column >> value (range strictly greater than)
210    fn range_gt(mut self, column: &str, value: impl IntoSqlParam) -> Self {
211        if let Err(e) = validate_column_name(column) {
212            tracing::error!("Invalid column name in range_gt filter: {e}");
213            return self;
214        }
215        let idx = self.params_mut().push_value(value);
216        self.filters_mut().push(FilterCondition::ArrayRange {
217            column: column.to_string(),
218            operator: ArrayRangeOperator::RangeGt,
219            param_index: idx,
220        });
221        self
222    }
223
224    /// Filter: column &> value (range greater than or equal)
225    fn range_gte(mut self, column: &str, value: impl IntoSqlParam) -> Self {
226        if let Err(e) = validate_column_name(column) {
227            tracing::error!("Invalid column name in range_gte filter: {e}");
228            return self;
229        }
230        let idx = self.params_mut().push_value(value);
231        self.filters_mut().push(FilterCondition::ArrayRange {
232            column: column.to_string(),
233            operator: ArrayRangeOperator::RangeGte,
234            param_index: idx,
235        });
236        self
237    }
238
239    /// Filter: column << value (range strictly less than)
240    fn range_lt(mut self, column: &str, value: impl IntoSqlParam) -> Self {
241        if let Err(e) = validate_column_name(column) {
242            tracing::error!("Invalid column name in range_lt filter: {e}");
243            return self;
244        }
245        let idx = self.params_mut().push_value(value);
246        self.filters_mut().push(FilterCondition::ArrayRange {
247            column: column.to_string(),
248            operator: ArrayRangeOperator::RangeLt,
249            param_index: idx,
250        });
251        self
252    }
253
254    /// Filter: column &< value (range less than or equal)
255    fn range_lte(mut self, column: &str, value: impl IntoSqlParam) -> Self {
256        if let Err(e) = validate_column_name(column) {
257            tracing::error!("Invalid column name in range_lte filter: {e}");
258            return self;
259        }
260        let idx = self.params_mut().push_value(value);
261        self.filters_mut().push(FilterCondition::ArrayRange {
262            column: column.to_string(),
263            operator: ArrayRangeOperator::RangeLte,
264            param_index: idx,
265        });
266        self
267    }
268
269    /// Filter: column -|- value (range adjacent)
270    fn range_adjacent(mut self, column: &str, value: impl IntoSqlParam) -> Self {
271        if let Err(e) = validate_column_name(column) {
272            tracing::error!("Invalid column name in range_adjacent filter: {e}");
273            return self;
274        }
275        let idx = self.params_mut().push_value(value);
276        self.filters_mut().push(FilterCondition::ArrayRange {
277            column: column.to_string(),
278            operator: ArrayRangeOperator::RangeAdjacent,
279            param_index: idx,
280        });
281        self
282    }
283
284    /// Full-text search filter.
285    fn text_search(
286        mut self,
287        column: &str,
288        query: impl IntoSqlParam,
289        search_type: TextSearchType,
290        config: Option<&str>,
291    ) -> Self {
292        if let Err(e) = validate_column_name(column) {
293            tracing::error!("Invalid column name in text_search filter: {e}");
294            return self;
295        }
296        let idx = self.params_mut().push_value(query);
297        self.filters_mut().push(FilterCondition::TextSearch {
298            column: column.to_string(),
299            query_param_index: idx,
300            config: config.map(|s| s.to_string()),
301            search_type,
302        });
303        self
304    }
305
306    /// Negate a filter condition using a closure.
307    fn not(mut self, f: impl FnOnce(FilterCollector) -> FilterCollector) -> Self {
308        let collector = f(FilterCollector::new(self.params_mut()));
309        if let Some(condition) = collector.into_single_condition() {
310            self.filters_mut().push(FilterCondition::Not(Box::new(condition)));
311        }
312        self
313    }
314
315    /// OR filter: combine multiple conditions with OR.
316    fn or_filter(mut self, f: impl FnOnce(FilterCollector) -> FilterCollector) -> Self {
317        let collector = f(FilterCollector::new(self.params_mut()));
318        let conditions = collector.into_conditions();
319        if !conditions.is_empty() {
320            self.filters_mut().push(FilterCondition::Or(conditions));
321        }
322        self
323    }
324
325    /// Match multiple column=value pairs (all must match).
326    fn match_filter(mut self, pairs: Vec<(&str, impl IntoSqlParam + Clone)>) -> Self {
327        let conditions: Vec<(String, usize)> = pairs
328            .into_iter()
329            .filter_map(|(col, val)| {
330                if let Err(e) = validate_column_name(col) {
331                    tracing::error!("Invalid column name in match_filter: {e}");
332                    return None;
333                }
334                let idx = self.params_mut().push_value(val);
335                Some((col.to_string(), idx))
336            })
337            .collect();
338        if !conditions.is_empty() {
339            self.filters_mut().push(FilterCondition::Match { conditions });
340        }
341        self
342    }
343
344    /// Raw filter escape hatch. The string should be a valid SQL boolean expression.
345    fn filter(mut self, raw_sql: &str) -> Self {
346        self.filters_mut()
347            .push(FilterCondition::Raw(raw_sql.to_string()));
348        self
349    }
350}
351
352/// Temporary collector used in closures for `not()` and `or_filter()`.
353pub struct FilterCollector<'a> {
354    filters: Vec<FilterCondition>,
355    params: &'a mut ParamStore,
356}
357
358impl<'a> FilterCollector<'a> {
359    pub fn new(params: &'a mut ParamStore) -> Self {
360        Self {
361            filters: Vec::new(),
362            params,
363        }
364    }
365
366    pub fn eq(mut self, column: &str, value: impl IntoSqlParam) -> Self {
367        if validate_column_name(column).is_ok() {
368            let idx = self.params.push_value(value);
369            self.filters.push(FilterCondition::Comparison {
370                column: column.to_string(),
371                operator: FilterOperator::Eq,
372                param_index: idx,
373            });
374        }
375        self
376    }
377
378    pub fn neq(mut self, column: &str, value: impl IntoSqlParam) -> Self {
379        if validate_column_name(column).is_ok() {
380            let idx = self.params.push_value(value);
381            self.filters.push(FilterCondition::Comparison {
382                column: column.to_string(),
383                operator: FilterOperator::Neq,
384                param_index: idx,
385            });
386        }
387        self
388    }
389
390    pub fn gt(mut self, column: &str, value: impl IntoSqlParam) -> Self {
391        if validate_column_name(column).is_ok() {
392            let idx = self.params.push_value(value);
393            self.filters.push(FilterCondition::Comparison {
394                column: column.to_string(),
395                operator: FilterOperator::Gt,
396                param_index: idx,
397            });
398        }
399        self
400    }
401
402    pub fn gte(mut self, column: &str, value: impl IntoSqlParam) -> Self {
403        if validate_column_name(column).is_ok() {
404            let idx = self.params.push_value(value);
405            self.filters.push(FilterCondition::Comparison {
406                column: column.to_string(),
407                operator: FilterOperator::Gte,
408                param_index: idx,
409            });
410        }
411        self
412    }
413
414    pub fn lt(mut self, column: &str, value: impl IntoSqlParam) -> Self {
415        if validate_column_name(column).is_ok() {
416            let idx = self.params.push_value(value);
417            self.filters.push(FilterCondition::Comparison {
418                column: column.to_string(),
419                operator: FilterOperator::Lt,
420                param_index: idx,
421            });
422        }
423        self
424    }
425
426    pub fn lte(mut self, column: &str, value: impl IntoSqlParam) -> Self {
427        if validate_column_name(column).is_ok() {
428            let idx = self.params.push_value(value);
429            self.filters.push(FilterCondition::Comparison {
430                column: column.to_string(),
431                operator: FilterOperator::Lte,
432                param_index: idx,
433            });
434        }
435        self
436    }
437
438    pub fn like(mut self, column: &str, pattern: impl IntoSqlParam) -> Self {
439        if validate_column_name(column).is_ok() {
440            let idx = self.params.push_value(pattern);
441            self.filters.push(FilterCondition::Pattern {
442                column: column.to_string(),
443                operator: PatternOperator::Like,
444                param_index: idx,
445            });
446        }
447        self
448    }
449
450    pub fn ilike(mut self, column: &str, pattern: impl IntoSqlParam) -> Self {
451        if validate_column_name(column).is_ok() {
452            let idx = self.params.push_value(pattern);
453            self.filters.push(FilterCondition::Pattern {
454                column: column.to_string(),
455                operator: PatternOperator::ILike,
456                param_index: idx,
457            });
458        }
459        self
460    }
461
462    pub fn is(mut self, column: &str, value: IsValue) -> Self {
463        if validate_column_name(column).is_ok() {
464            self.filters.push(FilterCondition::Is {
465                column: column.to_string(),
466                value,
467            });
468        }
469        self
470    }
471
472    pub fn into_conditions(self) -> Vec<FilterCondition> {
473        self.filters
474    }
475
476    pub fn into_single_condition(self) -> Option<FilterCondition> {
477        let mut filters = self.filters;
478        if filters.len() == 1 {
479            Some(filters.remove(0))
480        } else if filters.is_empty() {
481            None
482        } else {
483            Some(FilterCondition::And(filters))
484        }
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use crate::backend::QueryBackend;
492    use crate::select::SelectBuilder;
493    use crate::sql::*;
494    use std::marker::PhantomData;
495    use std::sync::Arc;
496
497    fn make_select() -> SelectBuilder<supabase_client_core::Row> {
498        SelectBuilder {
499            backend: QueryBackend::Rest {
500                http: reqwest::Client::new(),
501                base_url: Arc::from("http://localhost"),
502                api_key: Arc::from("key"),
503                schema: "public".to_string(),
504            },
505            parts: SqlParts::new(SqlOperation::Select, "public", "test"),
506            params: ParamStore::new(),
507            _marker: PhantomData,
508        }
509    }
510
511    #[test]
512    fn test_eq_adds_comparison_filter() {
513        let builder = make_select().eq("name", "Alice");
514        assert_eq!(builder.parts.filters.len(), 1);
515        match &builder.parts.filters[0] {
516            FilterCondition::Comparison { column, operator, param_index } => {
517                assert_eq!(column, "name");
518                assert_eq!(*operator, FilterOperator::Eq);
519                assert_eq!(*param_index, 1);
520            }
521            _ => panic!("expected Comparison filter"),
522        }
523    }
524
525    #[test]
526    fn test_neq_adds_comparison_filter() {
527        let builder = make_select().neq("status", "inactive");
528        assert_eq!(builder.parts.filters.len(), 1);
529        match &builder.parts.filters[0] {
530            FilterCondition::Comparison { column, operator, .. } => {
531                assert_eq!(column, "status");
532                assert_eq!(*operator, FilterOperator::Neq);
533            }
534            _ => panic!("expected Comparison filter"),
535        }
536    }
537
538    #[test]
539    fn test_gt_adds_comparison_filter() {
540        let builder = make_select().gt("age", 18i32);
541        match &builder.parts.filters[0] {
542            FilterCondition::Comparison { column, operator, .. } => {
543                assert_eq!(column, "age");
544                assert_eq!(*operator, FilterOperator::Gt);
545            }
546            _ => panic!("expected Comparison filter"),
547        }
548    }
549
550    #[test]
551    fn test_gte_adds_comparison_filter() {
552        let builder = make_select().gte("score", 90i32);
553        match &builder.parts.filters[0] {
554            FilterCondition::Comparison { column, operator, .. } => {
555                assert_eq!(column, "score");
556                assert_eq!(*operator, FilterOperator::Gte);
557            }
558            _ => panic!("expected Comparison filter"),
559        }
560    }
561
562    #[test]
563    fn test_lt_adds_comparison_filter() {
564        let builder = make_select().lt("price", 100i32);
565        match &builder.parts.filters[0] {
566            FilterCondition::Comparison { column, operator, .. } => {
567                assert_eq!(column, "price");
568                assert_eq!(*operator, FilterOperator::Lt);
569            }
570            _ => panic!("expected Comparison filter"),
571        }
572    }
573
574    #[test]
575    fn test_lte_adds_comparison_filter() {
576        let builder = make_select().lte("count", 50i32);
577        match &builder.parts.filters[0] {
578            FilterCondition::Comparison { column, operator, .. } => {
579                assert_eq!(column, "count");
580                assert_eq!(*operator, FilterOperator::Lte);
581            }
582            _ => panic!("expected Comparison filter"),
583        }
584    }
585
586    #[test]
587    fn test_like_adds_pattern_filter() {
588        let builder = make_select().like("name", "%test%");
589        assert_eq!(builder.parts.filters.len(), 1);
590        match &builder.parts.filters[0] {
591            FilterCondition::Pattern { column, operator, .. } => {
592                assert_eq!(column, "name");
593                assert_eq!(*operator, PatternOperator::Like);
594            }
595            _ => panic!("expected Pattern filter"),
596        }
597    }
598
599    #[test]
600    fn test_ilike_adds_pattern_filter() {
601        let builder = make_select().ilike("name", "%TEST%");
602        match &builder.parts.filters[0] {
603            FilterCondition::Pattern { column, operator, .. } => {
604                assert_eq!(column, "name");
605                assert_eq!(*operator, PatternOperator::ILike);
606            }
607            _ => panic!("expected Pattern filter"),
608        }
609    }
610
611    #[test]
612    fn test_is_null() {
613        let builder = make_select().is("deleted_at", IsValue::Null);
614        match &builder.parts.filters[0] {
615            FilterCondition::Is { column, value } => {
616                assert_eq!(column, "deleted_at");
617                assert_eq!(*value, IsValue::Null);
618            }
619            _ => panic!("expected Is filter"),
620        }
621    }
622
623    #[test]
624    fn test_is_not_null() {
625        let builder = make_select().is("name", IsValue::NotNull);
626        match &builder.parts.filters[0] {
627            FilterCondition::Is { value, .. } => assert_eq!(*value, IsValue::NotNull),
628            _ => panic!("expected Is filter"),
629        }
630    }
631
632    #[test]
633    fn test_is_true() {
634        let builder = make_select().is("active", IsValue::True);
635        match &builder.parts.filters[0] {
636            FilterCondition::Is { value, .. } => assert_eq!(*value, IsValue::True),
637            _ => panic!("expected Is filter"),
638        }
639    }
640
641    #[test]
642    fn test_is_false() {
643        let builder = make_select().is("active", IsValue::False);
644        match &builder.parts.filters[0] {
645            FilterCondition::Is { value, .. } => assert_eq!(*value, IsValue::False),
646            _ => panic!("expected Is filter"),
647        }
648    }
649
650    #[test]
651    fn test_in_with_values() {
652        let builder = make_select().in_("id", vec![1i32, 2, 3]);
653        assert_eq!(builder.parts.filters.len(), 1);
654        match &builder.parts.filters[0] {
655            FilterCondition::In { column, param_indices } => {
656                assert_eq!(column, "id");
657                assert_eq!(param_indices.len(), 3);
658                assert_eq!(param_indices[0], 1);
659                assert_eq!(param_indices[1], 2);
660                assert_eq!(param_indices[2], 3);
661            }
662            _ => panic!("expected In filter"),
663        }
664    }
665
666    #[test]
667    fn test_contains_adds_array_range_filter() {
668        let builder = make_select().contains("tags", "rust");
669        match &builder.parts.filters[0] {
670            FilterCondition::ArrayRange { column, operator, .. } => {
671                assert_eq!(column, "tags");
672                assert_eq!(*operator, ArrayRangeOperator::Contains);
673            }
674            _ => panic!("expected ArrayRange filter"),
675        }
676    }
677
678    #[test]
679    fn test_contained_by_adds_array_range_filter() {
680        let builder = make_select().contained_by("tags", "all_tags");
681        match &builder.parts.filters[0] {
682            FilterCondition::ArrayRange { column, operator, .. } => {
683                assert_eq!(column, "tags");
684                assert_eq!(*operator, ArrayRangeOperator::ContainedBy);
685            }
686            _ => panic!("expected ArrayRange filter"),
687        }
688    }
689
690    #[test]
691    fn test_overlaps_adds_array_range_filter() {
692        let builder = make_select().overlaps("tags", "some_tags");
693        match &builder.parts.filters[0] {
694            FilterCondition::ArrayRange { column, operator, .. } => {
695                assert_eq!(column, "tags");
696                assert_eq!(*operator, ArrayRangeOperator::Overlaps);
697            }
698            _ => panic!("expected ArrayRange filter"),
699        }
700    }
701
702    #[test]
703    fn test_range_gt() {
704        let builder = make_select().range_gt("period", "[2024-01-01,2024-12-31]");
705        match &builder.parts.filters[0] {
706            FilterCondition::ArrayRange { operator, .. } => {
707                assert_eq!(*operator, ArrayRangeOperator::RangeGt);
708            }
709            _ => panic!("expected ArrayRange filter"),
710        }
711    }
712
713    #[test]
714    fn test_range_gte() {
715        let builder = make_select().range_gte("period", "[2024-01-01,2024-12-31]");
716        match &builder.parts.filters[0] {
717            FilterCondition::ArrayRange { operator, .. } => {
718                assert_eq!(*operator, ArrayRangeOperator::RangeGte);
719            }
720            _ => panic!("expected ArrayRange filter"),
721        }
722    }
723
724    #[test]
725    fn test_range_lt() {
726        let builder = make_select().range_lt("period", "[2024-01-01,2024-12-31]");
727        match &builder.parts.filters[0] {
728            FilterCondition::ArrayRange { operator, .. } => {
729                assert_eq!(*operator, ArrayRangeOperator::RangeLt);
730            }
731            _ => panic!("expected ArrayRange filter"),
732        }
733    }
734
735    #[test]
736    fn test_range_lte() {
737        let builder = make_select().range_lte("period", "[2024-01-01,2024-12-31]");
738        match &builder.parts.filters[0] {
739            FilterCondition::ArrayRange { operator, .. } => {
740                assert_eq!(*operator, ArrayRangeOperator::RangeLte);
741            }
742            _ => panic!("expected ArrayRange filter"),
743        }
744    }
745
746    #[test]
747    fn test_range_adjacent() {
748        let builder = make_select().range_adjacent("period", "[2024-01-01,2024-12-31]");
749        match &builder.parts.filters[0] {
750            FilterCondition::ArrayRange { operator, .. } => {
751                assert_eq!(*operator, ArrayRangeOperator::RangeAdjacent);
752            }
753            _ => panic!("expected ArrayRange filter"),
754        }
755    }
756
757    #[test]
758    fn test_text_search_without_config() {
759        let builder = make_select().text_search("body", "hello world", TextSearchType::Plain, None);
760        match &builder.parts.filters[0] {
761            FilterCondition::TextSearch { column, config, search_type, .. } => {
762                assert_eq!(column, "body");
763                assert!(config.is_none());
764                assert_eq!(*search_type, TextSearchType::Plain);
765            }
766            _ => panic!("expected TextSearch filter"),
767        }
768    }
769
770    #[test]
771    fn test_text_search_with_config() {
772        let builder = make_select().text_search(
773            "body",
774            "hello world",
775            TextSearchType::Websearch,
776            Some("english"),
777        );
778        match &builder.parts.filters[0] {
779            FilterCondition::TextSearch { config, search_type, .. } => {
780                assert_eq!(config.as_deref(), Some("english"));
781                assert_eq!(*search_type, TextSearchType::Websearch);
782            }
783            _ => panic!("expected TextSearch filter"),
784        }
785    }
786
787    #[test]
788    fn test_not_wraps_in_not() {
789        let builder = make_select().not(|f| f.eq("active", true));
790        assert_eq!(builder.parts.filters.len(), 1);
791        match &builder.parts.filters[0] {
792            FilterCondition::Not(inner) => {
793                assert!(matches!(inner.as_ref(), FilterCondition::Comparison { .. }));
794            }
795            _ => panic!("expected Not filter"),
796        }
797    }
798
799    #[test]
800    fn test_or_filter_wraps_in_or() {
801        let builder = make_select().or_filter(|f| f.eq("a", 1i32).eq("b", 2i32));
802        assert_eq!(builder.parts.filters.len(), 1);
803        match &builder.parts.filters[0] {
804            FilterCondition::Or(conditions) => {
805                assert_eq!(conditions.len(), 2);
806            }
807            _ => panic!("expected Or filter"),
808        }
809    }
810
811    #[test]
812    fn test_match_filter_creates_match_conditions() {
813        let builder = make_select().match_filter(vec![("name", "Alice"), ("age", "30")]);
814        assert_eq!(builder.parts.filters.len(), 1);
815        match &builder.parts.filters[0] {
816            FilterCondition::Match { conditions } => {
817                assert_eq!(conditions.len(), 2);
818                assert_eq!(conditions[0].0, "name");
819                assert_eq!(conditions[1].0, "age");
820            }
821            _ => panic!("expected Match filter"),
822        }
823    }
824
825    #[test]
826    fn test_filter_raw_sql() {
827        let builder = make_select().filter("age > 18 AND status = 'active'");
828        match &builder.parts.filters[0] {
829            FilterCondition::Raw(sql) => {
830                assert_eq!(sql, "age > 18 AND status = 'active'");
831            }
832            _ => panic!("expected Raw filter"),
833        }
834    }
835
836    #[test]
837    fn test_invalid_column_name_silently_ignored() {
838        let builder = make_select().eq("bad;col", "value");
839        assert!(builder.parts.filters.is_empty());
840    }
841
842    #[test]
843    fn test_invalid_column_name_in_neq_silently_ignored() {
844        let builder = make_select().neq("bad\"col", "value");
845        assert!(builder.parts.filters.is_empty());
846    }
847
848    #[test]
849    fn test_invalid_column_name_in_like_silently_ignored() {
850        let builder = make_select().like("bad--col", "%test%");
851        assert!(builder.parts.filters.is_empty());
852    }
853
854    #[test]
855    fn test_invalid_column_name_in_is_silently_ignored() {
856        let builder = make_select().is("", IsValue::Null);
857        assert!(builder.parts.filters.is_empty());
858    }
859
860    // ---- FilterCollector ----
861
862    #[test]
863    fn test_filter_collector_into_conditions() {
864        let mut params = ParamStore::new();
865        let collector = FilterCollector::new(&mut params)
866            .eq("a", 1i32)
867            .eq("b", 2i32);
868        let conditions = collector.into_conditions();
869        assert_eq!(conditions.len(), 2);
870    }
871
872    #[test]
873    fn test_filter_collector_into_single_condition_single() {
874        let mut params = ParamStore::new();
875        let collector = FilterCollector::new(&mut params).eq("a", 1i32);
876        let condition = collector.into_single_condition();
877        assert!(condition.is_some());
878        assert!(matches!(condition.unwrap(), FilterCondition::Comparison { .. }));
879    }
880
881    #[test]
882    fn test_filter_collector_into_single_condition_empty() {
883        let mut params = ParamStore::new();
884        let collector = FilterCollector::new(&mut params);
885        let condition = collector.into_single_condition();
886        assert!(condition.is_none());
887    }
888
889    #[test]
890    fn test_filter_collector_into_single_condition_multiple() {
891        let mut params = ParamStore::new();
892        let collector = FilterCollector::new(&mut params)
893            .eq("a", 1i32)
894            .neq("b", 2i32);
895        let condition = collector.into_single_condition();
896        assert!(condition.is_some());
897        assert!(matches!(condition.unwrap(), FilterCondition::And(_)));
898    }
899
900    #[test]
901    fn test_filter_collector_all_methods() {
902        let mut params = ParamStore::new();
903        let collector = FilterCollector::new(&mut params)
904            .gt("a", 1i32)
905            .gte("b", 2i32)
906            .lt("c", 3i32)
907            .lte("d", 4i32)
908            .like("e", "%test%")
909            .ilike("f", "%TEST%")
910            .is("g", IsValue::Null);
911        let conditions = collector.into_conditions();
912        assert_eq!(conditions.len(), 7);
913    }
914
915    #[test]
916    fn test_filter_collector_invalid_column_skipped() {
917        let mut params = ParamStore::new();
918        let collector = FilterCollector::new(&mut params)
919            .eq("good_col", 1i32)
920            .eq("bad;col", 2i32);
921        let conditions = collector.into_conditions();
922        assert_eq!(conditions.len(), 1);
923    }
924}