Skip to main content

fraiseql_db/where_clause/
operator_table.rs

1//! The one table of WHERE operator names.
2//!
3//! Before #828 there were two: `WhereOperator::match_exact` decided what the
4//! executor could *run*, and `fraiseql_core::utils::operators::OPERATOR_REGISTRY`
5//! decided what the REST transport would *accept*. They disagreed on 27 names,
6//! so `?status[ne]=archived` passed validation and then failed in the WHERE
7//! parser with `Unknown WHERE operator: ne` — and the 400's "Available
8//! operators" list recommended two dozen more names with the same behaviour.
9//!
10//! Both are now generated from [`WHERE_OPERATORS`] by the `where_operators!`
11//! macro below, so a name that is advertised is a name that runs, by
12//! construction. Adding a variant without giving it a name is a compile error;
13//! adding a name here advertises it everywhere at once.
14
15use super::WhereOperator;
16
17/// Broad family of an operator. Drives documentation grouping and the REST
18/// surface's error messages; it is not consulted during SQL generation.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum OperatorCategory {
22    /// Basic comparison: `=`, `!=`, `>`, `<`, `>=`, `<=`.
23    Comparison,
24    /// String operations: LIKE, ILIKE, regex.
25    String,
26    /// NULL checks: IS NULL, IS NOT NULL.
27    Null,
28    /// Array/list containment: `@>`, `<@`, `&&`.
29    Array,
30    /// pgvector distance operators.
31    Vector,
32    /// `PostgreSQL` full-text search.
33    Fulltext,
34    /// JSONB containment: `@>`, `<@`.
35    Containment,
36    /// Network/IP operators.
37    Network,
38    /// Ltree (hierarchical) operators.
39    Ltree,
40}
41
42/// Everything known about one WHERE operator.
43#[derive(Debug, Clone, Copy)]
44pub struct WhereOperatorSpec {
45    /// Canonical GraphQL name (e.g. `"eq"`).
46    pub name:           &'static str,
47    /// Accepted alternative spellings (e.g. `"ne"` for `"neq"`).
48    pub aliases:        &'static [&'static str],
49    /// SQL operator or function template, for documentation.
50    pub sql_op:         &'static str,
51    /// Family this operator belongs to.
52    pub category:       OperatorCategory,
53    /// Whether the operand must be a list.
54    pub requires_array: bool,
55    /// Whether the operator works on the JSONB value rather than its text form.
56    pub jsonb_operator: bool,
57}
58
59impl WhereOperatorSpec {
60    /// Canonical name and aliases, in that order.
61    pub fn all_names(&self) -> impl Iterator<Item = &'static str> + '_ {
62        std::iter::once(self.name).chain(self.aliases.iter().copied())
63    }
64}
65
66/// Generate [`WHERE_OPERATORS`] and `WhereOperator::match_exact` from one list.
67macro_rules! where_operators {
68    ($(
69        $variant:ident, $name:literal, [$($alias:literal),*], $sql:literal,
70        $category:ident, $array:literal, $jsonb:literal;
71    )*) => {
72        /// Every WHERE operator the executor can run, with the names it answers to.
73        pub const WHERE_OPERATORS: &[WhereOperatorSpec] = &[
74            $(WhereOperatorSpec {
75                name:           $name,
76                aliases:        &[$($alias),*],
77                sql_op:         $sql,
78                category:       OperatorCategory::$category,
79                requires_array: $array,
80                jsonb_operator: $jsonb,
81            }),*
82        ];
83
84        impl WhereOperator {
85            /// Match an operator name exactly against the known set.
86            pub(super) fn match_exact(s: &str) -> Option<Self> {
87                match s {
88                    $($name $(| $alias)* => Some(Self::$variant),)*
89                    _ => None,
90                }
91            }
92        }
93    };
94}
95
96where_operators! {
97    // ── Comparison ────────────────────────────────────────────────────────
98    Eq,  "eq",  [],     "=",      Comparison, false, false;
99    Neq, "neq", ["ne"], "!=",     Comparison, false, false;
100    Gt,  "gt",  [],     ">",      Comparison, false, false;
101    Gte, "gte", [],     ">=",     Comparison, false, false;
102    Lt,  "lt",  [],     "<",      Comparison, false, false;
103    Lte, "lte", [],     "<=",     Comparison, false, false;
104    In,  "in",  [],     "IN",     Comparison, true,  false;
105    Nin, "nin", ["notin"], "NOT IN", Comparison, true, false;
106
107    // ── NULL ──────────────────────────────────────────────────────────────
108    IsNull,    "isnull",      ["is_null"],     "IS NULL",     Null, false, false;
109    IsNotNull, "is_not_null", ["isnotnull"],   "IS NOT NULL", Null, false, false;
110
111    // ── String ────────────────────────────────────────────────────────────
112    Contains,    "contains",    [], "LIKE",     String, false, false;
113    Icontains,   "icontains",   [], "ILIKE",    String, false, false;
114    Startswith,  "startswith",  [], "LIKE",     String, false, false;
115    Istartswith, "istartswith", [], "ILIKE",    String, false, false;
116    Endswith,    "endswith",    [], "LIKE",     String, false, false;
117    Iendswith,   "iendswith",   [], "ILIKE",    String, false, false;
118    Like,        "like",        [], "LIKE",     String, false, false;
119    Ilike,       "ilike",       [], "ILIKE",    String, false, false;
120    Nlike,       "nlike",       [], "NOT LIKE", String, false, false;
121    Nilike,      "nilike",      [], "NOT ILIKE", String, false, false;
122    Regex,       "regex",       [], "~",        String, false, false;
123    Iregex,      "iregex",      ["imatches"],    "~*",  String, false, false;
124    Nregex,      "nregex",      ["not_matches"], "!~",  String, false, false;
125    Niregex,     "niregex",     [], "!~*",      String, false, false;
126
127    // ── Array ─────────────────────────────────────────────────────────────
128    ArrayContains,     "array_contains",     [], "@>", Array, false, false;
129    ArrayContainedBy,  "array_contained_by", ["array_contained_in", "contained_in"], "<@",
130                       Array, false, false;
131    ArrayOverlaps,     "array_overlaps",     [], "&&", Array, false, false;
132    LenEq,             "len_eq",             [], "jsonb_array_length =",  Array, false, true;
133    LenNeq,            "len_neq",            [], "jsonb_array_length !=", Array, false, true;
134    LenGt,             "len_gt",             [], "jsonb_array_length >",  Array, false, true;
135    LenGte,            "len_gte",            [], "jsonb_array_length >=", Array, false, true;
136    LenLt,             "len_lt",             [], "jsonb_array_length <",  Array, false, true;
137    LenLte,            "len_lte",            [], "jsonb_array_length <=", Array, false, true;
138
139    // ── JSONB containment ─────────────────────────────────────────────────
140    StrictlyContains, "strictly_contains", [], "@>", Containment, false, true;
141
142    // ── Vector (pgvector) ─────────────────────────────────────────────────
143    CosineDistance,  "cosine_distance",  [], "<=>", Vector, false, false;
144    L2Distance,      "l2_distance",      [], "<->", Vector, false, false;
145    L1Distance,      "l1_distance",      [], "<+>", Vector, false, false;
146    HammingDistance, "hamming_distance", [], "<~>", Vector, false, false;
147    InnerProduct,    "inner_product",    [], "<#>", Vector, false, false;
148    JaccardDistance, "jaccard_distance", [], "<%>", Vector, false, false;
149
150    // ── Full-text search ──────────────────────────────────────────────────
151    Matches,        "matches",         ["search"],               "@@", Fulltext, false, false;
152    PlainQuery,     "plain_query",     ["plainto_tsquery"],      "@@", Fulltext, false, false;
153    PhraseQuery,    "phrase_query",    ["phraseto_tsquery"],     "@@", Fulltext, false, false;
154    WebsearchQuery, "websearch_query", ["websearch_to_tsquery"], "@@", Fulltext, false, false;
155
156    // ── Network (INET/CIDR) ───────────────────────────────────────────────
157    IsIPv4,           "is_ipv4",           ["isIPv4"],  "family({}) = 4",   Network, false, false;
158    IsIPv6,           "is_ipv6",           ["isIPv6"],  "family({}) = 6",   Network, false, false;
159    IsPrivate,        "is_private",        ["isPrivate"],        "CIDR_RANGE_CHECK", Network, false, false;
160    IsLoopback,       "is_loopback",       ["isLoopback"],       "CIDR_RANGE_CHECK", Network, false, false;
161    IsMulticast,      "is_multicast",      ["isMulticast"],      "CIDR_RANGE_CHECK", Network, false, false;
162    IsLinkLocal,      "is_link_local",     ["isLinkLocal"],      "CIDR_RANGE_CHECK", Network, false, false;
163    IsDocumentation,  "is_documentation",  ["isDocumentation"],  "CIDR_RANGE_CHECK", Network, false, false;
164    IsCarrierGrade,   "is_carrier_grade",  ["isCarrierGrade"],   "CIDR_RANGE_CHECK", Network, false, false;
165    InSubnet,         "in_subnet",         ["inrange", "inSubnet"], "<<",   Network, false, false;
166    ContainsSubnet,   "contains_subnet",   ["subnet_contains"],  ">>",      Network, false, false;
167    ContainsIP,       "contains_ip",       [],                   ">>",      Network, false, false;
168    Overlaps,         "overlaps",          ["subnet_overlaps"],  "&&",      Network, false, false;
169
170    // ── Ltree ─────────────────────────────────────────────────────────────
171    AncestorOf,       "ancestor_of",        [],               "@>", Ltree, false, false;
172    DescendantOf,     "descendant_of",      ["isdescendant"], "<@", Ltree, false, false;
173    MatchesLquery,    "matches_lquery",     [], "~", Ltree, false, false;
174    MatchesLtxtquery, "matches_ltxtquery",  [], "@", Ltree, false, false;
175    MatchesAnyLquery, "matches_any_lquery", [], "?", Ltree, true,  false;
176    DepthEq,          "depth_eq",           [], "nlevel({}) =",  Ltree, false, false;
177    DepthNeq,         "depth_neq",          [], "nlevel({}) !=", Ltree, false, false;
178    DepthGt,          "depth_gt",           [], "nlevel({}) >",  Ltree, false, false;
179    DepthGte,         "depth_gte",          [], "nlevel({}) >=", Ltree, false, false;
180    DepthLt,          "depth_lt",           [], "nlevel({}) <",  Ltree, false, false;
181    DepthLte,         "depth_lte",          [], "nlevel({}) <=", Ltree, false, false;
182    Lca,              "lca",                [], "lca()", Ltree, true, false;
183    DescendantOfId,   "descendant_of_id",   [], "<@", Ltree, false, false;
184    AncestorOfId,     "ancestor_of_id",     [], "@>", Ltree, false, false;
185}
186
187/// Look up an operator's specification by any of its accepted names.
188#[must_use]
189pub fn operator_spec(name: &str) -> Option<&'static WhereOperatorSpec> {
190    WHERE_OPERATORS.iter().find(|spec| spec.all_names().any(|n| n == name))
191}
192
193#[cfg(test)]
194#[path = "operator_table_tests.rs"]
195mod operator_table_tests;