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