qail_core/ast/
builders.rs

1//! Ergonomic builder functions for QAIL AST expressions.
2//!
3//! This module provides convenient helper functions to construct AST nodes
4//! without the verbosity of creating structs directly.
5//!
6//! # Example
7//! ```ignore
8//! use qail_core::ast::builders::*;
9//!
10//! let expr = count_filter(vec![
11//!     eq("direction", "outbound"),
12//!     gt("created_at", now_minus("24 hours")),
13//! ]).alias("messages_sent_24h");
14//! ```
15
16use crate::ast::{AggregateFunc, BinaryOp, Condition, Expr, Operator, Value};
17
18// ==================== Column Reference ====================
19
20/// Create a column reference expression
21pub fn col(name: &str) -> Expr {
22    Expr::Named(name.to_string())
23}
24
25/// Create a star (*) expression for SELECT *
26pub fn star() -> Expr {
27    Expr::Star
28}
29
30// ==================== Aggregate Functions ====================
31
32/// COUNT(*) aggregate
33pub fn count() -> AggregateBuilder {
34    AggregateBuilder {
35        col: "*".to_string(),
36        func: AggregateFunc::Count,
37        distinct: false,
38        filter: None,
39        alias: None,
40    }
41}
42
43/// COUNT(DISTINCT column) aggregate
44pub fn count_distinct(column: &str) -> AggregateBuilder {
45    AggregateBuilder {
46        col: column.to_string(),
47        func: AggregateFunc::Count,
48        distinct: true,
49        filter: None,
50        alias: None,
51    }
52}
53
54/// COUNT(*) FILTER (WHERE conditions) aggregate
55pub fn count_filter(conditions: Vec<Condition>) -> AggregateBuilder {
56    AggregateBuilder {
57        col: "*".to_string(),
58        func: AggregateFunc::Count,
59        distinct: false,
60        filter: Some(conditions),
61        alias: None,
62    }
63}
64
65/// SUM(column) aggregate
66pub fn sum(column: &str) -> AggregateBuilder {
67    AggregateBuilder {
68        col: column.to_string(),
69        func: AggregateFunc::Sum,
70        distinct: false,
71        filter: None,
72        alias: None,
73    }
74}
75
76/// AVG(column) aggregate
77pub fn avg(column: &str) -> AggregateBuilder {
78    AggregateBuilder {
79        col: column.to_string(),
80        func: AggregateFunc::Avg,
81        distinct: false,
82        filter: None,
83        alias: None,
84    }
85}
86
87/// MIN(column) aggregate
88pub fn min(column: &str) -> AggregateBuilder {
89    AggregateBuilder {
90        col: column.to_string(),
91        func: AggregateFunc::Min,
92        distinct: false,
93        filter: None,
94        alias: None,
95    }
96}
97
98/// MAX(column) aggregate
99pub fn max(column: &str) -> AggregateBuilder {
100    AggregateBuilder {
101        col: column.to_string(),
102        func: AggregateFunc::Max,
103        distinct: false,
104        filter: None,
105        alias: None,
106    }
107}
108
109/// Builder for aggregate expressions
110#[derive(Debug, Clone)]
111pub struct AggregateBuilder {
112    col: String,
113    func: AggregateFunc,
114    distinct: bool,
115    filter: Option<Vec<Condition>>,
116    alias: Option<String>,
117}
118
119impl AggregateBuilder {
120    /// Add DISTINCT modifier
121    pub fn distinct(mut self) -> Self {
122        self.distinct = true;
123        self
124    }
125
126    /// Add FILTER (WHERE ...) clause
127    pub fn filter(mut self, conditions: Vec<Condition>) -> Self {
128        self.filter = Some(conditions);
129        self
130    }
131
132    /// Add alias (AS name)
133    pub fn alias(mut self, name: &str) -> Expr {
134        self.alias = Some(name.to_string());
135        self.build()
136    }
137
138    /// Build the final Expr
139    pub fn build(self) -> Expr {
140        Expr::Aggregate {
141            col: self.col,
142            func: self.func,
143            distinct: self.distinct,
144            filter: self.filter,
145            alias: self.alias,
146        }
147    }
148}
149
150impl From<AggregateBuilder> for Expr {
151    fn from(builder: AggregateBuilder) -> Self {
152        builder.build()
153    }
154}
155
156// ==================== Time Functions ====================
157
158/// NOW() function
159pub fn now() -> Expr {
160    Expr::FunctionCall {
161        name: "NOW".to_string(),
162        args: vec![],
163        alias: None,
164    }
165}
166
167/// INTERVAL 'duration' expression
168pub fn interval(duration: &str) -> Expr {
169    Expr::SpecialFunction {
170        name: "INTERVAL".to_string(),
171        args: vec![(None, Box::new(Expr::Named(format!("'{}'", duration))))],
172        alias: None,
173    }
174}
175
176/// NOW() - INTERVAL 'duration' helper
177pub fn now_minus(duration: &str) -> Expr {
178    Expr::Binary {
179        left: Box::new(now()),
180        op: BinaryOp::Sub,
181        right: Box::new(interval(duration)),
182        alias: None,
183    }
184}
185
186/// NOW() + INTERVAL 'duration' helper
187pub fn now_plus(duration: &str) -> Expr {
188    Expr::Binary {
189        left: Box::new(now()),
190        op: BinaryOp::Add,
191        right: Box::new(interval(duration)),
192        alias: None,
193    }
194}
195
196// ==================== Type Casting ====================
197
198/// Cast expression to target type (expr::type)
199pub fn cast(expr: impl Into<Expr>, target_type: &str) -> CastBuilder {
200    CastBuilder {
201        expr: expr.into(),
202        target_type: target_type.to_string(),
203        alias: None,
204    }
205}
206
207/// Builder for cast expressions
208#[derive(Debug, Clone)]
209pub struct CastBuilder {
210    expr: Expr,
211    target_type: String,
212    alias: Option<String>,
213}
214
215impl CastBuilder {
216    /// Add alias (AS name)
217    pub fn alias(mut self, name: &str) -> Expr {
218        self.alias = Some(name.to_string());
219        self.build()
220    }
221
222    /// Build the final Expr
223    pub fn build(self) -> Expr {
224        Expr::Cast {
225            expr: Box::new(self.expr),
226            target_type: self.target_type,
227            alias: self.alias,
228        }
229    }
230}
231
232impl From<CastBuilder> for Expr {
233    fn from(builder: CastBuilder) -> Self {
234        builder.build()
235    }
236}
237
238// ==================== CASE WHEN ====================
239
240/// Start a CASE WHEN expression
241pub fn case_when(condition: Condition, then_expr: impl Into<Expr>) -> CaseBuilder {
242    CaseBuilder {
243        when_clauses: vec![(condition, Box::new(then_expr.into()))],
244        else_value: None,
245        alias: None,
246    }
247}
248
249/// Builder for CASE expressions
250#[derive(Debug, Clone)]
251pub struct CaseBuilder {
252    when_clauses: Vec<(Condition, Box<Expr>)>,
253    else_value: Option<Box<Expr>>,
254    alias: Option<String>,
255}
256
257impl CaseBuilder {
258    /// Add another WHEN clause
259    pub fn when(mut self, condition: Condition, then_expr: impl Into<Expr>) -> Self {
260        self.when_clauses.push((condition, Box::new(then_expr.into())));
261        self
262    }
263
264    /// Add ELSE clause
265    pub fn otherwise(mut self, else_expr: impl Into<Expr>) -> Self {
266        self.else_value = Some(Box::new(else_expr.into()));
267        self
268    }
269
270    /// Add alias (AS name)
271    pub fn alias(mut self, name: &str) -> Expr {
272        self.alias = Some(name.to_string());
273        self.build()
274    }
275
276    /// Build the final Expr
277    pub fn build(self) -> Expr {
278        Expr::Case {
279            when_clauses: self.when_clauses,
280            else_value: self.else_value,
281            alias: self.alias,
282        }
283    }
284}
285
286impl From<CaseBuilder> for Expr {
287    fn from(builder: CaseBuilder) -> Self {
288        builder.build()
289    }
290}
291
292// ==================== Binary Expressions ====================
293
294/// Create a binary expression (left op right)
295pub fn binary(left: impl Into<Expr>, op: BinaryOp, right: impl Into<Expr>) -> BinaryBuilder {
296    BinaryBuilder {
297        left: left.into(),
298        op,
299        right: right.into(),
300        alias: None,
301    }
302}
303
304/// Builder for binary expressions
305#[derive(Debug, Clone)]
306pub struct BinaryBuilder {
307    left: Expr,
308    op: BinaryOp,
309    right: Expr,
310    alias: Option<String>,
311}
312
313impl BinaryBuilder {
314    /// Add alias (AS name)
315    pub fn alias(mut self, name: &str) -> Expr {
316        self.alias = Some(name.to_string());
317        self.build()
318    }
319
320    /// Build the final Expr
321    pub fn build(self) -> Expr {
322        Expr::Binary {
323            left: Box::new(self.left),
324            op: self.op,
325            right: Box::new(self.right),
326            alias: self.alias,
327        }
328    }
329}
330
331impl From<BinaryBuilder> for Expr {
332    fn from(builder: BinaryBuilder) -> Self {
333        builder.build()
334    }
335}
336
337// ==================== Condition Helpers ====================
338
339/// Helper to create a condition
340fn make_condition(column: &str, op: Operator, value: Value) -> Condition {
341    Condition {
342        left: Expr::Named(column.to_string()),
343        op,
344        value,
345        is_array_unnest: false,
346    }
347}
348
349/// Create an equality condition (column = value)
350pub fn eq(column: &str, value: impl Into<Value>) -> Condition {
351    make_condition(column, Operator::Eq, value.into())
352}
353
354/// Create a not-equal condition (column != value)
355pub fn ne(column: &str, value: impl Into<Value>) -> Condition {
356    make_condition(column, Operator::Ne, value.into())
357}
358
359/// Create a greater-than condition (column > value)
360pub fn gt(column: &str, value: impl Into<Value>) -> Condition {
361    make_condition(column, Operator::Gt, value.into())
362}
363
364/// Create a greater-than-or-equal condition (column >= value)
365pub fn gte(column: &str, value: impl Into<Value>) -> Condition {
366    make_condition(column, Operator::Gte, value.into())
367}
368
369/// Create a less-than condition (column < value)
370pub fn lt(column: &str, value: impl Into<Value>) -> Condition {
371    make_condition(column, Operator::Lt, value.into())
372}
373
374/// Create a less-than-or-equal condition (column <= value)
375pub fn lte(column: &str, value: impl Into<Value>) -> Condition {
376    make_condition(column, Operator::Lte, value.into())
377}
378
379/// Create an IN condition (column IN (values))
380pub fn is_in<V: Into<Value>>(column: &str, values: impl IntoIterator<Item = V>) -> Condition {
381    let vals: Vec<Value> = values.into_iter().map(|v| v.into()).collect();
382    make_condition(column, Operator::In, Value::Array(vals))
383}
384
385/// Create a NOT IN condition (column NOT IN (values))
386pub fn not_in<V: Into<Value>>(column: &str, values: impl IntoIterator<Item = V>) -> Condition {
387    let vals: Vec<Value> = values.into_iter().map(|v| v.into()).collect();
388    make_condition(column, Operator::NotIn, Value::Array(vals))
389}
390
391/// Create an IS NULL condition
392pub fn is_null(column: &str) -> Condition {
393    make_condition(column, Operator::IsNull, Value::Null)
394}
395
396/// Create an IS NOT NULL condition
397pub fn is_not_null(column: &str) -> Condition {
398    make_condition(column, Operator::IsNotNull, Value::Null)
399}
400
401/// Create a LIKE condition (column LIKE pattern)
402pub fn like(column: &str, pattern: &str) -> Condition {
403    make_condition(column, Operator::Like, Value::String(pattern.to_string()))
404}
405
406/// Create an ILIKE condition (case-insensitive LIKE)
407pub fn ilike(column: &str, pattern: &str) -> Condition {
408    make_condition(column, Operator::ILike, Value::String(pattern.to_string()))
409}
410
411// ==================== Function Calls ====================
412
413/// Create a function call expression
414pub fn func(name: &str, args: Vec<Expr>) -> FunctionBuilder {
415    FunctionBuilder {
416        name: name.to_string(),
417        args,
418        alias: None,
419    }
420}
421
422/// COALESCE(args...) function
423pub fn coalesce(args: Vec<Expr>) -> FunctionBuilder {
424    func("COALESCE", args)
425}
426
427/// NULLIF(a, b) function
428pub fn nullif(a: impl Into<Expr>, b: impl Into<Expr>) -> FunctionBuilder {
429    func("NULLIF", vec![a.into(), b.into()])
430}
431
432/// Builder for function call expressions
433#[derive(Debug, Clone)]
434pub struct FunctionBuilder {
435    name: String,
436    args: Vec<Expr>,
437    alias: Option<String>,
438}
439
440impl FunctionBuilder {
441    /// Add alias (AS name)
442    pub fn alias(mut self, name: &str) -> Expr {
443        self.alias = Some(name.to_string());
444        self.build()
445    }
446
447    /// Build the final Expr
448    pub fn build(self) -> Expr {
449        Expr::FunctionCall {
450            name: self.name,
451            args: self.args,
452            alias: self.alias,
453        }
454    }
455}
456
457impl From<FunctionBuilder> for Expr {
458    fn from(builder: FunctionBuilder) -> Self {
459        builder.build()
460    }
461}
462
463// ==================== Literal Values ====================
464
465/// Create an integer literal expression
466pub fn int(value: i64) -> Expr {
467    Expr::Named(value.to_string())
468}
469
470/// Create a float literal expression  
471pub fn float(value: f64) -> Expr {
472    Expr::Named(value.to_string())
473}
474
475/// Create a string literal expression
476pub fn text(value: &str) -> Expr {
477    Expr::Named(format!("'{}'", value))
478}
479
480// ==================== Extension Trait for Expr ====================
481
482/// Extension trait to add fluent methods to Expr
483pub trait ExprExt {
484    /// Add an alias to this expression
485    fn as_alias(self, alias: &str) -> Expr;
486}
487
488impl ExprExt for Expr {
489    fn as_alias(self, alias: &str) -> Expr {
490        match self {
491            Expr::Named(name) => Expr::Aliased { name, alias: alias.to_string() },
492            Expr::Aggregate { col, func, distinct, filter, .. } => {
493                Expr::Aggregate { col, func, distinct, filter, alias: Some(alias.to_string()) }
494            }
495            Expr::Cast { expr, target_type, .. } => {
496                Expr::Cast { expr, target_type, alias: Some(alias.to_string()) }
497            }
498            Expr::Case { when_clauses, else_value, .. } => {
499                Expr::Case { when_clauses, else_value, alias: Some(alias.to_string()) }
500            }
501            Expr::FunctionCall { name, args, .. } => {
502                Expr::FunctionCall { name, args, alias: Some(alias.to_string()) }
503            }
504            Expr::Binary { left, op, right, .. } => {
505                Expr::Binary { left, op, right, alias: Some(alias.to_string()) }
506            }
507            other => other,  // Star, Aliased, etc. - return as-is
508        }
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    #[test]
517    fn test_count_filter() {
518        let expr = count_filter(vec![
519            eq("direction", "outbound"),
520        ]).alias("sent_count");
521        
522        assert!(matches!(expr, Expr::Aggregate { alias: Some(a), .. } if a == "sent_count"));
523    }
524
525    #[test]
526    fn test_now_minus() {
527        let expr = now_minus("24 hours");
528        assert!(matches!(expr, Expr::Binary { op: BinaryOp::Sub, .. }));
529    }
530
531    #[test]
532    fn test_case_when() {
533        let expr = case_when(gt("x", 0), int(1))
534            .otherwise(int(0))
535            .alias("result");
536        
537        assert!(matches!(expr, Expr::Case { alias: Some(a), .. } if a == "result"));
538    }
539
540    #[test]
541    fn test_cast() {
542        let expr = cast(col("value"), "float8").alias("value_f");
543        assert!(matches!(expr, Expr::Cast { target_type, .. } if target_type == "float8"));
544    }
545}