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