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