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}