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        "isLoopback",
575        OperatorInfo {
576            name:           "isLoopback",
577            sql_op:         "CIDR_RANGE_CHECK",
578            category:       OperatorCategory::Network,
579            requires_array: false,
580            jsonb_operator: false,
581        },
582    );
583
584    m.insert(
585        "isMulticast",
586        OperatorInfo {
587            name:           "isMulticast",
588            sql_op:         "CIDR_RANGE_CHECK",
589            category:       OperatorCategory::Network,
590            requires_array: false,
591            jsonb_operator: false,
592        },
593    );
594
595    m.insert(
596        "isLinkLocal",
597        OperatorInfo {
598            name:           "isLinkLocal",
599            sql_op:         "CIDR_RANGE_CHECK",
600            category:       OperatorCategory::Network,
601            requires_array: false,
602            jsonb_operator: false,
603        },
604    );
605
606    m.insert(
607        "isDocumentation",
608        OperatorInfo {
609            name:           "isDocumentation",
610            sql_op:         "CIDR_RANGE_CHECK",
611            category:       OperatorCategory::Network,
612            requires_array: false,
613            jsonb_operator: false,
614        },
615    );
616
617    m.insert(
618        "isCarrierGrade",
619        OperatorInfo {
620            name:           "isCarrierGrade",
621            sql_op:         "CIDR_RANGE_CHECK",
622            category:       OperatorCategory::Network,
623            requires_array: false,
624            jsonb_operator: false,
625        },
626    );
627
628    m.insert(
629        "inSubnet",
630        OperatorInfo {
631            name:           "inSubnet",
632            sql_op:         "{} <<= {}",
633            category:       OperatorCategory::Network,
634            requires_array: false,
635            jsonb_operator: false,
636        },
637    );
638
639    m.insert(
640        "notInSubnet",
641        OperatorInfo {
642            name:           "notInSubnet",
643            sql_op:         "NOT ({} <<= {})",
644            category:       OperatorCategory::Network,
645            requires_array: false,
646            jsonb_operator: false,
647        },
648    );
649
650    m.insert(
651        "subnet_contains",
652        OperatorInfo {
653            name:           "subnet_contains",
654            sql_op:         ">>",
655            category:       OperatorCategory::Network,
656            requires_array: false,
657            jsonb_operator: false,
658        },
659    );
660
661    m.insert(
662        "subnet_overlaps",
663        OperatorInfo {
664            name:           "subnet_overlaps",
665            sql_op:         "&&",
666            category:       OperatorCategory::Network,
667            requires_array: false,
668            jsonb_operator: false,
669        },
670    );
671
672    // ========== DATE/RANGE OPERATORS ==========
673    m.insert(
674        "contains_date",
675        OperatorInfo {
676            name:           "contains_date",
677            sql_op:         "@>",
678            category:       OperatorCategory::DateRange,
679            requires_array: false,
680            jsonb_operator: false,
681        },
682    );
683
684    m.insert(
685        "adjacent",
686        OperatorInfo {
687            name:           "adjacent",
688            sql_op:         "-|-",
689            category:       OperatorCategory::DateRange,
690            requires_array: false,
691            jsonb_operator: false,
692        },
693    );
694
695    m.insert(
696        "strictly_left",
697        OperatorInfo {
698            name:           "strictly_left",
699            sql_op:         "<<",
700            category:       OperatorCategory::DateRange,
701            requires_array: false,
702            jsonb_operator: false,
703        },
704    );
705
706    m.insert(
707        "strictly_right",
708        OperatorInfo {
709            name:           "strictly_right",
710            sql_op:         ">>",
711            category:       OperatorCategory::DateRange,
712            requires_array: false,
713            jsonb_operator: false,
714        },
715    );
716
717    m.insert(
718        "not_left",
719        OperatorInfo {
720            name:           "not_left",
721            sql_op:         "&>",
722            category:       OperatorCategory::DateRange,
723            requires_array: false,
724            jsonb_operator: false,
725        },
726    );
727
728    m.insert(
729        "not_right",
730        OperatorInfo {
731            name:           "not_right",
732            sql_op:         "&<",
733            category:       OperatorCategory::DateRange,
734            requires_array: false,
735            jsonb_operator: false,
736        },
737    );
738
739    m.insert(
740        "overlaps",
741        OperatorInfo {
742            name:           "overlaps",
743            sql_op:         "&&",
744            category:       OperatorCategory::DateRange,
745            requires_array: false,
746            jsonb_operator: false,
747        },
748    );
749
750    // ========== LTREE (HIERARCHICAL) OPERATORS ==========
751    m.insert(
752        "ancestor_of",
753        OperatorInfo {
754            name:           "ancestor_of",
755            sql_op:         "@>",
756            category:       OperatorCategory::Ltree,
757            requires_array: false,
758            jsonb_operator: false,
759        },
760    );
761
762    m.insert(
763        "descendant_of",
764        OperatorInfo {
765            name:           "descendant_of",
766            sql_op:         "<@",
767            category:       OperatorCategory::Ltree,
768            requires_array: false,
769            jsonb_operator: false,
770        },
771    );
772
773    m.insert(
774        "matches_lquery",
775        OperatorInfo {
776            name:           "matches_lquery",
777            sql_op:         "~",
778            category:       OperatorCategory::Ltree,
779            requires_array: false,
780            jsonb_operator: false,
781        },
782    );
783
784    m.insert(
785        "matches_ltxtquery",
786        OperatorInfo {
787            name:           "matches_ltxtquery",
788            sql_op:         "@",
789            category:       OperatorCategory::Ltree,
790            requires_array: false,
791            jsonb_operator: false,
792        },
793    );
794
795    m.insert(
796        "matches_any_lquery",
797        OperatorInfo {
798            name:           "matches_any_lquery",
799            sql_op:         "?",
800            category:       OperatorCategory::Ltree,
801            requires_array: true,
802            jsonb_operator: false,
803        },
804    );
805
806    // ========== PATH OPERATORS ==========
807    m.insert(
808        "depth_eq",
809        OperatorInfo {
810            name:           "depth_eq",
811            sql_op:         "nlevel({}) =",
812            category:       OperatorCategory::Path,
813            requires_array: false,
814            jsonb_operator: false,
815        },
816    );
817
818    m.insert(
819        "depth_gt",
820        OperatorInfo {
821            name:           "depth_gt",
822            sql_op:         "nlevel({}) >",
823            category:       OperatorCategory::Path,
824            requires_array: false,
825            jsonb_operator: false,
826        },
827    );
828
829    m.insert(
830        "depth_lt",
831        OperatorInfo {
832            name:           "depth_lt",
833            sql_op:         "nlevel({}) <",
834            category:       OperatorCategory::Path,
835            requires_array: false,
836            jsonb_operator: false,
837        },
838    );
839
840    m.insert(
841        "isdescendant",
842        OperatorInfo {
843            name:           "isdescendant",
844            sql_op:         "<@",
845            category:       OperatorCategory::Path,
846            requires_array: false,
847            jsonb_operator: false,
848        },
849    );
850
851    // ========== SPATIAL/COORDINATE OPERATORS ==========
852    m.insert(
853        "distance_within",
854        OperatorInfo {
855            name:           "distance_within",
856            sql_op:         "distance_within",
857            category:       OperatorCategory::Spatial,
858            requires_array: false,
859            jsonb_operator: false,
860        },
861    );
862
863    // ========== JSONB ADVANCED OPERATORS ==========
864    m.insert(
865        "strictly_contains",
866        OperatorInfo {
867            name:           "strictly_contains",
868            sql_op:         "@>",
869            category:       OperatorCategory::Containment,
870            requires_array: false,
871            jsonb_operator: true,
872        },
873    );
874
875    // ========== ADDITIONAL ALIASES ==========
876    m.insert(
877        "neq",
878        OperatorInfo {
879            name:           "neq",
880            sql_op:         "!=",
881            category:       OperatorCategory::Comparison,
882            requires_array: false,
883            jsonb_operator: false,
884        },
885    );
886
887    m.insert(
888        "isnull",
889        OperatorInfo {
890            name:           "isnull",
891            sql_op:         "IS NULL",
892            category:       OperatorCategory::Null,
893            requires_array: false,
894            jsonb_operator: false,
895        },
896    );
897
898    m.insert(
899        "array_eq",
900        OperatorInfo {
901            name:           "array_eq",
902            sql_op:         "=",
903            category:       OperatorCategory::Array,
904            requires_array: false,
905            jsonb_operator: false,
906        },
907    );
908
909    m.insert(
910        "array_neq",
911        OperatorInfo {
912            name:           "array_neq",
913            sql_op:         "!=",
914            category:       OperatorCategory::Array,
915            requires_array: false,
916            jsonb_operator: false,
917        },
918    );
919
920    m.insert(
921        "array_contained_by",
922        OperatorInfo {
923            name:           "array_contained_by",
924            sql_op:         "<@",
925            category:       OperatorCategory::Array,
926            requires_array: false,
927            jsonb_operator: false,
928        },
929    );
930
931    m.insert(
932        "notin",
933        OperatorInfo {
934            name:           "notin",
935            sql_op:         "NOT IN",
936            category:       OperatorCategory::Comparison,
937            requires_array: true,
938            jsonb_operator: false,
939        },
940    );
941
942    m
943});
944
945/// Get operator information by name
946///
947/// # Example
948/// ```
949/// use fraiseql_core::utils::operators::get_operator_info;
950///
951/// let op = get_operator_info("eq").unwrap();
952/// assert_eq!(op.sql_op, "=");
953/// ```
954#[must_use]
955pub fn get_operator_info(name: &str) -> Option<&'static OperatorInfo> {
956    OPERATOR_REGISTRY.get(name)
957}
958
959/// Check if a string is a valid operator name
960///
961/// # Example
962/// ```
963/// use fraiseql_core::utils::operators::is_operator;
964///
965/// assert!(is_operator("eq"));
966/// assert!(is_operator("contains"));
967/// assert!(!is_operator("unknown_operator"));
968/// ```
969#[must_use]
970pub fn is_operator(name: &str) -> bool {
971    OPERATOR_REGISTRY.contains_key(name)
972}
973
974/// Get all operators in a specific category
975///
976/// # Example
977/// ```
978/// use fraiseql_core::utils::operators::{get_operators_by_category, OperatorCategory};
979///
980/// let comparison_ops = get_operators_by_category(OperatorCategory::Comparison);
981/// assert!(comparison_ops.len() >= 8);
982/// ```
983#[must_use]
984pub fn get_operators_by_category(category: OperatorCategory) -> Vec<&'static OperatorInfo> {
985    OPERATOR_REGISTRY.values().filter(|op| op.category == category).collect()
986}