Skip to main content

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/// Build a `COALESCE(args...)` expression that returns the first non-NULL argument.
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![
47            column.into(),
48            Expr::Literal(crate::ast::Value::String(delimiter.to_string())),
49        ],
50    )
51}
52
53/// Builder for function call expressions
54#[derive(Debug, Clone)]
55pub struct FunctionBuilder {
56    pub(crate) name: String,
57    pub(crate) args: Vec<Expr>,
58    pub(crate) alias: Option<String>,
59}
60
61impl FunctionBuilder {
62    /// Add alias (AS name)
63    pub fn alias(mut self, name: &str) -> Expr {
64        self.alias = Some(name.to_string());
65        self.build()
66    }
67
68    /// Build the final Expr
69    pub fn build(self) -> Expr {
70        Expr::FunctionCall {
71            name: self.name,
72            args: self.args,
73            alias: self.alias,
74        }
75    }
76}
77
78impl From<FunctionBuilder> for Expr {
79    fn from(builder: FunctionBuilder) -> Self {
80        builder.build()
81    }
82}
83
84/// SUBSTRING(source FROM start [FOR length])
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/// # Example
116/// ```ignore
117/// concat([col("first_name"), text(" "), col("last_name")])
118/// ```
119pub fn concat<E: Into<Expr>>(exprs: impl IntoIterator<Item = E>) -> ConcatBuilder {
120    let exprs: Vec<Expr> = exprs.into_iter().map(|e| e.into()).collect();
121    ConcatBuilder { exprs, alias: None }
122}
123
124/// Builder for concat expressions
125#[derive(Debug, Clone)]
126pub struct ConcatBuilder {
127    pub(crate) exprs: Vec<Expr>,
128    pub(crate) alias: Option<String>,
129}
130
131impl ConcatBuilder {
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        use super::literals::text;
141
142        if self.exprs.is_empty() {
143            return text("");
144        }
145
146        let mut iter = self.exprs.into_iter();
147        let Some(first) = iter.next() else {
148            return text("");
149        };
150
151        let result = iter.fold(first, |acc, expr| Expr::Binary {
152            left: Box::new(acc),
153            op: BinaryOp::Concat,
154            right: Box::new(expr),
155            alias: None,
156        });
157
158        // Apply alias to the final result
159        if let Some(alias) = self.alias {
160            match result {
161                Expr::Binary {
162                    left, op, right, ..
163                } => Expr::Binary {
164                    left,
165                    op,
166                    right,
167                    alias: Some(alias),
168                },
169                other => other,
170            }
171        } else {
172            result
173        }
174    }
175}
176
177impl From<ConcatBuilder> for Expr {
178    fn from(builder: ConcatBuilder) -> Self {
179        builder.build()
180    }
181}