Skip to main content

fraiseql_core/utils/
operators.rs

1//! GraphQL WHERE clause operator definitions and registry.
2//!
3//! This module provides a comprehensive registry of all GraphQL operators
4//! supported by `FraiseQL`, including comparison, string, array, vector,
5//! and full-text search operators.
6
7use std::{collections::HashMap, sync::LazyLock};
8
9/// Category of operator (affects SQL generation strategy)
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum OperatorCategory {
12    /// Basic comparison: =, !=, >, <, >=, <=
13    Comparison,
14    /// String operations: LIKE, ILIKE, regex, etc.
15    String,
16    /// NULL checks: IS NULL, IS NOT NULL
17    Null,
18    /// Array/list containment: @>, <@, &&
19    Array,
20    /// pgvector distance operators
21    Vector,
22    /// `PostgreSQL` full-text search
23    Fulltext,
24    /// Containment for JSONB: @>, <@
25    Containment,
26    /// Network/IP operators
27    Network,
28    /// Date/range operators
29    DateRange,
30    /// Ltree (hierarchical) operators
31    Ltree,
32    /// Spatial/coordinate operators
33    Spatial,
34    /// Path operators
35    Path,
36}
37
38/// Information about a single operator
39#[derive(Debug, Clone)]
40pub struct OperatorInfo {
41    /// GraphQL operator name (e.g., "eq", "contains")
42    pub name:           &'static str,
43    /// SQL operator or function (e.g., "=", "LIKE", "@>")
44    pub sql_op:         &'static str,
45    /// Category of operator
46    pub category:       OperatorCategory,
47    /// Whether this operator expects an array value
48    pub requires_array: bool,
49    /// Whether this operator needs special JSONB handling
50    pub jsonb_operator: bool,
51}
52
53/// Global registry of all supported operators
54pub static OPERATOR_REGISTRY: LazyLock<HashMap<&'static str, OperatorInfo>> = LazyLock::new(|| {
55    let mut m = HashMap::new();
56
57    // ========== COMPARISON OPERATORS ==========
58    m.insert(
59        "eq",
60        OperatorInfo {
61            name:           "eq",
62            sql_op:         "=",
63            category:       OperatorCategory::Comparison,
64            requires_array: false,
65            jsonb_operator: false,
66        },
67    );
68
69    m.insert(
70        "ne",
71        OperatorInfo {
72            name:           "ne",
73            sql_op:         "!=",
74            category:       OperatorCategory::Comparison,
75            requires_array: false,
76            jsonb_operator: false,
77        },
78    );
79
80    m.insert(
81        "gt",
82        OperatorInfo {
83            name:           "gt",
84            sql_op:         ">",
85            category:       OperatorCategory::Comparison,
86            requires_array: false,
87            jsonb_operator: false,
88        },
89    );
90
91    m.insert(
92        "gte",
93        OperatorInfo {
94            name:           "gte",
95            sql_op:         ">=",
96            category:       OperatorCategory::Comparison,
97            requires_array: false,
98            jsonb_operator: false,
99        },
100    );
101
102    m.insert(
103        "lt",
104        OperatorInfo {
105            name:           "lt",
106            sql_op:         "<",
107            category:       OperatorCategory::Comparison,
108            requires_array: false,
109            jsonb_operator: false,
110        },
111    );
112
113    m.insert(
114        "lte",
115        OperatorInfo {
116            name:           "lte",
117            sql_op:         "<=",
118            category:       OperatorCategory::Comparison,
119            requires_array: false,
120            jsonb_operator: false,
121        },
122    );
123
124    m.insert(
125        "in",
126        OperatorInfo {
127            name:           "in",
128            sql_op:         "IN",
129            category:       OperatorCategory::Comparison,
130            requires_array: true,
131            jsonb_operator: false,
132        },
133    );
134
135    m.insert(
136        "nin",
137        OperatorInfo {
138            name:           "nin",
139            sql_op:         "NOT IN",
140            category:       OperatorCategory::Comparison,
141            requires_array: true,
142            jsonb_operator: false,
143        },
144    );
145
146    // ========== STRING OPERATORS ==========
147    m.insert(
148        "like",
149        OperatorInfo {
150            name:           "like",
151            sql_op:         "LIKE",
152            category:       OperatorCategory::String,
153            requires_array: false,
154            jsonb_operator: false,
155        },
156    );
157
158    m.insert(
159        "ilike",
160        OperatorInfo {
161            name:           "ilike",
162            sql_op:         "ILIKE",
163            category:       OperatorCategory::String,
164            requires_array: false,
165            jsonb_operator: false,
166        },
167    );
168
169    m.insert(
170        "nlike",
171        OperatorInfo {
172            name:           "nlike",
173            sql_op:         "NOT LIKE",
174            category:       OperatorCategory::String,
175            requires_array: false,
176            jsonb_operator: false,
177        },
178    );
179
180    m.insert(
181        "nilike",
182        OperatorInfo {
183            name:           "nilike",
184            sql_op:         "NOT ILIKE",
185            category:       OperatorCategory::String,
186            requires_array: false,
187            jsonb_operator: false,
188        },
189    );
190
191    m.insert(
192        "regex",
193        OperatorInfo {
194            name:           "regex",
195            sql_op:         "~",
196            category:       OperatorCategory::String,
197            requires_array: false,
198            jsonb_operator: false,
199        },
200    );
201
202    m.insert(
203        "iregex",
204        OperatorInfo {
205            name:           "iregex",
206            sql_op:         "~*",
207            category:       OperatorCategory::String,
208            requires_array: false,
209            jsonb_operator: false,
210        },
211    );
212
213    m.insert(
214        "nregex",
215        OperatorInfo {
216            name:           "nregex",
217            sql_op:         "!~",
218            category:       OperatorCategory::String,
219            requires_array: false,
220            jsonb_operator: false,
221        },
222    );
223
224    m.insert(
225        "niregex",
226        OperatorInfo {
227            name:           "niregex",
228            sql_op:         "!~*",
229            category:       OperatorCategory::String,
230            requires_array: false,
231            jsonb_operator: false,
232        },
233    );
234
235    // ========== NULL OPERATORS ==========
236    m.insert(
237        "is_null",
238        OperatorInfo {
239            name:           "is_null",
240            sql_op:         "IS NULL",
241            category:       OperatorCategory::Null,
242            requires_array: false,
243            jsonb_operator: false,
244        },
245    );
246
247    m.insert(
248        "is_not_null",
249        OperatorInfo {
250            name:           "is_not_null",
251            sql_op:         "IS NOT NULL",
252            category:       OperatorCategory::Null,
253            requires_array: false,
254            jsonb_operator: false,
255        },
256    );
257
258    // ========== CONTAINMENT OPERATORS (JSONB) ==========
259    m.insert(
260        "contains",
261        OperatorInfo {
262            name:           "contains",
263            sql_op:         "@>",
264            category:       OperatorCategory::Containment,
265            requires_array: false,
266            jsonb_operator: true,
267        },
268    );
269
270    m.insert(
271        "contained_in",
272        OperatorInfo {
273            name:           "contained_in",
274            sql_op:         "<@",
275            category:       OperatorCategory::Containment,
276            requires_array: false,
277            jsonb_operator: true,
278        },
279    );
280
281    m.insert(
282        "has_key",
283        OperatorInfo {
284            name:           "has_key",
285            sql_op:         "?",
286            category:       OperatorCategory::Containment,
287            requires_array: false,
288            jsonb_operator: true,
289        },
290    );
291
292    m.insert(
293        "has_any_keys",
294        OperatorInfo {
295            name:           "has_any_keys",
296            sql_op:         "?|",
297            category:       OperatorCategory::Containment,
298            requires_array: true,
299            jsonb_operator: true,
300        },
301    );
302
303    m.insert(
304        "has_all_keys",
305        OperatorInfo {
306            name:           "has_all_keys",
307            sql_op:         "?&",
308            category:       OperatorCategory::Containment,
309            requires_array: true,
310            jsonb_operator: true,
311        },
312    );
313
314    // ========== ARRAY OPERATORS ==========
315    m.insert(
316        "array_contains",
317        OperatorInfo {
318            name:           "array_contains",
319            sql_op:         "@>",
320            category:       OperatorCategory::Array,
321            requires_array: false,
322            jsonb_operator: false,
323        },
324    );
325
326    m.insert(
327        "array_contained_in",
328        OperatorInfo {
329            name:           "array_contained_in",
330            sql_op:         "<@",
331            category:       OperatorCategory::Array,
332            requires_array: false,
333            jsonb_operator: false,
334        },
335    );
336
337    m.insert(
338        "array_overlaps",
339        OperatorInfo {
340            name:           "array_overlaps",
341            sql_op:         "&&",
342            category:       OperatorCategory::Array,
343            requires_array: false,
344            jsonb_operator: false,
345        },
346    );
347
348    // ========== VECTOR OPERATORS (pgvector) ==========
349    m.insert(
350        "cosine_distance",
351        OperatorInfo {
352            name:           "cosine_distance",
353            sql_op:         "<=>",
354            category:       OperatorCategory::Vector,
355            requires_array: false,
356            jsonb_operator: false,
357        },
358    );
359
360    m.insert(
361        "l2_distance",
362        OperatorInfo {
363            name:           "l2_distance",
364            sql_op:         "<->",
365            category:       OperatorCategory::Vector,
366            requires_array: false,
367            jsonb_operator: false,
368        },
369    );
370
371    m.insert(
372        "inner_product",
373        OperatorInfo {
374            name:           "inner_product",
375            sql_op:         "<#>",
376            category:       OperatorCategory::Vector,
377            requires_array: false,
378            jsonb_operator: false,
379        },
380    );
381
382    m.insert(
383        "l1_distance",
384        OperatorInfo {
385            name:           "l1_distance",
386            sql_op:         "<+>",
387            category:       OperatorCategory::Vector,
388            requires_array: false,
389            jsonb_operator: false,
390        },
391    );
392
393    m.insert(
394        "hamming_distance",
395        OperatorInfo {
396            name:           "hamming_distance",
397            sql_op:         "<~>",
398            category:       OperatorCategory::Vector,
399            requires_array: false,
400            jsonb_operator: false,
401        },
402    );
403
404    m.insert(
405        "jaccard_distance",
406        OperatorInfo {
407            name:           "jaccard_distance",
408            sql_op:         "<%>",
409            category:       OperatorCategory::Vector,
410            requires_array: false,
411            jsonb_operator: false,
412        },
413    );
414
415    // ========== FULLTEXT OPERATORS ==========
416    m.insert(
417        "search",
418        OperatorInfo {
419            name:           "search",
420            sql_op:         "@@",
421            category:       OperatorCategory::Fulltext,
422            requires_array: false,
423            jsonb_operator: false,
424        },
425    );
426
427    m.insert(
428        "plainto_tsquery",
429        OperatorInfo {
430            name:           "plainto_tsquery",
431            sql_op:         "@@",
432            category:       OperatorCategory::Fulltext,
433            requires_array: false,
434            jsonb_operator: false,
435        },
436    );
437
438    m.insert(
439        "phraseto_tsquery",
440        OperatorInfo {
441            name:           "phraseto_tsquery",
442            sql_op:         "@@",
443            category:       OperatorCategory::Fulltext,
444            requires_array: false,
445            jsonb_operator: false,
446        },
447    );
448
449    m.insert(
450        "websearch_to_tsquery",
451        OperatorInfo {
452            name:           "websearch_to_tsquery",
453            sql_op:         "@@",
454            category:       OperatorCategory::Fulltext,
455            requires_array: false,
456            jsonb_operator: false,
457        },
458    );
459
460    // ========== STRING PATTERN OPERATORS (Extended) ==========
461    m.insert(
462        "startswith",
463        OperatorInfo {
464            name:           "startswith",
465            sql_op:         "LIKE",
466            category:       OperatorCategory::String,
467            requires_array: false,
468            jsonb_operator: false,
469        },
470    );
471
472    m.insert(
473        "istartswith",
474        OperatorInfo {
475            name:           "istartswith",
476            sql_op:         "ILIKE",
477            category:       OperatorCategory::String,
478            requires_array: false,
479            jsonb_operator: false,
480        },
481    );
482
483    m.insert(
484        "endswith",
485        OperatorInfo {
486            name:           "endswith",
487            sql_op:         "LIKE",
488            category:       OperatorCategory::String,
489            requires_array: false,
490            jsonb_operator: false,
491        },
492    );
493
494    m.insert(
495        "iendswith",
496        OperatorInfo {
497            name:           "iendswith",
498            sql_op:         "ILIKE",
499            category:       OperatorCategory::String,
500            requires_array: false,
501            jsonb_operator: false,
502        },
503    );
504
505    m.insert(
506        "icontains",
507        OperatorInfo {
508            name:           "icontains",
509            sql_op:         "ILIKE",
510            category:       OperatorCategory::String,
511            requires_array: false,
512            jsonb_operator: false,
513        },
514    );
515
516    m.insert(
517        "imatches",
518        OperatorInfo {
519            name:           "imatches",
520            sql_op:         "~*",
521            category:       OperatorCategory::String,
522            requires_array: false,
523            jsonb_operator: false,
524        },
525    );
526
527    m.insert(
528        "not_matches",
529        OperatorInfo {
530            name:           "not_matches",
531            sql_op:         "!~",
532            category:       OperatorCategory::String,
533            requires_array: false,
534            jsonb_operator: false,
535        },
536    );
537
538    // ========== NETWORK/IP OPERATORS ==========
539    m.insert(
540        "isIPv4",
541        OperatorInfo {
542            name:           "isIPv4",
543            sql_op:         "family({}) = 4",
544            category:       OperatorCategory::Network,
545            requires_array: false,
546            jsonb_operator: false,
547        },
548    );
549
550    m.insert(
551        "isIPv6",
552        OperatorInfo {
553            name:           "isIPv6",
554            sql_op:         "family({}) = 6",
555            category:       OperatorCategory::Network,
556            requires_array: false,
557            jsonb_operator: false,
558        },
559    );
560
561    m.insert(
562        "isPrivate",
563        OperatorInfo {
564            name:           "isPrivate",
565            sql_op:         "CIDR_RANGE_CHECK",
566            category:       OperatorCategory::Network,
567            requires_array: false,
568            jsonb_operator: false,
569        },
570    );
571
572    m.insert(
573        "isPublic",
574        OperatorInfo {
575            name:           "isPublic",
576            sql_op:         "NOT_CIDR_RANGE_CHECK",
577            category:       OperatorCategory::Network,
578            requires_array: false,
579            jsonb_operator: false,
580        },
581    );
582
583    m.insert(
584        "inSubnet",
585        OperatorInfo {
586            name:           "inSubnet",
587            sql_op:         "{} <<= {}",
588            category:       OperatorCategory::Network,
589            requires_array: false,
590            jsonb_operator: false,
591        },
592    );
593
594    m.insert(
595        "notInSubnet",
596        OperatorInfo {
597            name:           "notInSubnet",
598            sql_op:         "NOT ({} <<= {})",
599            category:       OperatorCategory::Network,
600            requires_array: false,
601            jsonb_operator: false,
602        },
603    );
604
605    m.insert(
606        "subnet_contains",
607        OperatorInfo {
608            name:           "subnet_contains",
609            sql_op:         ">>",
610            category:       OperatorCategory::Network,
611            requires_array: false,
612            jsonb_operator: false,
613        },
614    );
615
616    m.insert(
617        "subnet_overlaps",
618        OperatorInfo {
619            name:           "subnet_overlaps",
620            sql_op:         "&&",
621            category:       OperatorCategory::Network,
622            requires_array: false,
623            jsonb_operator: false,
624        },
625    );
626
627    // ========== DATE/RANGE OPERATORS ==========
628    m.insert(
629        "contains_date",
630        OperatorInfo {
631            name:           "contains_date",
632            sql_op:         "@>",
633            category:       OperatorCategory::DateRange,
634            requires_array: false,
635            jsonb_operator: false,
636        },
637    );
638
639    m.insert(
640        "adjacent",
641        OperatorInfo {
642            name:           "adjacent",
643            sql_op:         "-|-",
644            category:       OperatorCategory::DateRange,
645            requires_array: false,
646            jsonb_operator: false,
647        },
648    );
649
650    m.insert(
651        "strictly_left",
652        OperatorInfo {
653            name:           "strictly_left",
654            sql_op:         "<<",
655            category:       OperatorCategory::DateRange,
656            requires_array: false,
657            jsonb_operator: false,
658        },
659    );
660
661    m.insert(
662        "strictly_right",
663        OperatorInfo {
664            name:           "strictly_right",
665            sql_op:         ">>",
666            category:       OperatorCategory::DateRange,
667            requires_array: false,
668            jsonb_operator: false,
669        },
670    );
671
672    m.insert(
673        "not_left",
674        OperatorInfo {
675            name:           "not_left",
676            sql_op:         "&>",
677            category:       OperatorCategory::DateRange,
678            requires_array: false,
679            jsonb_operator: false,
680        },
681    );
682
683    m.insert(
684        "not_right",
685        OperatorInfo {
686            name:           "not_right",
687            sql_op:         "&<",
688            category:       OperatorCategory::DateRange,
689            requires_array: false,
690            jsonb_operator: false,
691        },
692    );
693
694    m.insert(
695        "overlaps",
696        OperatorInfo {
697            name:           "overlaps",
698            sql_op:         "&&",
699            category:       OperatorCategory::DateRange,
700            requires_array: false,
701            jsonb_operator: false,
702        },
703    );
704
705    // ========== LTREE (HIERARCHICAL) OPERATORS ==========
706    m.insert(
707        "ancestor_of",
708        OperatorInfo {
709            name:           "ancestor_of",
710            sql_op:         "@>",
711            category:       OperatorCategory::Ltree,
712            requires_array: false,
713            jsonb_operator: false,
714        },
715    );
716
717    m.insert(
718        "descendant_of",
719        OperatorInfo {
720            name:           "descendant_of",
721            sql_op:         "<@",
722            category:       OperatorCategory::Ltree,
723            requires_array: false,
724            jsonb_operator: false,
725        },
726    );
727
728    m.insert(
729        "matches_lquery",
730        OperatorInfo {
731            name:           "matches_lquery",
732            sql_op:         "~",
733            category:       OperatorCategory::Ltree,
734            requires_array: false,
735            jsonb_operator: false,
736        },
737    );
738
739    m.insert(
740        "matches_ltxtquery",
741        OperatorInfo {
742            name:           "matches_ltxtquery",
743            sql_op:         "@",
744            category:       OperatorCategory::Ltree,
745            requires_array: false,
746            jsonb_operator: false,
747        },
748    );
749
750    m.insert(
751        "matches_any_lquery",
752        OperatorInfo {
753            name:           "matches_any_lquery",
754            sql_op:         "?",
755            category:       OperatorCategory::Ltree,
756            requires_array: true,
757            jsonb_operator: false,
758        },
759    );
760
761    // ========== PATH OPERATORS ==========
762    m.insert(
763        "depth_eq",
764        OperatorInfo {
765            name:           "depth_eq",
766            sql_op:         "nlevel({}) =",
767            category:       OperatorCategory::Path,
768            requires_array: false,
769            jsonb_operator: false,
770        },
771    );
772
773    m.insert(
774        "depth_gt",
775        OperatorInfo {
776            name:           "depth_gt",
777            sql_op:         "nlevel({}) >",
778            category:       OperatorCategory::Path,
779            requires_array: false,
780            jsonb_operator: false,
781        },
782    );
783
784    m.insert(
785        "depth_lt",
786        OperatorInfo {
787            name:           "depth_lt",
788            sql_op:         "nlevel({}) <",
789            category:       OperatorCategory::Path,
790            requires_array: false,
791            jsonb_operator: false,
792        },
793    );
794
795    m.insert(
796        "isdescendant",
797        OperatorInfo {
798            name:           "isdescendant",
799            sql_op:         "<@",
800            category:       OperatorCategory::Path,
801            requires_array: false,
802            jsonb_operator: false,
803        },
804    );
805
806    // ========== SPATIAL/COORDINATE OPERATORS ==========
807    m.insert(
808        "distance_within",
809        OperatorInfo {
810            name:           "distance_within",
811            sql_op:         "distance_within",
812            category:       OperatorCategory::Spatial,
813            requires_array: false,
814            jsonb_operator: false,
815        },
816    );
817
818    // ========== JSONB ADVANCED OPERATORS ==========
819    m.insert(
820        "strictly_contains",
821        OperatorInfo {
822            name:           "strictly_contains",
823            sql_op:         "@>",
824            category:       OperatorCategory::Containment,
825            requires_array: false,
826            jsonb_operator: true,
827        },
828    );
829
830    // ========== ADDITIONAL ALIASES ==========
831    m.insert(
832        "neq",
833        OperatorInfo {
834            name:           "neq",
835            sql_op:         "!=",
836            category:       OperatorCategory::Comparison,
837            requires_array: false,
838            jsonb_operator: false,
839        },
840    );
841
842    m.insert(
843        "isnull",
844        OperatorInfo {
845            name:           "isnull",
846            sql_op:         "IS NULL",
847            category:       OperatorCategory::Null,
848            requires_array: false,
849            jsonb_operator: false,
850        },
851    );
852
853    m.insert(
854        "array_eq",
855        OperatorInfo {
856            name:           "array_eq",
857            sql_op:         "=",
858            category:       OperatorCategory::Array,
859            requires_array: false,
860            jsonb_operator: false,
861        },
862    );
863
864    m.insert(
865        "array_neq",
866        OperatorInfo {
867            name:           "array_neq",
868            sql_op:         "!=",
869            category:       OperatorCategory::Array,
870            requires_array: false,
871            jsonb_operator: false,
872        },
873    );
874
875    m.insert(
876        "array_contained_by",
877        OperatorInfo {
878            name:           "array_contained_by",
879            sql_op:         "<@",
880            category:       OperatorCategory::Array,
881            requires_array: false,
882            jsonb_operator: false,
883        },
884    );
885
886    m.insert(
887        "notin",
888        OperatorInfo {
889            name:           "notin",
890            sql_op:         "NOT IN",
891            category:       OperatorCategory::Comparison,
892            requires_array: true,
893            jsonb_operator: false,
894        },
895    );
896
897    m
898});
899
900/// Get operator information by name
901///
902/// # Example
903/// ```
904/// use fraiseql_core::utils::operators::get_operator_info;
905///
906/// let op = get_operator_info("eq").unwrap();
907/// assert_eq!(op.sql_op, "=");
908/// ```
909#[must_use]
910pub fn get_operator_info(name: &str) -> Option<&'static OperatorInfo> {
911    OPERATOR_REGISTRY.get(name)
912}
913
914/// Check if a string is a valid operator name
915///
916/// # Example
917/// ```
918/// use fraiseql_core::utils::operators::is_operator;
919///
920/// assert!(is_operator("eq"));
921/// assert!(is_operator("contains"));
922/// assert!(!is_operator("unknown_operator"));
923/// ```
924#[must_use]
925pub fn is_operator(name: &str) -> bool {
926    OPERATOR_REGISTRY.contains_key(name)
927}
928
929/// Get all operators in a specific category
930///
931/// # Example
932/// ```
933/// use fraiseql_core::utils::operators::{get_operators_by_category, OperatorCategory};
934///
935/// let comparison_ops = get_operators_by_category(OperatorCategory::Comparison);
936/// assert!(comparison_ops.len() >= 8);
937/// ```
938#[must_use]
939pub fn get_operators_by_category(category: OperatorCategory) -> Vec<&'static OperatorInfo> {
940    OPERATOR_REGISTRY.values().filter(|op| op.category == category).collect()
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946
947    #[test]
948    fn test_operator_registry_initialized() {
949        // Should have all 40+ operators
950        assert!(OPERATOR_REGISTRY.len() >= 40);
951    }
952
953    #[test]
954    fn test_comparison_operators() {
955        let operators = ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"];
956
957        for op_name in &operators {
958            let op = get_operator_info(op_name);
959            assert!(op.is_some(), "Operator {op_name} should exist");
960
961            let op = op.unwrap();
962            assert_eq!(op.category, OperatorCategory::Comparison);
963            assert!(!op.jsonb_operator);
964        }
965    }
966
967    #[test]
968    fn test_string_operators() {
969        let operators = [
970            "like", "ilike", "nlike", "nilike", "regex", "iregex", "nregex", "niregex",
971        ];
972
973        for op_name in &operators {
974            let op = get_operator_info(op_name);
975            assert!(op.is_some(), "String operator {op_name} should exist");
976
977            let op = op.unwrap();
978            assert_eq!(op.category, OperatorCategory::String);
979        }
980    }
981
982    #[test]
983    fn test_null_operators() {
984        let op1 = get_operator_info("is_null").unwrap();
985        assert_eq!(op1.sql_op, "IS NULL");
986        assert_eq!(op1.category, OperatorCategory::Null);
987
988        let op2 = get_operator_info("is_not_null").unwrap();
989        assert_eq!(op2.sql_op, "IS NOT NULL");
990        assert_eq!(op2.category, OperatorCategory::Null);
991    }
992
993    #[test]
994    fn test_containment_operators() {
995        let operators = [
996            "contains",
997            "contained_in",
998            "has_key",
999            "has_any_keys",
1000            "has_all_keys",
1001        ];
1002
1003        for op_name in &operators {
1004            let op = get_operator_info(op_name);
1005            assert!(op.is_some(), "Containment operator {op_name} should exist");
1006
1007            let op = op.unwrap();
1008            assert_eq!(op.category, OperatorCategory::Containment);
1009            assert!(op.jsonb_operator, "{op_name} should be JSONB operator");
1010        }
1011    }
1012
1013    #[test]
1014    fn test_array_operators() {
1015        let operators = ["array_contains", "array_contained_in", "array_overlaps"];
1016
1017        for op_name in &operators {
1018            let op = get_operator_info(op_name);
1019            assert!(op.is_some(), "Array operator {op_name} should exist");
1020
1021            let op = op.unwrap();
1022            assert_eq!(op.category, OperatorCategory::Array);
1023        }
1024    }
1025
1026    #[test]
1027    fn test_vector_operators() {
1028        let operators = [
1029            ("cosine_distance", "<=>"),
1030            ("l2_distance", "<->"),
1031            ("inner_product", "<#>"),
1032            ("l1_distance", "<+>"),
1033            ("hamming_distance", "<~>"),
1034            ("jaccard_distance", "<%>"),
1035        ];
1036
1037        for (op_name, expected_sql) in &operators {
1038            let op = get_operator_info(op_name);
1039            assert!(op.is_some(), "Vector operator {op_name} should exist");
1040
1041            let op = op.unwrap();
1042            assert_eq!(op.category, OperatorCategory::Vector);
1043            assert_eq!(op.sql_op, *expected_sql);
1044        }
1045    }
1046
1047    #[test]
1048    fn test_fulltext_operators() {
1049        let operators = [
1050            "search",
1051            "plainto_tsquery",
1052            "phraseto_tsquery",
1053            "websearch_to_tsquery",
1054        ];
1055
1056        for op_name in &operators {
1057            let op = get_operator_info(op_name);
1058            assert!(op.is_some(), "Fulltext operator {op_name} should exist");
1059
1060            let op = op.unwrap();
1061            assert_eq!(op.category, OperatorCategory::Fulltext);
1062            assert_eq!(op.sql_op, "@@");
1063        }
1064    }
1065
1066    #[test]
1067    fn test_is_operator() {
1068        assert!(is_operator("eq"));
1069        assert!(is_operator("contains"));
1070        assert!(is_operator("cosine_distance"));
1071        assert!(!is_operator("invalid_operator"));
1072        assert!(!is_operator(""));
1073    }
1074
1075    #[test]
1076    fn test_get_operators_by_category() {
1077        let comparison_ops = get_operators_by_category(OperatorCategory::Comparison);
1078        assert!(comparison_ops.len() >= 8);
1079
1080        let vector_ops = get_operators_by_category(OperatorCategory::Vector);
1081        assert!(vector_ops.len() >= 6);
1082
1083        let fulltext_ops = get_operators_by_category(OperatorCategory::Fulltext);
1084        assert!(fulltext_ops.len() >= 4);
1085    }
1086
1087    #[test]
1088    fn test_requires_array_flag() {
1089        // IN and NOT IN require arrays
1090        assert!(get_operator_info("in").unwrap().requires_array);
1091        assert!(get_operator_info("nin").unwrap().requires_array);
1092
1093        // Most operators don't require arrays
1094        assert!(!get_operator_info("eq").unwrap().requires_array);
1095        assert!(!get_operator_info("like").unwrap().requires_array);
1096    }
1097
1098    #[test]
1099    fn test_jsonb_operator_flag() {
1100        // Containment operators are JSONB-specific
1101        assert!(get_operator_info("contains").unwrap().jsonb_operator);
1102        assert!(get_operator_info("has_key").unwrap().jsonb_operator);
1103
1104        // Most operators are not JSONB-specific
1105        assert!(!get_operator_info("eq").unwrap().jsonb_operator);
1106        assert!(!get_operator_info("like").unwrap().jsonb_operator);
1107    }
1108}