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}