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}