Skip to main content

nautilus_core/
expr.rs

1//! Expression AST for building WHERE clauses and filters.
2
3use crate::select::Select;
4use crate::value::Value;
5
6/// Binary operators for expressions.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum BinaryOp {
9    /// Equality (`=`).
10    Eq,
11    /// Not equal (`!=`).
12    Ne,
13    /// Less than (`<`).
14    Lt,
15    /// Less than or equal (`<=`).
16    Le,
17    /// Greater than (`>`).
18    Gt,
19    /// Greater than or equal (`>=`).
20    Ge,
21    /// Logical AND.
22    And,
23    /// Logical OR.
24    Or,
25    /// LIKE pattern matching.
26    Like,
27    /// Array contains (`@>` in PostgreSQL).
28    ArrayContains,
29    /// Array is contained by (`<@` in PostgreSQL).
30    ArrayContainedBy,
31    /// Array overlaps (`&&` in PostgreSQL).
32    ArrayOverlaps,
33    /// IN list membership.
34    In,
35    /// NOT IN list membership.
36    NotIn,
37}
38
39/// Expression node for WHERE clauses and filters.
40#[derive(Debug, Clone, PartialEq)]
41pub enum Expr {
42    /// Column reference.
43    Column(String),
44    /// Parameter placeholder.
45    Param(Value),
46    /// Binary operation.
47    Binary {
48        /// Left operand.
49        left: Box<Expr>,
50        /// Operator.
51        op: BinaryOp,
52        /// Right operand.
53        right: Box<Expr>,
54    },
55    /// Logical NOT.
56    Not(Box<Expr>),
57    /// Function call (e.g., json_agg, COALESCE).
58    FunctionCall {
59        /// Function name.
60        name: String,
61        /// Function arguments.
62        args: Vec<Expr>,
63    },
64    /// SQL FILTER clause for aggregate functions (PostgreSQL).
65    Filter {
66        /// The aggregation expression.
67        expr: Box<Expr>,
68        /// The filter predicate.
69        predicate: Box<Expr>,
70    },
71    /// EXISTS subquery predicate — compiles to `EXISTS (SELECT ...)`.
72    Exists(Box<Select>),
73    /// NOT EXISTS subquery predicate — compiles to `NOT EXISTS (SELECT ...)`.
74    NotExists(Box<Select>),
75    /// Scalar subquery — compiles to `(SELECT ...)`.
76    ///
77    /// The inner SELECT must return exactly one row and one column.  Used for
78    /// correlated aggregate sub‑queries (e.g. relation includes) that must not
79    /// produce a cartesian product when two or more relations are joined.
80    ScalarSubquery(Box<Select>),
81    /// IS NULL check — compiles to `expr IS NULL`.
82    IsNull(Box<Expr>),
83    /// IS NOT NULL check — compiles to `expr IS NOT NULL`.
84    IsNotNull(Box<Expr>),
85    /// A raw SQL string literal emitted verbatim (no parameter binding).
86    ///
87    /// Use this sparingly — only for values that must appear as SQL literals
88    /// rather than positional parameters (e.g. keys in `json_build_object`).
89    /// Never pass untrusted user input through `Literal`.
90    Literal(String),
91    /// An ordered list of expressions for use in IN / NOT IN clauses.
92    ///
93    /// Rendered as a comma-separated sequence; the surrounding parentheses are
94    /// added by the IN/NOT IN rendering path in each dialect.
95    List(Vec<Expr>),
96    /// CASE WHEN … THEN … ELSE NULL END.
97    CaseWhen {
98        /// The condition.
99        condition: Box<Expr>,
100        /// The THEN result.
101        then: Box<Expr>,
102    },
103    /// SQL wildcard `*` — used inside aggregate functions like `COUNT(*)`.
104    Star,
105}
106
107impl Expr {
108    /// Creates a column reference.
109    pub fn column(name: impl Into<String>) -> Self {
110        Expr::Column(name.into())
111    }
112
113    /// Creates a parameter placeholder.
114    pub fn param(value: impl Into<Value>) -> Self {
115        Expr::Param(value.into())
116    }
117
118    /// Creates an equality comparison (`=`).
119    #[must_use]
120    pub fn eq(self, other: Expr) -> Self {
121        Expr::Binary {
122            left: Box::new(self),
123            op: BinaryOp::Eq,
124            right: Box::new(other),
125        }
126    }
127
128    /// Creates a not-equal comparison (`!=`).
129    #[must_use]
130    pub fn ne(self, other: Expr) -> Self {
131        Expr::Binary {
132            left: Box::new(self),
133            op: BinaryOp::Ne,
134            right: Box::new(other),
135        }
136    }
137
138    /// Creates a less-than comparison (`<`).
139    #[must_use]
140    pub fn lt(self, other: Expr) -> Self {
141        Expr::Binary {
142            left: Box::new(self),
143            op: BinaryOp::Lt,
144            right: Box::new(other),
145        }
146    }
147
148    /// Creates a less-than-or-equal comparison (`<=`).
149    #[must_use]
150    pub fn le(self, other: Expr) -> Self {
151        Expr::Binary {
152            left: Box::new(self),
153            op: BinaryOp::Le,
154            right: Box::new(other),
155        }
156    }
157
158    /// Creates a greater-than comparison (`>`).
159    #[must_use]
160    pub fn gt(self, other: Expr) -> Self {
161        Expr::Binary {
162            left: Box::new(self),
163            op: BinaryOp::Gt,
164            right: Box::new(other),
165        }
166    }
167
168    /// Creates a greater-than-or-equal comparison (`>=`).
169    #[must_use]
170    pub fn ge(self, other: Expr) -> Self {
171        Expr::Binary {
172            left: Box::new(self),
173            op: BinaryOp::Ge,
174            right: Box::new(other),
175        }
176    }
177
178    /// Creates a logical AND.
179    #[must_use]
180    pub fn and(self, other: Expr) -> Self {
181        Expr::Binary {
182            left: Box::new(self),
183            op: BinaryOp::And,
184            right: Box::new(other),
185        }
186    }
187
188    /// Creates a logical OR.
189    #[must_use]
190    pub fn or(self, other: Expr) -> Self {
191        Expr::Binary {
192            left: Box::new(self),
193            op: BinaryOp::Or,
194            right: Box::new(other),
195        }
196    }
197
198    /// Creates a LIKE pattern match.
199    #[must_use]
200    pub fn like(self, pattern: Expr) -> Self {
201        Expr::Binary {
202            left: Box::new(self),
203            op: BinaryOp::Like,
204            right: Box::new(pattern),
205        }
206    }
207
208    /// Creates an IN list membership check.
209    #[must_use]
210    pub fn in_list(self, exprs: Vec<Expr>) -> Self {
211        Expr::Binary {
212            left: Box::new(self),
213            op: BinaryOp::In,
214            right: Box::new(Expr::List(exprs)),
215        }
216    }
217
218    /// Creates a NOT IN list membership check.
219    #[must_use]
220    pub fn not_in_list(self, exprs: Vec<Expr>) -> Self {
221        Expr::Binary {
222            left: Box::new(self),
223            op: BinaryOp::NotIn,
224            right: Box::new(Expr::List(exprs)),
225        }
226    }
227
228    /// Creates a function call expression.
229    pub fn function_call(name: impl Into<String>, args: Vec<Expr>) -> Self {
230        Expr::FunctionCall {
231            name: name.into(),
232            args,
233        }
234    }
235
236    /// Creates a json_agg() aggregate function.
237    pub fn json_agg(expr: Expr) -> Self {
238        Expr::FunctionCall {
239            name: "json_agg".to_string(),
240            args: vec![expr],
241        }
242    }
243
244    /// Creates a json_build_object() function with key-value pairs.
245    ///
246    /// Keys are emitted as SQL string literals (not bound parameters) because
247    /// `json_build_object` requires literal key names in all supported dialects.
248    ///
249    /// # Safety
250    ///
251    /// Keys must be static/compile-time strings. Never pass untrusted input as
252    /// a key — use [`Expr::param`] for that and handle it in application logic.
253    pub fn json_build_object(pairs: Vec<(String, Expr)>) -> Self {
254        let args: Vec<Expr> = pairs
255            .into_iter()
256            .flat_map(|(key, value)| vec![Expr::Literal(key), value])
257            .collect();
258
259        Expr::FunctionCall {
260            name: "json_build_object".to_string(),
261            args,
262        }
263    }
264
265    /// Creates a COALESCE() function to return first non-NULL value.
266    pub fn coalesce(exprs: Vec<Expr>) -> Self {
267        Expr::FunctionCall {
268            name: "COALESCE".to_string(),
269            args: exprs,
270        }
271    }
272
273    /// Creates an IS NOT NULL check — compiles to `expr IS NOT NULL`.
274    #[must_use]
275    pub fn is_not_null(self) -> Self {
276        Expr::IsNotNull(Box::new(self))
277    }
278
279    /// Creates an IS NULL check — compiles to `expr IS NULL`.
280    #[must_use]
281    pub fn is_null(self) -> Self {
282        Expr::IsNull(Box::new(self))
283    }
284
285    /// Adds a FILTER clause to an aggregate expression (PostgreSQL).
286    #[must_use]
287    pub fn filter(self, predicate: Expr) -> Self {
288        Expr::Filter {
289            expr: Box::new(self),
290            predicate: Box::new(predicate),
291        }
292    }
293
294    /// Creates an EXISTS subquery predicate.
295    pub fn exists(subquery: Select) -> Self {
296        Expr::Exists(Box::new(subquery))
297    }
298
299    /// Creates a NOT EXISTS subquery predicate.
300    pub fn not_exists(subquery: Select) -> Self {
301        Expr::NotExists(Box::new(subquery))
302    }
303
304    /// Creates a scalar subquery expression `(SELECT ...)`.
305    ///
306    /// The inner SELECT must return exactly one column and at most one row.
307    pub fn scalar_subquery(subquery: Select) -> Self {
308        Expr::ScalarSubquery(Box::new(subquery))
309    }
310
311    /// Creates a `CASE WHEN condition THEN result ELSE NULL END` expression.
312    pub fn case_when(condition: Expr, then: Expr) -> Self {
313        Expr::CaseWhen {
314            condition: Box::new(condition),
315            then: Box::new(then),
316        }
317    }
318
319    /// Creates the SQL wildcard `*` (for use in `COUNT(*)`).
320    pub fn star() -> Self {
321        Expr::Star
322    }
323}
324
325/// Implements the `!` operator for expressions, producing a SQL `NOT` clause.
326impl std::ops::Not for Expr {
327    type Output = Self;
328
329    fn not(self) -> Self::Output {
330        Expr::Not(Box::new(self))
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_column_expr() {
340        let expr = Expr::column("email");
341        match expr {
342            Expr::Column(name) => assert_eq!(name, "email"),
343            _ => panic!("Expected Column variant"),
344        }
345    }
346
347    #[test]
348    fn test_param_expr() {
349        let expr = Expr::param(42i64);
350        match expr {
351            Expr::Param(Value::I64(42)) => {}
352            _ => panic!("Expected Param with I64(42)"),
353        }
354    }
355
356    #[test]
357    fn test_binary_ops() {
358        let col = Expr::column("age");
359        let val = Expr::param(18i64);
360
361        let expr = col.ge(val);
362        match expr {
363            Expr::Binary { op, .. } => assert_eq!(op, BinaryOp::Ge),
364            _ => panic!("Expected Binary expression"),
365        }
366    }
367
368    #[test]
369    fn test_complex_expr() {
370        // (age >= 18) AND (email LIKE '%@gmail.com')
371        let expr = Expr::column("age")
372            .ge(Expr::param(18i64))
373            .and(Expr::column("email").like(Expr::param("%@gmail.com")));
374
375        match expr {
376            Expr::Binary { op, .. } => assert_eq!(op, BinaryOp::And),
377            _ => panic!("Expected Binary AND expression"),
378        }
379    }
380
381    #[test]
382    fn test_not_expr() {
383        let expr = !Expr::column("active").eq(Expr::param(true));
384        match expr {
385            Expr::Not(_) => {}
386            _ => panic!("Expected Not expression"),
387        }
388    }
389
390    #[test]
391    fn test_in_list() {
392        let expr = Expr::column("status").in_list(vec![
393            Expr::param(Value::String("active".to_string())),
394            Expr::param(Value::String("pending".to_string())),
395        ]);
396        match expr {
397            Expr::Binary { op, .. } => assert_eq!(op, BinaryOp::In),
398            _ => panic!("Expected Binary IN expression"),
399        }
400    }
401
402    #[test]
403    fn test_not_in_list() {
404        let expr = Expr::column("role").not_in_list(vec![
405            Expr::param(Value::String("admin".to_string())),
406            Expr::param(Value::String("superuser".to_string())),
407        ]);
408        match expr {
409            Expr::Binary { op, .. } => assert_eq!(op, BinaryOp::NotIn),
410            _ => panic!("Expected Binary NOT IN expression"),
411        }
412    }
413}