qail_core/ast/builders/
functions.rs

1//! Function call builders (COALESCE, REPLACE, SUBSTRING, etc.)
2
3use super::literals::int;
4use crate::ast::{BinaryOp, Expr};
5
6/// Create a function call expression
7pub fn func(name: &str, args: Vec<Expr>) -> FunctionBuilder {
8    FunctionBuilder {
9        name: name.to_string(),
10        args,
11        alias: None,
12    }
13}
14
15/// COALESCE(args...) function
16pub fn coalesce<E: Into<Expr>>(args: impl IntoIterator<Item = E>) -> FunctionBuilder {
17    func("COALESCE", args.into_iter().map(|e| e.into()).collect())
18}
19
20/// NULLIF(a, b) function
21pub fn nullif(a: impl Into<Expr>, b: impl Into<Expr>) -> FunctionBuilder {
22    func("NULLIF", vec![a.into(), b.into()])
23}
24
25/// REPLACE(source, from, to) function
26/// # Example
27/// ```ignore
28/// replace(col("phone"), text("+"), text(""))  // REPLACE(phone, '+', '')
29/// ```
30pub fn replace(
31    source: impl Into<Expr>,
32    from: impl Into<Expr>,
33    to: impl Into<Expr>,
34) -> FunctionBuilder {
35    func("REPLACE", vec![source.into(), from.into(), to.into()])
36}
37
38/// STRING_AGG(column, delimiter) - concatenate all values with delimiter
39/// # Example
40/// ```ignore
41/// string_agg("name", ", ")  // STRING_AGG(name, ', ')
42/// ```
43pub fn string_agg(column: impl Into<Expr>, delimiter: &str) -> FunctionBuilder {
44    func(
45        "STRING_AGG",
46        vec![column.into(), Expr::Literal(crate::ast::Value::String(delimiter.to_string()))],
47    )
48}
49
50/// Builder for function call expressions
51#[derive(Debug, Clone)]
52pub struct FunctionBuilder {
53    pub(crate) name: String,
54    pub(crate) args: Vec<Expr>,
55    pub(crate) alias: Option<String>,
56}
57
58impl FunctionBuilder {
59    /// Add alias (AS name)
60    pub fn alias(mut self, name: &str) -> Expr {
61        self.alias = Some(name.to_string());
62        self.build()
63    }
64
65    /// Build the final Expr
66    pub fn build(self) -> Expr {
67        Expr::FunctionCall {
68            name: self.name,
69            args: self.args,
70            alias: self.alias,
71        }
72    }
73}
74
75impl From<FunctionBuilder> for Expr {
76    fn from(builder: FunctionBuilder) -> Self {
77        builder.build()
78    }
79}
80
81/// SUBSTRING(source FROM start [FOR length])
82/// # Example
83/// ```ignore
84/// substring(col("phone"), 2)       // SUBSTRING(phone FROM 2)
85/// substring_for(col("name"), 1, 5) // SUBSTRING(name FROM 1 FOR 5)
86/// ```
87pub fn substring(source: impl Into<Expr>, from: i32) -> Expr {
88    Expr::SpecialFunction {
89        name: "SUBSTRING".to_string(),
90        args: vec![
91            (None, Box::new(source.into())),
92            (Some("FROM".to_string()), Box::new(int(from as i64))),
93        ],
94        alias: None,
95    }
96}
97
98/// SUBSTRING(source FROM start FOR length)
99pub fn substring_for(source: impl Into<Expr>, from: i32, length: i32) -> Expr {
100    Expr::SpecialFunction {
101        name: "SUBSTRING".to_string(),
102        args: vec![
103            (None, Box::new(source.into())),
104            (Some("FROM".to_string()), Box::new(int(from as i64))),
105            (Some("FOR".to_string()), Box::new(int(length as i64))),
106        ],
107        alias: None,
108    }
109}
110
111/// String concatenation (a || b || c)
112/// # Example
113/// ```ignore
114/// concat([col("first_name"), text(" "), col("last_name")])
115/// ```
116pub fn concat<E: Into<Expr>>(exprs: impl IntoIterator<Item = E>) -> ConcatBuilder {
117    let exprs: Vec<Expr> = exprs.into_iter().map(|e| e.into()).collect();
118    ConcatBuilder { exprs, alias: None }
119}
120
121/// Builder for concat expressions
122#[derive(Debug, Clone)]
123pub struct ConcatBuilder {
124    pub(crate) exprs: Vec<Expr>,
125    pub(crate) alias: Option<String>,
126}
127
128impl ConcatBuilder {
129    /// Add alias (AS name)
130    pub fn alias(mut self, name: &str) -> Expr {
131        self.alias = Some(name.to_string());
132        self.build()
133    }
134
135    /// Build the final Expr
136    pub fn build(self) -> Expr {
137        use super::literals::text;
138
139        if self.exprs.is_empty() {
140            return text("");
141        }
142
143        let mut iter = self.exprs.into_iter();
144        let first = iter.next().unwrap();
145
146        let result = iter.fold(first, |acc, expr| Expr::Binary {
147            left: Box::new(acc),
148            op: BinaryOp::Concat,
149            right: Box::new(expr),
150            alias: None,
151        });
152
153        // Apply alias to the final result
154        if let Some(alias) = self.alias {
155            match result {
156                Expr::Binary {
157                    left, op, right, ..
158                } => Expr::Binary {
159                    left,
160                    op,
161                    right,
162                    alias: Some(alias),
163                },
164                other => other,
165            }
166        } else {
167            result
168        }
169    }
170}
171
172impl From<ConcatBuilder> for Expr {
173    fn from(builder: ConcatBuilder) -> Self {
174        builder.build()
175    }
176}