Skip to main content

fraiseql_core/db/
where_clause.rs

1//! WHERE clause abstract syntax tree.
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{FraiseQLError, Result};
6
7/// WHERE clause abstract syntax tree.
8///
9/// Represents a type-safe WHERE condition that can be compiled to database-specific SQL.
10///
11/// # Example
12///
13/// ```rust
14/// use fraiseql_core::db::{WhereClause, WhereOperator};
15/// use serde_json::json;
16///
17/// // Simple condition: email ILIKE '%example.com%'
18/// let where_clause = WhereClause::Field {
19///     path: vec!["email".to_string()],
20///     operator: WhereOperator::Icontains,
21///     value: json!("example.com"),
22/// };
23///
24/// // Complex condition: (published = true) AND (views >= 100)
25/// let where_clause = WhereClause::And(vec![
26///     WhereClause::Field {
27///         path: vec!["published".to_string()],
28///         operator: WhereOperator::Eq,
29///         value: json!(true),
30///     },
31///     WhereClause::Field {
32///         path: vec!["views".to_string()],
33///         operator: WhereOperator::Gte,
34///         value: json!(100),
35///     },
36/// ]);
37/// ```
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub enum WhereClause {
40    /// Single field condition.
41    Field {
42        /// JSONB path (e.g., ["email"] or ["posts", "title"]).
43        path:     Vec<String>,
44        /// Comparison operator.
45        operator: WhereOperator,
46        /// Value to compare against.
47        value:    serde_json::Value,
48    },
49
50    /// Logical AND of multiple conditions.
51    And(Vec<WhereClause>),
52
53    /// Logical OR of multiple conditions.
54    Or(Vec<WhereClause>),
55
56    /// Logical NOT of a condition.
57    Not(Box<WhereClause>),
58}
59
60impl WhereClause {
61    /// Check if WHERE clause is empty.
62    #[must_use]
63    pub fn is_empty(&self) -> bool {
64        match self {
65            Self::And(clauses) | Self::Or(clauses) => clauses.is_empty(),
66            Self::Not(_) | Self::Field { .. } => false,
67        }
68    }
69}
70
71/// WHERE operators (FraiseQL v1 compatibility).
72///
73/// All operators from v1 are supported for backwards compatibility.
74/// No underscore prefix (e.g., `eq`, `icontains`, not `_eq`, `_icontains`).
75///
76/// Note: ExtendedOperator variants may contain f64 values which don't implement Eq,
77/// so WhereOperator derives PartialEq only (not Eq).
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub enum WhereOperator {
80    // ========================================================================
81    // Comparison Operators
82    // ========================================================================
83    /// Equal (=).
84    Eq,
85    /// Not equal (!=).
86    Neq,
87    /// Greater than (>).
88    Gt,
89    /// Greater than or equal (>=).
90    Gte,
91    /// Less than (<).
92    Lt,
93    /// Less than or equal (<=).
94    Lte,
95
96    // ========================================================================
97    // Containment Operators
98    // ========================================================================
99    /// In list (IN).
100    In,
101    /// Not in list (NOT IN).
102    Nin,
103
104    // ========================================================================
105    // String Operators
106    // ========================================================================
107    /// Contains substring (LIKE '%value%').
108    Contains,
109    /// Contains substring (case-insensitive) (ILIKE '%value%').
110    Icontains,
111    /// Starts with (LIKE 'value%').
112    Startswith,
113    /// Starts with (case-insensitive) (ILIKE 'value%').
114    Istartswith,
115    /// Ends with (LIKE '%value').
116    Endswith,
117    /// Ends with (case-insensitive) (ILIKE '%value').
118    Iendswith,
119    /// Pattern matching (LIKE).
120    Like,
121    /// Pattern matching (case-insensitive) (ILIKE).
122    Ilike,
123
124    // ========================================================================
125    // Null Checks
126    // ========================================================================
127    /// Is null (IS NULL or IS NOT NULL).
128    IsNull,
129
130    // ========================================================================
131    // Array Operators
132    // ========================================================================
133    /// Array contains (@>).
134    ArrayContains,
135    /// Array contained by (<@).
136    ArrayContainedBy,
137    /// Array overlaps (&&).
138    ArrayOverlaps,
139    /// Array length equal.
140    LenEq,
141    /// Array length greater than.
142    LenGt,
143    /// Array length less than.
144    LenLt,
145    /// Array length greater than or equal.
146    LenGte,
147    /// Array length less than or equal.
148    LenLte,
149    /// Array length not equal.
150    LenNeq,
151
152    // ========================================================================
153    // Vector Operators (pgvector)
154    // ========================================================================
155    /// Cosine distance (<=>).
156    CosineDistance,
157    /// L2 (Euclidean) distance (<->).
158    L2Distance,
159    /// L1 (Manhattan) distance (<+>).
160    L1Distance,
161    /// Hamming distance (<~>).
162    HammingDistance,
163    /// Inner product (<#>). Higher values = more similar.
164    InnerProduct,
165    /// Jaccard distance for set similarity.
166    JaccardDistance,
167
168    // ========================================================================
169    // Full-Text Search
170    // ========================================================================
171    /// Full-text search (@@).
172    Matches,
173    /// Plain text query (plainto_tsquery).
174    PlainQuery,
175    /// Phrase query (phraseto_tsquery).
176    PhraseQuery,
177    /// Web search query (websearch_to_tsquery).
178    WebsearchQuery,
179
180    // ========================================================================
181    // Network Operators (INET/CIDR)
182    // ========================================================================
183    /// Is IPv4.
184    IsIPv4,
185    /// Is IPv6.
186    IsIPv6,
187    /// Is private IP (RFC1918 ranges).
188    IsPrivate,
189    /// Is public IP (not private).
190    IsPublic,
191    /// Is loopback address (127.0.0.0/8 or ::1).
192    IsLoopback,
193    /// In subnet (<<) - IP is contained within subnet.
194    InSubnet,
195    /// Contains subnet (>>) - subnet contains another subnet.
196    ContainsSubnet,
197    /// Contains IP (>>) - subnet contains an IP address.
198    ContainsIP,
199    /// Overlaps (&&) - subnets overlap.
200    Overlaps,
201
202    // ========================================================================
203    // JSONB Operators
204    // ========================================================================
205    /// Strictly contains (@>).
206    StrictlyContains,
207
208    // ========================================================================
209    // LTree Operators (Hierarchical)
210    // ========================================================================
211    /// Ancestor of (@>).
212    AncestorOf,
213    /// Descendant of (<@).
214    DescendantOf,
215    /// Matches lquery (~).
216    MatchesLquery,
217    /// Matches ltxtquery (@) - Boolean query syntax.
218    MatchesLtxtquery,
219    /// Matches any lquery (?).
220    MatchesAnyLquery,
221    /// Depth equal (nlevel() =).
222    DepthEq,
223    /// Depth not equal (nlevel() !=).
224    DepthNeq,
225    /// Depth greater than (nlevel() >).
226    DepthGt,
227    /// Depth greater than or equal (nlevel() >=).
228    DepthGte,
229    /// Depth less than (nlevel() <).
230    DepthLt,
231    /// Depth less than or equal (nlevel() <=).
232    DepthLte,
233    /// Lowest common ancestor (lca()).
234    Lca,
235
236    // ========================================================================
237    // Extended Operators (Rich Type Filters)
238    // ========================================================================
239    /// Extended operator for rich scalar types (Email, VIN, CountryCode, etc.)
240    /// These operators are specialized filters enabled via feature flags.
241    /// See `fraiseql_core::filters::ExtendedOperator` for available operators.
242    #[serde(skip)]
243    Extended(crate::filters::ExtendedOperator),
244}
245
246impl WhereOperator {
247    /// Parse operator from string (GraphQL input).
248    ///
249    /// # Errors
250    ///
251    /// Returns `FraiseQLError::Validation` if operator name is unknown.
252    pub fn from_str(s: &str) -> Result<Self> {
253        match s {
254            "eq" => Ok(Self::Eq),
255            "neq" => Ok(Self::Neq),
256            "gt" => Ok(Self::Gt),
257            "gte" => Ok(Self::Gte),
258            "lt" => Ok(Self::Lt),
259            "lte" => Ok(Self::Lte),
260            "in" => Ok(Self::In),
261            "nin" => Ok(Self::Nin),
262            "contains" => Ok(Self::Contains),
263            "icontains" => Ok(Self::Icontains),
264            "startswith" => Ok(Self::Startswith),
265            "istartswith" => Ok(Self::Istartswith),
266            "endswith" => Ok(Self::Endswith),
267            "iendswith" => Ok(Self::Iendswith),
268            "like" => Ok(Self::Like),
269            "ilike" => Ok(Self::Ilike),
270            "isnull" => Ok(Self::IsNull),
271            "array_contains" => Ok(Self::ArrayContains),
272            "array_contained_by" => Ok(Self::ArrayContainedBy),
273            "array_overlaps" => Ok(Self::ArrayOverlaps),
274            "len_eq" => Ok(Self::LenEq),
275            "len_gt" => Ok(Self::LenGt),
276            "len_lt" => Ok(Self::LenLt),
277            "len_gte" => Ok(Self::LenGte),
278            "len_lte" => Ok(Self::LenLte),
279            "len_neq" => Ok(Self::LenNeq),
280            "cosine_distance" => Ok(Self::CosineDistance),
281            "l2_distance" => Ok(Self::L2Distance),
282            "l1_distance" => Ok(Self::L1Distance),
283            "hamming_distance" => Ok(Self::HammingDistance),
284            "inner_product" => Ok(Self::InnerProduct),
285            "jaccard_distance" => Ok(Self::JaccardDistance),
286            "matches" => Ok(Self::Matches),
287            "plain_query" => Ok(Self::PlainQuery),
288            "phrase_query" => Ok(Self::PhraseQuery),
289            "websearch_query" => Ok(Self::WebsearchQuery),
290            "is_ipv4" => Ok(Self::IsIPv4),
291            "is_ipv6" => Ok(Self::IsIPv6),
292            "is_private" => Ok(Self::IsPrivate),
293            "is_public" => Ok(Self::IsPublic),
294            "is_loopback" => Ok(Self::IsLoopback),
295            "in_subnet" => Ok(Self::InSubnet),
296            "contains_subnet" => Ok(Self::ContainsSubnet),
297            "contains_ip" => Ok(Self::ContainsIP),
298            "overlaps" => Ok(Self::Overlaps),
299            "strictly_contains" => Ok(Self::StrictlyContains),
300            "ancestor_of" => Ok(Self::AncestorOf),
301            "descendant_of" => Ok(Self::DescendantOf),
302            "matches_lquery" => Ok(Self::MatchesLquery),
303            "matches_ltxtquery" => Ok(Self::MatchesLtxtquery),
304            "matches_any_lquery" => Ok(Self::MatchesAnyLquery),
305            "depth_eq" => Ok(Self::DepthEq),
306            "depth_neq" => Ok(Self::DepthNeq),
307            "depth_gt" => Ok(Self::DepthGt),
308            "depth_gte" => Ok(Self::DepthGte),
309            "depth_lt" => Ok(Self::DepthLt),
310            "depth_lte" => Ok(Self::DepthLte),
311            "lca" => Ok(Self::Lca),
312            _ => Err(FraiseQLError::validation(format!("Unknown WHERE operator: {s}"))),
313        }
314    }
315
316    /// Check if operator requires array value.
317    #[must_use]
318    pub const fn expects_array(&self) -> bool {
319        matches!(self, Self::In | Self::Nin)
320    }
321
322    /// Check if operator is case-insensitive.
323    #[must_use]
324    pub const fn is_case_insensitive(&self) -> bool {
325        matches!(self, Self::Icontains | Self::Istartswith | Self::Iendswith | Self::Ilike)
326    }
327
328    /// Check if operator works with strings.
329    #[must_use]
330    pub const fn is_string_operator(&self) -> bool {
331        matches!(
332            self,
333            Self::Contains
334                | Self::Icontains
335                | Self::Startswith
336                | Self::Istartswith
337                | Self::Endswith
338                | Self::Iendswith
339                | Self::Like
340                | Self::Ilike
341        )
342    }
343}
344
345/// HAVING clause abstract syntax tree.
346///
347/// HAVING filters aggregated results after GROUP BY, while WHERE filters rows before aggregation.
348///
349/// # Example
350///
351/// ```rust
352/// use fraiseql_core::db::{HavingClause, WhereOperator};
353/// use serde_json::json;
354///
355/// // Simple condition: COUNT(*) > 10
356/// let having_clause = HavingClause::Aggregate {
357///     aggregate: "count".to_string(),
358///     operator: WhereOperator::Gt,
359///     value: json!(10),
360/// };
361///
362/// // Complex condition: (COUNT(*) > 10) AND (SUM(revenue) >= 1000)
363/// let having_clause = HavingClause::And(vec![
364///     HavingClause::Aggregate {
365///         aggregate: "count".to_string(),
366///         operator: WhereOperator::Gt,
367///         value: json!(10),
368///     },
369///     HavingClause::Aggregate {
370///         aggregate: "revenue_sum".to_string(),
371///         operator: WhereOperator::Gte,
372///         value: json!(1000),
373///     },
374/// ]);
375/// ```
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377pub enum HavingClause {
378    /// Aggregate field condition (e.g., count_gt, revenue_sum_gte).
379    Aggregate {
380        /// Aggregate name: "count" or "field_function" (e.g., "revenue_sum").
381        aggregate: String,
382        /// Comparison operator.
383        operator:  WhereOperator,
384        /// Value to compare against.
385        value:     serde_json::Value,
386    },
387
388    /// Logical AND of multiple conditions.
389    And(Vec<HavingClause>),
390
391    /// Logical OR of multiple conditions.
392    Or(Vec<HavingClause>),
393
394    /// Logical NOT of a condition.
395    Not(Box<HavingClause>),
396}
397
398impl HavingClause {
399    /// Check if HAVING clause is empty.
400    #[must_use]
401    pub fn is_empty(&self) -> bool {
402        match self {
403            Self::And(clauses) | Self::Or(clauses) => clauses.is_empty(),
404            Self::Not(_) | Self::Aggregate { .. } => false,
405        }
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use serde_json::json;
412
413    use super::*;
414
415    #[test]
416    fn test_where_operator_from_str() {
417        assert_eq!(WhereOperator::from_str("eq").unwrap(), WhereOperator::Eq);
418        assert_eq!(WhereOperator::from_str("icontains").unwrap(), WhereOperator::Icontains);
419        assert_eq!(WhereOperator::from_str("gte").unwrap(), WhereOperator::Gte);
420        assert!(WhereOperator::from_str("unknown").is_err());
421    }
422
423    #[test]
424    fn test_where_operator_expects_array() {
425        assert!(WhereOperator::In.expects_array());
426        assert!(WhereOperator::Nin.expects_array());
427        assert!(!WhereOperator::Eq.expects_array());
428    }
429
430    #[test]
431    fn test_where_operator_is_case_insensitive() {
432        assert!(WhereOperator::Icontains.is_case_insensitive());
433        assert!(WhereOperator::Ilike.is_case_insensitive());
434        assert!(!WhereOperator::Contains.is_case_insensitive());
435    }
436
437    #[test]
438    fn test_where_clause_simple() {
439        let clause = WhereClause::Field {
440            path:     vec!["email".to_string()],
441            operator: WhereOperator::Eq,
442            value:    json!("test@example.com"),
443        };
444
445        assert!(!clause.is_empty());
446    }
447
448    #[test]
449    fn test_where_clause_and() {
450        let clause = WhereClause::And(vec![
451            WhereClause::Field {
452                path:     vec!["published".to_string()],
453                operator: WhereOperator::Eq,
454                value:    json!(true),
455            },
456            WhereClause::Field {
457                path:     vec!["views".to_string()],
458                operator: WhereOperator::Gte,
459                value:    json!(100),
460            },
461        ]);
462
463        assert!(!clause.is_empty());
464    }
465
466    #[test]
467    fn test_where_clause_empty() {
468        let clause = WhereClause::And(vec![]);
469        assert!(clause.is_empty());
470    }
471}