Skip to main content

fraiseql_db/
where_clause.rs

1//! WHERE clause abstract syntax tree.
2
3use fraiseql_error::{FraiseQLError, Result};
4use serde::{Deserialize, Serialize};
5
6/// WHERE clause abstract syntax tree.
7///
8/// Represents a type-safe WHERE condition that can be compiled to database-specific SQL.
9///
10/// # Example
11///
12/// ```rust
13/// use fraiseql_db::{WhereClause, WhereOperator};
14/// use serde_json::json;
15///
16/// // Simple condition: email ILIKE '%example.com%'
17/// let where_clause = WhereClause::Field {
18///     path: vec!["email".to_string()],
19///     operator: WhereOperator::Icontains,
20///     value: json!("example.com"),
21/// };
22///
23/// // Complex condition: (published = true) AND (views >= 100)
24/// let where_clause = WhereClause::And(vec![
25///     WhereClause::Field {
26///         path: vec!["published".to_string()],
27///         operator: WhereOperator::Eq,
28///         value: json!(true),
29///     },
30///     WhereClause::Field {
31///         path: vec!["views".to_string()],
32///         operator: WhereOperator::Gte,
33///         value: json!(100),
34///     },
35/// ]);
36/// ```
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[non_exhaustive]
39pub enum WhereClause {
40    /// Single field condition.
41    Field {
42        /// JSONB path (e.g., `["email"]` or `["posts", "title"]`).
43        path:     Vec<String>,
44        /// Comparison operator.
45        operator: WhereOperator,
46        /// Value to compare against.
47        value:    serde_json::Value,
48    },
49
50    /// Logical AND of multiple conditions.
51    And(Vec<WhereClause>),
52
53    /// Logical OR of multiple conditions.
54    Or(Vec<WhereClause>),
55
56    /// Logical NOT of a condition.
57    Not(Box<WhereClause>),
58}
59
60impl WhereClause {
61    /// Check if WHERE clause is empty.
62    #[must_use]
63    pub const fn is_empty(&self) -> bool {
64        match self {
65            Self::And(clauses) | Self::Or(clauses) => clauses.is_empty(),
66            Self::Not(_) | Self::Field { .. } => false,
67        }
68    }
69
70    /// Parse a `WhereClause` from a nested GraphQL JSON `where` variable.
71    ///
72    /// Expected format (nested object with field → operator → value):
73    /// ```json
74    /// {
75    ///   "status": { "eq": "active" },
76    ///   "name": { "icontains": "john" },
77    ///   "_and": [ { "age": { "gte": 18 } }, { "age": { "lte": 65 } } ],
78    ///   "_or": [ { "role": { "eq": "admin" } } ],
79    ///   "_not": { "deleted": { "eq": true } }
80    /// }
81    /// ```
82    ///
83    /// Each top-level key is either a field name (mapped to `WhereClause::Field`
84    /// with operator sub-keys) or a logical combinator (`_and`, `_or`, `_not`).
85    /// Multiple top-level keys are combined with AND.
86    ///
87    /// # Errors
88    ///
89    /// Returns `FraiseQLError::Validation` if the JSON structure is invalid or
90    /// contains unknown operators.
91    ///
92    /// # Panics
93    ///
94    /// Cannot panic: the internal `.expect("checked len == 1")` is only reached
95    /// after verifying `conditions.len() == 1`.
96    pub fn from_graphql_json(value: &serde_json::Value) -> Result<Self> {
97        let Some(obj) = value.as_object() else {
98            return Err(FraiseQLError::Validation {
99                message: "where clause must be a JSON object".to_string(),
100                path:    None,
101            });
102        };
103
104        let mut conditions = Vec::new();
105
106        for (key, val) in obj {
107            match key.as_str() {
108                "_and" => {
109                    let arr = val.as_array().ok_or_else(|| FraiseQLError::Validation {
110                        message: "_and must be an array".to_string(),
111                        path:    None,
112                    })?;
113                    let sub: Result<Vec<Self>> = arr.iter().map(Self::from_graphql_json).collect();
114                    conditions.push(Self::And(sub?));
115                },
116                "_or" => {
117                    let arr = val.as_array().ok_or_else(|| FraiseQLError::Validation {
118                        message: "_or must be an array".to_string(),
119                        path:    None,
120                    })?;
121                    let sub: Result<Vec<Self>> = arr.iter().map(Self::from_graphql_json).collect();
122                    conditions.push(Self::Or(sub?));
123                },
124                "_not" => {
125                    let sub = Self::from_graphql_json(val)?;
126                    conditions.push(Self::Not(Box::new(sub)));
127                },
128                field_name => {
129                    // Field → { operator: value } or { op1: val1, op2: val2 }
130                    let ops = val.as_object().ok_or_else(|| FraiseQLError::Validation {
131                        message: format!(
132                            "where field '{field_name}' must be an object of {{operator: value}}"
133                        ),
134                        path:    None,
135                    })?;
136                    for (op_str, op_val) in ops {
137                        let operator = WhereOperator::from_str(op_str)?;
138                        conditions.push(Self::Field {
139                            path: vec![field_name.to_string()],
140                            operator,
141                            value: op_val.clone(),
142                        });
143                    }
144                },
145            }
146        }
147
148        if conditions.len() == 1 {
149            // Reason: iterator has exactly one element — length was checked on the line above
150            Ok(conditions.into_iter().next().expect("checked len == 1"))
151        } else {
152            Ok(Self::And(conditions))
153        }
154    }
155}
156
157/// WHERE operators (FraiseQL v1 compatibility).
158///
159/// All standard operators are supported.
160/// No underscore prefix (e.g., `eq`, `icontains`, not `_eq`, `_icontains`).
161///
162/// Note: ExtendedOperator variants may contain f64 values which don't implement Eq,
163/// so WhereOperator derives PartialEq only (not Eq).
164///
165/// This enum is marked `#[non_exhaustive]` so that new operators (e.g., `Between`,
166/// `Similar`) can be added in future minor versions without breaking downstream
167/// exhaustive `match` expressions.
168#[non_exhaustive]
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub enum WhereOperator {
171    // ========================================================================
172    // Comparison Operators
173    // ========================================================================
174    /// Equal (=).
175    Eq,
176    /// Not equal (!=).
177    Neq,
178    /// Greater than (>).
179    Gt,
180    /// Greater than or equal (>=).
181    Gte,
182    /// Less than (<).
183    Lt,
184    /// Less than or equal (<=).
185    Lte,
186
187    // ========================================================================
188    // Containment Operators
189    // ========================================================================
190    /// In list (IN).
191    In,
192    /// Not in list (NOT IN).
193    Nin,
194
195    // ========================================================================
196    // String Operators
197    // ========================================================================
198    /// Contains substring (LIKE '%value%').
199    Contains,
200    /// Contains substring (case-insensitive) (ILIKE '%value%').
201    Icontains,
202    /// Starts with (LIKE 'value%').
203    Startswith,
204    /// Starts with (case-insensitive) (ILIKE 'value%').
205    Istartswith,
206    /// Ends with (LIKE '%value').
207    Endswith,
208    /// Ends with (case-insensitive) (ILIKE '%value').
209    Iendswith,
210    /// Pattern matching (LIKE).
211    Like,
212    /// Pattern matching (case-insensitive) (ILIKE).
213    Ilike,
214    /// Negated pattern matching (NOT LIKE).
215    Nlike,
216    /// Negated pattern matching (case-insensitive) (NOT ILIKE).
217    Nilike,
218    /// POSIX regex match (~).
219    Regex,
220    /// POSIX regex match (case-insensitive) (~*).
221    Iregex,
222    /// Negated POSIX regex match (!~).
223    Nregex,
224    /// Negated POSIX regex match (case-insensitive) (!~*).
225    Niregex,
226
227    // ========================================================================
228    // Null Checks
229    // ========================================================================
230    /// Is null (IS NULL or IS NOT NULL).
231    IsNull,
232
233    // ========================================================================
234    // Array Operators
235    // ========================================================================
236    /// Array contains (@>).
237    ArrayContains,
238    /// Array contained by (<@).
239    ArrayContainedBy,
240    /// Array overlaps (&&).
241    ArrayOverlaps,
242    /// Array length equal.
243    LenEq,
244    /// Array length greater than.
245    LenGt,
246    /// Array length less than.
247    LenLt,
248    /// Array length greater than or equal.
249    LenGte,
250    /// Array length less than or equal.
251    LenLte,
252    /// Array length not equal.
253    LenNeq,
254
255    // ========================================================================
256    // Vector Operators (pgvector)
257    // ========================================================================
258    /// Cosine distance (<=>).
259    CosineDistance,
260    /// L2 (Euclidean) distance (<->).
261    L2Distance,
262    /// L1 (Manhattan) distance (<+>).
263    L1Distance,
264    /// Hamming distance (<~>).
265    HammingDistance,
266    /// Inner product (<#>). Higher values = more similar.
267    InnerProduct,
268    /// Jaccard distance for set similarity.
269    JaccardDistance,
270
271    // ========================================================================
272    // Full-Text Search
273    // ========================================================================
274    /// Full-text search (@@).
275    Matches,
276    /// Plain text query (plainto_tsquery).
277    PlainQuery,
278    /// Phrase query (phraseto_tsquery).
279    PhraseQuery,
280    /// Web search query (websearch_to_tsquery).
281    WebsearchQuery,
282
283    // ========================================================================
284    // Network Operators (INET/CIDR)
285    // ========================================================================
286    /// Is IPv4.
287    IsIPv4,
288    /// Is IPv6.
289    IsIPv6,
290    /// Is private IP (RFC1918 ranges).
291    IsPrivate,
292    /// Is public IP (not private).
293    IsPublic,
294    /// Is loopback address (127.0.0.0/8 or ::1).
295    IsLoopback,
296    /// In subnet (<<) - IP is contained within subnet.
297    InSubnet,
298    /// Contains subnet (>>) - subnet contains another subnet.
299    ContainsSubnet,
300    /// Contains IP (>>) - subnet contains an IP address.
301    ContainsIP,
302    /// Overlaps (&&) - subnets overlap.
303    Overlaps,
304
305    // ========================================================================
306    // JSONB Operators
307    // ========================================================================
308    /// Strictly contains (@>).
309    StrictlyContains,
310
311    // ========================================================================
312    // LTree Operators (Hierarchical)
313    // ========================================================================
314    /// Ancestor of (@>).
315    AncestorOf,
316    /// Descendant of (<@).
317    DescendantOf,
318    /// Matches lquery (~).
319    MatchesLquery,
320    /// Matches ltxtquery (@) - Boolean query syntax.
321    MatchesLtxtquery,
322    /// Matches any lquery (?).
323    MatchesAnyLquery,
324    /// Depth equal (nlevel() =).
325    DepthEq,
326    /// Depth not equal (nlevel() !=).
327    DepthNeq,
328    /// Depth greater than (nlevel() >).
329    DepthGt,
330    /// Depth greater than or equal (nlevel() >=).
331    DepthGte,
332    /// Depth less than (nlevel() <).
333    DepthLt,
334    /// Depth less than or equal (nlevel() <=).
335    DepthLte,
336    /// Lowest common ancestor (lca()).
337    Lca,
338
339    // ========================================================================
340    // Extended Operators (Rich Type Filters)
341    // ========================================================================
342    /// Extended operator for rich scalar types (Email, VIN, CountryCode, etc.)
343    /// These operators are specialized filters enabled via feature flags.
344    /// See `fraiseql_core::filters::ExtendedOperator` for available operators.
345    #[serde(skip)]
346    Extended(crate::filters::ExtendedOperator),
347}
348
349impl WhereOperator {
350    /// Parse operator from string (GraphQL input).
351    ///
352    /// # Errors
353    ///
354    /// Returns `FraiseQLError::Validation` if operator name is unknown.
355    #[allow(clippy::should_implement_trait)] // Reason: intentionally not implementing `FromStr` because this returns `FraiseQLError`, not `<Self as FromStr>::Err`.
356    pub fn from_str(s: &str) -> Result<Self> {
357        match s {
358            "eq" => Ok(Self::Eq),
359            "neq" => Ok(Self::Neq),
360            "gt" => Ok(Self::Gt),
361            "gte" => Ok(Self::Gte),
362            "lt" => Ok(Self::Lt),
363            "lte" => Ok(Self::Lte),
364            "in" => Ok(Self::In),
365            "nin" | "notin" => Ok(Self::Nin),
366            "contains" => Ok(Self::Contains),
367            "icontains" => Ok(Self::Icontains),
368            "startswith" => Ok(Self::Startswith),
369            "istartswith" => Ok(Self::Istartswith),
370            "endswith" => Ok(Self::Endswith),
371            "iendswith" => Ok(Self::Iendswith),
372            "like" => Ok(Self::Like),
373            "ilike" => Ok(Self::Ilike),
374            "nlike" => Ok(Self::Nlike),
375            "nilike" => Ok(Self::Nilike),
376            "regex" => Ok(Self::Regex),
377            "iregex" | "imatches" => Ok(Self::Iregex),
378            "nregex" | "not_matches" => Ok(Self::Nregex),
379            "niregex" => Ok(Self::Niregex),
380            "isnull" => Ok(Self::IsNull),
381            "array_contains" => Ok(Self::ArrayContains),
382            "array_contained_by" => Ok(Self::ArrayContainedBy),
383            "array_overlaps" => Ok(Self::ArrayOverlaps),
384            "len_eq" => Ok(Self::LenEq),
385            "len_gt" => Ok(Self::LenGt),
386            "len_lt" => Ok(Self::LenLt),
387            "len_gte" => Ok(Self::LenGte),
388            "len_lte" => Ok(Self::LenLte),
389            "len_neq" => Ok(Self::LenNeq),
390            "cosine_distance" => Ok(Self::CosineDistance),
391            "l2_distance" => Ok(Self::L2Distance),
392            "l1_distance" => Ok(Self::L1Distance),
393            "hamming_distance" => Ok(Self::HammingDistance),
394            "inner_product" => Ok(Self::InnerProduct),
395            "jaccard_distance" => Ok(Self::JaccardDistance),
396            "matches" => Ok(Self::Matches),
397            "plain_query" => Ok(Self::PlainQuery),
398            "phrase_query" => Ok(Self::PhraseQuery),
399            "websearch_query" => Ok(Self::WebsearchQuery),
400            "is_ipv4" => Ok(Self::IsIPv4),
401            "is_ipv6" => Ok(Self::IsIPv6),
402            "is_private" => Ok(Self::IsPrivate),
403            "is_public" => Ok(Self::IsPublic),
404            "is_loopback" => Ok(Self::IsLoopback),
405            "in_subnet" | "inrange" => Ok(Self::InSubnet),
406            "contains_subnet" => Ok(Self::ContainsSubnet),
407            "contains_ip" => Ok(Self::ContainsIP),
408            "overlaps" => Ok(Self::Overlaps),
409            "strictly_contains" => Ok(Self::StrictlyContains),
410            "ancestor_of" => Ok(Self::AncestorOf),
411            "descendant_of" => Ok(Self::DescendantOf),
412            "matches_lquery" => Ok(Self::MatchesLquery),
413            "matches_ltxtquery" => Ok(Self::MatchesLtxtquery),
414            "matches_any_lquery" => Ok(Self::MatchesAnyLquery),
415            "depth_eq" => Ok(Self::DepthEq),
416            "depth_neq" => Ok(Self::DepthNeq),
417            "depth_gt" => Ok(Self::DepthGt),
418            "depth_gte" => Ok(Self::DepthGte),
419            "depth_lt" => Ok(Self::DepthLt),
420            "depth_lte" => Ok(Self::DepthLte),
421            "lca" => Ok(Self::Lca),
422            _ => Err(FraiseQLError::validation(format!("Unknown WHERE operator: {s}"))),
423        }
424    }
425
426    /// Check if operator requires array value.
427    #[must_use]
428    pub const fn expects_array(&self) -> bool {
429        matches!(self, Self::In | Self::Nin)
430    }
431
432    /// Check if operator is case-insensitive.
433    #[must_use]
434    pub const fn is_case_insensitive(&self) -> bool {
435        matches!(
436            self,
437            Self::Icontains
438                | Self::Istartswith
439                | Self::Iendswith
440                | Self::Ilike
441                | Self::Nilike
442                | Self::Iregex
443                | Self::Niregex
444        )
445    }
446
447    /// Check if operator works with strings.
448    #[must_use]
449    pub const fn is_string_operator(&self) -> bool {
450        matches!(
451            self,
452            Self::Contains
453                | Self::Icontains
454                | Self::Startswith
455                | Self::Istartswith
456                | Self::Endswith
457                | Self::Iendswith
458                | Self::Like
459                | Self::Ilike
460                | Self::Nlike
461                | Self::Nilike
462                | Self::Regex
463                | Self::Iregex
464                | Self::Nregex
465                | Self::Niregex
466        )
467    }
468}
469
470/// HAVING clause abstract syntax tree.
471///
472/// HAVING filters aggregated results after GROUP BY, while WHERE filters rows before aggregation.
473///
474/// # Example
475///
476/// ```rust
477/// use fraiseql_db::{HavingClause, WhereOperator};
478/// use serde_json::json;
479///
480/// // Simple condition: COUNT(*) > 10
481/// let having_clause = HavingClause::Aggregate {
482///     aggregate: "count".to_string(),
483///     operator: WhereOperator::Gt,
484///     value: json!(10),
485/// };
486///
487/// // Complex condition: (COUNT(*) > 10) AND (SUM(revenue) >= 1000)
488/// let having_clause = HavingClause::And(vec![
489///     HavingClause::Aggregate {
490///         aggregate: "count".to_string(),
491///         operator: WhereOperator::Gt,
492///         value: json!(10),
493///     },
494///     HavingClause::Aggregate {
495///         aggregate: "revenue_sum".to_string(),
496///         operator: WhereOperator::Gte,
497///         value: json!(1000),
498///     },
499/// ]);
500/// ```
501#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
502#[non_exhaustive]
503pub enum HavingClause {
504    /// Aggregate field condition (e.g., count_gt, revenue_sum_gte).
505    Aggregate {
506        /// Aggregate name: "count" or "field_function" (e.g., "revenue_sum").
507        aggregate: String,
508        /// Comparison operator.
509        operator:  WhereOperator,
510        /// Value to compare against.
511        value:     serde_json::Value,
512    },
513
514    /// Logical AND of multiple conditions.
515    And(Vec<HavingClause>),
516
517    /// Logical OR of multiple conditions.
518    Or(Vec<HavingClause>),
519
520    /// Logical NOT of a condition.
521    Not(Box<HavingClause>),
522}
523
524impl HavingClause {
525    /// Check if HAVING clause is empty.
526    #[must_use]
527    pub const fn is_empty(&self) -> bool {
528        match self {
529            Self::And(clauses) | Self::Or(clauses) => clauses.is_empty(),
530            Self::Not(_) | Self::Aggregate { .. } => false,
531        }
532    }
533}
534
535#[cfg(test)]
536#[allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
537mod tests {
538    use serde_json::json;
539
540    use super::*;
541
542    #[test]
543    fn test_where_operator_from_str() {
544        assert_eq!(WhereOperator::from_str("eq").unwrap(), WhereOperator::Eq);
545        assert_eq!(WhereOperator::from_str("icontains").unwrap(), WhereOperator::Icontains);
546        assert_eq!(WhereOperator::from_str("gte").unwrap(), WhereOperator::Gte);
547        assert!(
548            matches!(WhereOperator::from_str("unknown"), Err(FraiseQLError::Validation { .. })),
549            "expected Validation error for unknown operator"
550        );
551    }
552
553    #[test]
554    fn test_where_operator_expects_array() {
555        assert!(WhereOperator::In.expects_array());
556        assert!(WhereOperator::Nin.expects_array());
557        assert!(!WhereOperator::Eq.expects_array());
558    }
559
560    #[test]
561    fn test_where_operator_is_case_insensitive() {
562        assert!(WhereOperator::Icontains.is_case_insensitive());
563        assert!(WhereOperator::Ilike.is_case_insensitive());
564        assert!(!WhereOperator::Contains.is_case_insensitive());
565    }
566
567    #[test]
568    fn test_where_clause_simple() {
569        let clause = WhereClause::Field {
570            path:     vec!["email".to_string()],
571            operator: WhereOperator::Eq,
572            value:    json!("test@example.com"),
573        };
574
575        assert!(!clause.is_empty());
576    }
577
578    #[test]
579    fn test_where_clause_and() {
580        let clause = WhereClause::And(vec![
581            WhereClause::Field {
582                path:     vec!["published".to_string()],
583                operator: WhereOperator::Eq,
584                value:    json!(true),
585            },
586            WhereClause::Field {
587                path:     vec!["views".to_string()],
588                operator: WhereOperator::Gte,
589                value:    json!(100),
590            },
591        ]);
592
593        assert!(!clause.is_empty());
594    }
595
596    #[test]
597    fn test_where_clause_empty() {
598        let clause = WhereClause::And(vec![]);
599        assert!(clause.is_empty());
600    }
601
602    #[test]
603    fn test_from_graphql_json_simple_field() {
604        let json = json!({ "status": { "eq": "active" } });
605        let clause = WhereClause::from_graphql_json(&json).unwrap();
606        assert_eq!(
607            clause,
608            WhereClause::Field {
609                path:     vec!["status".to_string()],
610                operator: WhereOperator::Eq,
611                value:    json!("active"),
612            }
613        );
614    }
615
616    #[test]
617    fn test_from_graphql_json_multiple_fields() {
618        let json = json!({
619            "status": { "eq": "active" },
620            "age": { "gte": 18 }
621        });
622        let clause = WhereClause::from_graphql_json(&json).unwrap();
623        match clause {
624            WhereClause::And(conditions) => assert_eq!(conditions.len(), 2),
625            _ => panic!("expected And"),
626        }
627    }
628
629    #[test]
630    fn test_from_graphql_json_logical_combinators() {
631        let json = json!({
632            "_or": [
633                { "role": { "eq": "admin" } },
634                { "role": { "eq": "superadmin" } }
635            ]
636        });
637        let clause = WhereClause::from_graphql_json(&json).unwrap();
638        match clause {
639            WhereClause::Or(conditions) => assert_eq!(conditions.len(), 2),
640            _ => panic!("expected Or"),
641        }
642    }
643
644    #[test]
645    fn test_from_graphql_json_not() {
646        let json = json!({ "_not": { "deleted": { "eq": true } } });
647        let clause = WhereClause::from_graphql_json(&json).unwrap();
648        assert!(matches!(clause, WhereClause::Not(_)));
649    }
650
651    #[test]
652    fn test_from_graphql_json_invalid_operator() {
653        let json = json!({ "field": { "nonexistent_op": 42 } });
654        let result = WhereClause::from_graphql_json(&json);
655        assert!(
656            matches!(result, Err(FraiseQLError::Validation { .. })),
657            "expected Validation error, got: {result:?}"
658        );
659    }
660
661    #[test]
662    fn test_new_string_operators_from_str() {
663        assert_eq!(WhereOperator::from_str("nlike").unwrap(), WhereOperator::Nlike);
664        assert_eq!(WhereOperator::from_str("nilike").unwrap(), WhereOperator::Nilike);
665        assert_eq!(WhereOperator::from_str("regex").unwrap(), WhereOperator::Regex);
666        assert_eq!(WhereOperator::from_str("iregex").unwrap(), WhereOperator::Iregex);
667        assert_eq!(WhereOperator::from_str("nregex").unwrap(), WhereOperator::Nregex);
668        assert_eq!(WhereOperator::from_str("niregex").unwrap(), WhereOperator::Niregex);
669    }
670
671    #[test]
672    fn test_v1_aliases_from_str() {
673        // notin → Nin
674        assert_eq!(WhereOperator::from_str("notin").unwrap(), WhereOperator::Nin);
675        // inrange → InSubnet
676        assert_eq!(WhereOperator::from_str("inrange").unwrap(), WhereOperator::InSubnet);
677        // imatches → Iregex
678        assert_eq!(WhereOperator::from_str("imatches").unwrap(), WhereOperator::Iregex);
679        // not_matches → Nregex
680        assert_eq!(WhereOperator::from_str("not_matches").unwrap(), WhereOperator::Nregex);
681    }
682
683    #[test]
684    fn test_new_operators_case_insensitive_flag() {
685        assert!(WhereOperator::Nilike.is_case_insensitive());
686        assert!(WhereOperator::Iregex.is_case_insensitive());
687        assert!(WhereOperator::Niregex.is_case_insensitive());
688        assert!(!WhereOperator::Nlike.is_case_insensitive());
689        assert!(!WhereOperator::Regex.is_case_insensitive());
690        assert!(!WhereOperator::Nregex.is_case_insensitive());
691    }
692
693    #[test]
694    fn test_new_operators_are_string_operators() {
695        assert!(WhereOperator::Nlike.is_string_operator());
696        assert!(WhereOperator::Nilike.is_string_operator());
697        assert!(WhereOperator::Regex.is_string_operator());
698        assert!(WhereOperator::Iregex.is_string_operator());
699        assert!(WhereOperator::Nregex.is_string_operator());
700        assert!(WhereOperator::Niregex.is_string_operator());
701    }
702}