Skip to main content

fraiseql_db/
where_clause.rs

1//! WHERE clause abstract syntax tree.
2
3use fraiseql_error::{FraiseQLError, Result};
4use serde::{Deserialize, Serialize};
5
6use crate::utils::to_snake_case;
7
8/// WHERE clause abstract syntax tree.
9///
10/// Represents a type-safe WHERE condition that can be compiled to database-specific SQL.
11///
12/// # Example
13///
14/// ```rust
15/// use fraiseql_db::{WhereClause, WhereOperator};
16/// use serde_json::json;
17///
18/// // Simple condition: email ILIKE '%example.com%'
19/// let where_clause = WhereClause::Field {
20///     path: vec!["email".to_string()],
21///     operator: WhereOperator::Icontains,
22///     value: json!("example.com"),
23/// };
24///
25/// // Complex condition: (published = true) AND (views >= 100)
26/// let where_clause = WhereClause::And(vec![
27///     WhereClause::Field {
28///         path: vec!["published".to_string()],
29///         operator: WhereOperator::Eq,
30///         value: json!(true),
31///     },
32///     WhereClause::Field {
33///         path: vec!["views".to_string()],
34///         operator: WhereOperator::Gte,
35///         value: json!(100),
36///     },
37/// ]);
38/// ```
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40#[non_exhaustive]
41pub enum WhereClause {
42    /// Single field condition.
43    Field {
44        /// JSONB path (e.g., `["email"]` or `["posts", "title"]`).
45        path:     Vec<String>,
46        /// Comparison operator.
47        operator: WhereOperator,
48        /// Value to compare against.
49        value:    serde_json::Value,
50    },
51
52    /// Logical AND of multiple conditions.
53    And(Vec<WhereClause>),
54
55    /// Logical OR of multiple conditions.
56    Or(Vec<WhereClause>),
57
58    /// Logical NOT of a condition.
59    Not(Box<WhereClause>),
60
61    /// Native column condition — bypasses JSONB extraction.
62    ///
63    /// Used when a direct query argument maps to a native column on `sql_source`,
64    /// detected at compile time. Generates `"column" = $N` (with an optional
65    /// PostgreSQL type cast on the parameter, e.g. `$1::uuid`) instead of the
66    /// default `data->>'column' = $N`.
67    NativeField {
68        /// Native column name (e.g., `"id"`).
69        column:   String,
70        /// PostgreSQL parameter cast suffix (e.g., `"uuid"`, `"int4"`).
71        /// Empty string means no cast is applied.
72        pg_cast:  String,
73        /// Comparison operator.
74        operator: WhereOperator,
75        /// Value to compare against.
76        value:    serde_json::Value,
77    },
78}
79
80impl WhereClause {
81    /// Check if WHERE clause is empty.
82    #[must_use]
83    pub const fn is_empty(&self) -> bool {
84        match self {
85            Self::And(clauses) | Self::Or(clauses) => clauses.is_empty(),
86            Self::Not(_) | Self::Field { .. } | Self::NativeField { .. } => false,
87        }
88    }
89
90    /// Collect all native column names referenced in this WHERE clause.
91    ///
92    /// Used to enrich error messages when a native column does not exist on the
93    /// target table — the caller can hint that the column was auto-inferred from
94    /// an `ID`/`UUID`-typed argument and suggest adding the column or using
95    /// explicit `native_columns` annotation.
96    #[must_use]
97    pub fn native_column_names(&self) -> Vec<&str> {
98        let mut names = Vec::new();
99        self.collect_native_column_names(&mut names);
100        names
101    }
102
103    fn collect_native_column_names<'a>(&'a self, out: &mut Vec<&'a str>) {
104        match self {
105            Self::And(clauses) | Self::Or(clauses) => {
106                for c in clauses {
107                    c.collect_native_column_names(out);
108                }
109            },
110            Self::Not(inner) => inner.collect_native_column_names(out),
111            Self::NativeField { column, .. } => out.push(column),
112            Self::Field { .. } => {},
113        }
114    }
115
116    /// Parse a `WhereClause` from a nested GraphQL JSON `where` variable.
117    ///
118    /// Expected format (nested object with field → operator → value):
119    /// ```json
120    /// {
121    ///   "status": { "eq": "active" },
122    ///   "name": { "icontains": "john" },
123    ///   "_and": [ { "age": { "gte": 18 } }, { "age": { "lte": 65 } } ],
124    ///   "_or": [ { "role": { "eq": "admin" } } ],
125    ///   "_not": { "deleted": { "eq": true } }
126    /// }
127    /// ```
128    ///
129    /// Each top-level key is either a field name (mapped to `WhereClause::Field`
130    /// with operator sub-keys) or a logical combinator (`_and`, `_or`, `_not`).
131    /// Multiple top-level keys are combined with AND.
132    ///
133    /// # Errors
134    ///
135    /// Returns `FraiseQLError::Validation` if the JSON structure is invalid or
136    /// contains unknown operators.
137    ///
138    /// # Panics
139    ///
140    /// Cannot panic: the internal `.expect("checked len == 1")` is only reached
141    /// after verifying `conditions.len() == 1`.
142    pub fn from_graphql_json(value: &serde_json::Value) -> Result<Self> {
143        Self::parse_where_object(value, &[])
144    }
145
146    /// Recursive WHERE parser that builds multi-segment paths for nested objects.
147    ///
148    /// When parsing `{ machine: { id: { eq: "..." } } }`:
149    /// 1. Key `machine`, value is `{ id: { eq: "..." } }` — not an operator map.
150    /// 2. Recurse with path prefix `["machine"]`.
151    /// 3. Key `id`, value is `{ eq: "..." }` — this IS an operator map.
152    /// 4. Emit `Field { path: ["machine", "id"], operator: Eq, value: "..." }`.
153    ///
154    /// The multi-segment path is then handled by `GenericWhereGenerator`, which
155    /// checks `IndexedColumnsCache` for `machine__id` (native column with index)
156    /// and falls back to JSONB extraction (`data->'machine'->>'id'`).
157    fn parse_where_object(value: &serde_json::Value, path_prefix: &[String]) -> Result<Self> {
158        let Some(obj) = value.as_object() else {
159            return Err(FraiseQLError::Validation {
160                message: "where clause must be a JSON object".to_string(),
161                path:    None,
162            });
163        };
164
165        let mut conditions = Vec::new();
166
167        for (key, val) in obj {
168            match key.as_str() {
169                "_and" => {
170                    let arr = val.as_array().ok_or_else(|| FraiseQLError::Validation {
171                        message: "_and must be an array".to_string(),
172                        path:    None,
173                    })?;
174                    let sub: Result<Vec<Self>> =
175                        arr.iter().map(|v| Self::parse_where_object(v, path_prefix)).collect();
176                    conditions.push(Self::And(sub?));
177                },
178                "_or" => {
179                    let arr = val.as_array().ok_or_else(|| FraiseQLError::Validation {
180                        message: "_or must be an array".to_string(),
181                        path:    None,
182                    })?;
183                    let sub: Result<Vec<Self>> =
184                        arr.iter().map(|v| Self::parse_where_object(v, path_prefix)).collect();
185                    conditions.push(Self::Or(sub?));
186                },
187                "_not" => {
188                    let sub = Self::parse_where_object(val, path_prefix)?;
189                    conditions.push(Self::Not(Box::new(sub)));
190                },
191                field_name => {
192                    let ops = val.as_object().ok_or_else(|| FraiseQLError::Validation {
193                        message: format!(
194                            "where field '{field_name}' must be an object of {{operator: value}}"
195                        ),
196                        path:    None,
197                    })?;
198                    let mut field_path = path_prefix.to_vec();
199                    field_path.push(to_snake_case(field_name));
200
201                    for (op_str, op_val) in ops {
202                        match WhereOperator::from_str(op_str) {
203                            Ok(operator) => {
204                                conditions.push(Self::Field {
205                                    path: field_path.clone(),
206                                    operator,
207                                    value: op_val.clone(),
208                                });
209                            },
210                            Err(_) if op_val.is_object() => {
211                                // Nested relation/object filter: recurse with extended path.
212                                // e.g., { machine: { id: { eq: "..." } } }
213                                //   → path_prefix=["machine"], key="id", value={ eq: "..." }
214                                let nested_json = serde_json::json!({ op_str: op_val });
215                                let nested = Self::parse_where_object(&nested_json, &field_path)?;
216                                conditions.push(nested);
217                            },
218                            Err(e) => return Err(e),
219                        }
220                    }
221                },
222            }
223        }
224
225        if conditions.len() == 1 {
226            // Reason: iterator has exactly one element — length was checked on the line above
227            Ok(conditions.into_iter().next().expect("checked len == 1"))
228        } else {
229            Ok(Self::And(conditions))
230        }
231    }
232}
233
234/// Maximum nesting depth for recursive WHERE field parsing.
235/// WHERE operators (FraiseQL v1 compatibility).
236///
237/// All standard operators are supported.
238/// No underscore prefix (e.g., `eq`, `icontains`, not `_eq`, `_icontains`).
239///
240/// Note: ExtendedOperator variants may contain f64 values which don't implement Eq,
241/// so WhereOperator derives PartialEq only (not Eq).
242///
243/// This enum is marked `#[non_exhaustive]` so that new operators (e.g., `Between`,
244/// `Similar`) can be added in future minor versions without breaking downstream
245/// exhaustive `match` expressions.
246#[non_exhaustive]
247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
248pub enum WhereOperator {
249    // ========================================================================
250    // Comparison Operators
251    // ========================================================================
252    /// Equal (=).
253    Eq,
254    /// Not equal (!=).
255    Neq,
256    /// Greater than (>).
257    Gt,
258    /// Greater than or equal (>=).
259    Gte,
260    /// Less than (<).
261    Lt,
262    /// Less than or equal (<=).
263    Lte,
264
265    // ========================================================================
266    // Containment Operators
267    // ========================================================================
268    /// In list (IN).
269    In,
270    /// Not in list (NOT IN).
271    Nin,
272
273    // ========================================================================
274    // String Operators
275    // ========================================================================
276    /// Contains substring (LIKE '%value%').
277    Contains,
278    /// Contains substring (case-insensitive) (ILIKE '%value%').
279    Icontains,
280    /// Starts with (LIKE 'value%').
281    Startswith,
282    /// Starts with (case-insensitive) (ILIKE 'value%').
283    Istartswith,
284    /// Ends with (LIKE '%value').
285    Endswith,
286    /// Ends with (case-insensitive) (ILIKE '%value').
287    Iendswith,
288    /// Pattern matching (LIKE).
289    Like,
290    /// Pattern matching (case-insensitive) (ILIKE).
291    Ilike,
292    /// Negated pattern matching (NOT LIKE).
293    Nlike,
294    /// Negated pattern matching (case-insensitive) (NOT ILIKE).
295    Nilike,
296    /// POSIX regex match (~).
297    Regex,
298    /// POSIX regex match (case-insensitive) (~*).
299    Iregex,
300    /// Negated POSIX regex match (!~).
301    Nregex,
302    /// Negated POSIX regex match (case-insensitive) (!~*).
303    Niregex,
304
305    // ========================================================================
306    // Null Checks
307    // ========================================================================
308    /// Is null (IS NULL or IS NOT NULL).
309    IsNull,
310
311    // ========================================================================
312    // Array Operators
313    // ========================================================================
314    /// Array contains (@>).
315    ArrayContains,
316    /// Array contained by (<@).
317    ArrayContainedBy,
318    /// Array overlaps (&&).
319    ArrayOverlaps,
320    /// Array length equal.
321    LenEq,
322    /// Array length greater than.
323    LenGt,
324    /// Array length less than.
325    LenLt,
326    /// Array length greater than or equal.
327    LenGte,
328    /// Array length less than or equal.
329    LenLte,
330    /// Array length not equal.
331    LenNeq,
332
333    // ========================================================================
334    // Vector Operators (pgvector)
335    // ========================================================================
336    /// Cosine distance (<=>).
337    CosineDistance,
338    /// L2 (Euclidean) distance (<->).
339    L2Distance,
340    /// L1 (Manhattan) distance (<+>).
341    L1Distance,
342    /// Hamming distance (<~>).
343    HammingDistance,
344    /// Inner product (<#>). Higher values = more similar.
345    InnerProduct,
346    /// Jaccard distance for set similarity.
347    JaccardDistance,
348
349    // ========================================================================
350    // Full-Text Search
351    // ========================================================================
352    /// Full-text search (@@).
353    Matches,
354    /// Plain text query (plainto_tsquery).
355    PlainQuery,
356    /// Phrase query (phraseto_tsquery).
357    PhraseQuery,
358    /// Web search query (websearch_to_tsquery).
359    WebsearchQuery,
360
361    // ========================================================================
362    // Network Operators (INET/CIDR)
363    // ========================================================================
364    /// Is IPv4.
365    IsIPv4,
366    /// Is IPv6.
367    IsIPv6,
368    /// Is private IP (RFC1918 ranges). Value controls negation (false = public).
369    IsPrivate,
370    /// Is loopback address (127.0.0.0/8 or ::1). Value controls negation.
371    IsLoopback,
372    /// Is multicast (224.0.0.0/4 or ff00::/8). Value controls negation.
373    IsMulticast,
374    /// Is link-local (169.254.0.0/16 or fe80::/10). Value controls negation.
375    IsLinkLocal,
376    /// Is documentation range (RFC 5737/3849). Value controls negation.
377    IsDocumentation,
378    /// Is carrier-grade NAT (100.64.0.0/10, RFC 6598). Value controls negation.
379    IsCarrierGrade,
380    /// In subnet (<<) - IP is contained within subnet.
381    InSubnet,
382    /// Contains subnet (>>) - subnet contains another subnet.
383    ContainsSubnet,
384    /// Contains IP (>>) - subnet contains an IP address.
385    ContainsIP,
386    /// Overlaps (&&) - subnets overlap.
387    Overlaps,
388
389    // ========================================================================
390    // JSONB Operators
391    // ========================================================================
392    /// Strictly contains (@>).
393    StrictlyContains,
394
395    // ========================================================================
396    // LTree Operators (Hierarchical)
397    // ========================================================================
398    /// Ancestor of (@>).
399    AncestorOf,
400    /// Descendant of (<@).
401    DescendantOf,
402    /// Matches lquery (~).
403    MatchesLquery,
404    /// Matches ltxtquery (@) - Boolean query syntax.
405    MatchesLtxtquery,
406    /// Matches any lquery (?).
407    MatchesAnyLquery,
408    /// Depth equal (nlevel() =).
409    DepthEq,
410    /// Depth not equal (nlevel() !=).
411    DepthNeq,
412    /// Depth greater than (nlevel() >).
413    DepthGt,
414    /// Depth greater than or equal (nlevel() >=).
415    DepthGte,
416    /// Depth less than (nlevel() <).
417    DepthLt,
418    /// Depth less than or equal (nlevel() <=).
419    DepthLte,
420    /// Lowest common ancestor (lca()).
421    Lca,
422
423    // ========================================================================
424    // LTree ID-Based Operators (resolve path from entity UUID)
425    // ========================================================================
426    /// Descendant of entity by ID: `path <@ (SELECT path FROM t WHERE id = $1)`.
427    DescendantOfId,
428    /// Ancestor of entity by ID: `path @> (SELECT path FROM t WHERE id = $1)`.
429    AncestorOfId,
430
431    // ========================================================================
432    // Extended Operators (Rich Type Filters)
433    // ========================================================================
434    /// Extended operator for rich scalar types (Email, VIN, CountryCode, etc.)
435    /// These operators are specialized filters enabled via feature flags.
436    /// See `fraiseql_core::filters::ExtendedOperator` for available operators.
437    #[serde(skip)]
438    Extended(crate::filters::ExtendedOperator),
439}
440
441impl WhereOperator {
442    /// Parse operator from string (GraphQL input).
443    ///
444    /// # Errors
445    ///
446    /// Returns `FraiseQLError::Validation` if operator name is unknown.
447    #[allow(clippy::should_implement_trait)] // Reason: intentionally not implementing `FromStr` because this returns `FraiseQLError`, not `<Self as FromStr>::Err`.
448    pub fn from_str(s: &str) -> Result<Self> {
449        if let Some(op) = Self::match_exact(s) {
450            return Ok(op);
451        }
452
453        // If the name has no underscores and contains uppercase letters, it may
454        // be a camelCase form of a registered snake_case operator. Convert and
455        // retry. This avoids allocation when the first match succeeds.
456        if !s.contains('_') && s.chars().any(char::is_uppercase) {
457            let snake = crate::utils::to_snake_case(s);
458            if let Some(op) = Self::match_exact(&snake) {
459                return Ok(op);
460            }
461        }
462
463        Err(FraiseQLError::validation(format!("Unknown WHERE operator: {s}")))
464    }
465
466    /// Match an operator name exactly against the known set.
467    fn match_exact(s: &str) -> Option<Self> {
468        match s {
469            "eq" => Some(Self::Eq),
470            "neq" => Some(Self::Neq),
471            "gt" => Some(Self::Gt),
472            "gte" => Some(Self::Gte),
473            "lt" => Some(Self::Lt),
474            "lte" => Some(Self::Lte),
475            "in" => Some(Self::In),
476            "nin" | "notin" => Some(Self::Nin),
477            "contains" => Some(Self::Contains),
478            "icontains" => Some(Self::Icontains),
479            "startswith" => Some(Self::Startswith),
480            "istartswith" => Some(Self::Istartswith),
481            "endswith" => Some(Self::Endswith),
482            "iendswith" => Some(Self::Iendswith),
483            "like" => Some(Self::Like),
484            "ilike" => Some(Self::Ilike),
485            "nlike" => Some(Self::Nlike),
486            "nilike" => Some(Self::Nilike),
487            "regex" => Some(Self::Regex),
488            "iregex" | "imatches" => Some(Self::Iregex),
489            "nregex" | "not_matches" => Some(Self::Nregex),
490            "niregex" => Some(Self::Niregex),
491            "isnull" => Some(Self::IsNull),
492            "array_contains" => Some(Self::ArrayContains),
493            "array_contained_by" => Some(Self::ArrayContainedBy),
494            "array_overlaps" => Some(Self::ArrayOverlaps),
495            "len_eq" => Some(Self::LenEq),
496            "len_gt" => Some(Self::LenGt),
497            "len_lt" => Some(Self::LenLt),
498            "len_gte" => Some(Self::LenGte),
499            "len_lte" => Some(Self::LenLte),
500            "len_neq" => Some(Self::LenNeq),
501            "cosine_distance" => Some(Self::CosineDistance),
502            "l2_distance" => Some(Self::L2Distance),
503            "l1_distance" => Some(Self::L1Distance),
504            "hamming_distance" => Some(Self::HammingDistance),
505            "inner_product" => Some(Self::InnerProduct),
506            "jaccard_distance" => Some(Self::JaccardDistance),
507            "matches" => Some(Self::Matches),
508            "plain_query" => Some(Self::PlainQuery),
509            "phrase_query" => Some(Self::PhraseQuery),
510            "websearch_query" => Some(Self::WebsearchQuery),
511            "is_ipv4" => Some(Self::IsIPv4),
512            "is_ipv6" => Some(Self::IsIPv6),
513            "is_private" => Some(Self::IsPrivate),
514            "is_loopback" => Some(Self::IsLoopback),
515            "is_multicast" => Some(Self::IsMulticast),
516            "is_link_local" => Some(Self::IsLinkLocal),
517            "is_documentation" => Some(Self::IsDocumentation),
518            "is_carrier_grade" => Some(Self::IsCarrierGrade),
519            "in_subnet" | "inrange" => Some(Self::InSubnet),
520            "contains_subnet" => Some(Self::ContainsSubnet),
521            "contains_ip" => Some(Self::ContainsIP),
522            "overlaps" => Some(Self::Overlaps),
523            "strictly_contains" => Some(Self::StrictlyContains),
524            "ancestor_of" => Some(Self::AncestorOf),
525            "descendant_of" => Some(Self::DescendantOf),
526            "matches_lquery" => Some(Self::MatchesLquery),
527            "matches_ltxtquery" => Some(Self::MatchesLtxtquery),
528            "matches_any_lquery" => Some(Self::MatchesAnyLquery),
529            "depth_eq" => Some(Self::DepthEq),
530            "depth_neq" => Some(Self::DepthNeq),
531            "depth_gt" => Some(Self::DepthGt),
532            "depth_gte" => Some(Self::DepthGte),
533            "depth_lt" => Some(Self::DepthLt),
534            "depth_lte" => Some(Self::DepthLte),
535            "lca" => Some(Self::Lca),
536            "descendant_of_id" => Some(Self::DescendantOfId),
537            "ancestor_of_id" => Some(Self::AncestorOfId),
538            _ => None,
539        }
540    }
541
542    /// Check if operator requires array value.
543    #[must_use]
544    pub const fn expects_array(&self) -> bool {
545        matches!(self, Self::In | Self::Nin)
546    }
547
548    /// Check if operator is case-insensitive.
549    #[must_use]
550    pub const fn is_case_insensitive(&self) -> bool {
551        matches!(
552            self,
553            Self::Icontains
554                | Self::Istartswith
555                | Self::Iendswith
556                | Self::Ilike
557                | Self::Nilike
558                | Self::Iregex
559                | Self::Niregex
560        )
561    }
562
563    /// Check if operator works with strings.
564    #[must_use]
565    pub const fn is_string_operator(&self) -> bool {
566        matches!(
567            self,
568            Self::Contains
569                | Self::Icontains
570                | Self::Startswith
571                | Self::Istartswith
572                | Self::Endswith
573                | Self::Iendswith
574                | Self::Like
575                | Self::Ilike
576                | Self::Nlike
577                | Self::Nilike
578                | Self::Regex
579                | Self::Iregex
580                | Self::Nregex
581                | Self::Niregex
582        )
583    }
584}
585
586/// HAVING clause abstract syntax tree.
587///
588/// HAVING filters aggregated results after GROUP BY, while WHERE filters rows before aggregation.
589///
590/// # Example
591///
592/// ```rust
593/// use fraiseql_db::{HavingClause, WhereOperator};
594/// use serde_json::json;
595///
596/// // Simple condition: COUNT(*) > 10
597/// let having_clause = HavingClause::Aggregate {
598///     aggregate: "count".to_string(),
599///     operator: WhereOperator::Gt,
600///     value: json!(10),
601/// };
602///
603/// // Complex condition: (COUNT(*) > 10) AND (SUM(revenue) >= 1000)
604/// let having_clause = HavingClause::And(vec![
605///     HavingClause::Aggregate {
606///         aggregate: "count".to_string(),
607///         operator: WhereOperator::Gt,
608///         value: json!(10),
609///     },
610///     HavingClause::Aggregate {
611///         aggregate: "revenue_sum".to_string(),
612///         operator: WhereOperator::Gte,
613///         value: json!(1000),
614///     },
615/// ]);
616/// ```
617#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
618#[non_exhaustive]
619pub enum HavingClause {
620    /// Aggregate field condition (e.g., count_gt, revenue_sum_gte).
621    Aggregate {
622        /// Aggregate name: "count" or "field_function" (e.g., "revenue_sum").
623        aggregate: String,
624        /// Comparison operator.
625        operator:  WhereOperator,
626        /// Value to compare against.
627        value:     serde_json::Value,
628    },
629
630    /// Logical AND of multiple conditions.
631    And(Vec<HavingClause>),
632
633    /// Logical OR of multiple conditions.
634    Or(Vec<HavingClause>),
635
636    /// Logical NOT of a condition.
637    Not(Box<HavingClause>),
638}
639
640impl HavingClause {
641    /// Check if HAVING clause is empty.
642    #[must_use]
643    pub const fn is_empty(&self) -> bool {
644        match self {
645            Self::And(clauses) | Self::Or(clauses) => clauses.is_empty(),
646            Self::Not(_) | Self::Aggregate { .. } => false,
647        }
648    }
649}
650
651#[cfg(test)]
652mod tests;