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/// Relation filter operator used by generated relation helpers.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum RelationFilterOp {
9    /// At least one related row matches the child filter.
10    Some,
11    /// No related row matches the child filter.
12    None,
13    /// Every related row matches the child filter.
14    Every,
15}
16
17/// Metadata needed to render or serialize a relation predicate.
18#[derive(Debug, Clone, PartialEq)]
19pub struct RelationFilter {
20    /// Logical relation field name on the parent model.
21    pub field: String,
22    /// Database table name of the parent model.
23    pub parent_table: String,
24    /// Database table name of the related child model.
25    pub target_table: String,
26    /// Child-side foreign-key column name.
27    pub fk_db: String,
28    /// Parent-side referenced key column name.
29    pub pk_db: String,
30    /// Child filter to apply inside the relation predicate.
31    pub filter: Box<Expr>,
32}
33
34/// Binary operators for expressions.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum BinaryOp {
37    /// Equality (`=`).
38    Eq,
39    /// Not equal (`!=`).
40    Ne,
41    /// Less than (`<`).
42    Lt,
43    /// Less than or equal (`<=`).
44    Le,
45    /// Greater than (`>`).
46    Gt,
47    /// Greater than or equal (`>=`).
48    Ge,
49    /// Logical AND.
50    And,
51    /// Logical OR.
52    Or,
53    /// LIKE pattern matching.
54    Like,
55    /// Array contains (`@>` in PostgreSQL).
56    ArrayContains,
57    /// Array is contained by (`<@` in PostgreSQL).
58    ArrayContainedBy,
59    /// Array overlaps (`&&` in PostgreSQL).
60    ArrayOverlaps,
61    /// IN list membership.
62    In,
63    /// NOT IN list membership.
64    NotIn,
65}
66
67/// Expression node for WHERE clauses and filters.
68#[derive(Debug, Clone, PartialEq)]
69pub enum Expr {
70    /// Column reference.
71    Column(String),
72    /// Parameter placeholder.
73    Param(Value),
74    /// Binary operation.
75    Binary {
76        /// Left operand.
77        left: Box<Expr>,
78        /// Operator.
79        op: BinaryOp,
80        /// Right operand.
81        right: Box<Expr>,
82    },
83    /// Logical NOT.
84    Not(Box<Expr>),
85    /// Function call (e.g., json_agg, COALESCE).
86    FunctionCall {
87        /// Function name.
88        name: String,
89        /// Function arguments.
90        args: Vec<Expr>,
91    },
92    /// SQL FILTER clause for aggregate functions (PostgreSQL).
93    Filter {
94        /// The aggregation expression.
95        expr: Box<Expr>,
96        /// The filter predicate.
97        predicate: Box<Expr>,
98    },
99    /// EXISTS subquery predicate — compiles to `EXISTS (SELECT ...)`.
100    Exists(Box<Select>),
101    /// NOT EXISTS subquery predicate — compiles to `NOT EXISTS (SELECT ...)`.
102    NotExists(Box<Select>),
103    /// Relation predicate (`some` / `none` / `every`) with explicit relation metadata.
104    Relation {
105        /// Which relation operator to apply.
106        op: RelationFilterOp,
107        /// Relation metadata and nested child filter.
108        relation: Box<RelationFilter>,
109    },
110    /// Scalar subquery — compiles to `(SELECT ...)`.
111    ///
112    /// The inner SELECT must return exactly one row and one column.  Used for
113    /// correlated aggregate sub‑queries (e.g. relation includes) that must not
114    /// produce a cartesian product when two or more relations are joined.
115    ScalarSubquery(Box<Select>),
116    /// IS NULL check — compiles to `expr IS NULL`.
117    IsNull(Box<Expr>),
118    /// IS NOT NULL check — compiles to `expr IS NOT NULL`.
119    IsNotNull(Box<Expr>),
120    /// A raw SQL string literal emitted verbatim (no parameter binding).
121    ///
122    /// Use this sparingly — only for values that must appear as SQL literals
123    /// rather than positional parameters (e.g. keys in `json_build_object`).
124    /// Never pass untrusted user input through `Literal`.
125    Literal(String),
126    /// An ordered list of expressions for use in IN / NOT IN clauses.
127    ///
128    /// Rendered as a comma-separated sequence; the surrounding parentheses are
129    /// added by the IN/NOT IN rendering path in each dialect.
130    List(Vec<Expr>),
131    /// CASE WHEN … THEN … ELSE NULL END.
132    CaseWhen {
133        /// The condition.
134        condition: Box<Expr>,
135        /// The THEN result.
136        then: Box<Expr>,
137    },
138    /// SQL wildcard `*` — used inside aggregate functions like `COUNT(*)`.
139    Star,
140}
141
142impl Expr {
143    /// Creates a column reference.
144    pub fn column(name: impl Into<String>) -> Self {
145        Expr::Column(name.into())
146    }
147
148    /// Creates a parameter placeholder.
149    pub fn param(value: impl Into<Value>) -> Self {
150        Expr::Param(value.into())
151    }
152
153    /// Creates an equality comparison (`=`).
154    #[must_use]
155    pub fn eq(self, other: Expr) -> Self {
156        Expr::Binary {
157            left: Box::new(self),
158            op: BinaryOp::Eq,
159            right: Box::new(other),
160        }
161    }
162
163    /// Creates a not-equal comparison (`!=`).
164    #[must_use]
165    pub fn ne(self, other: Expr) -> Self {
166        Expr::Binary {
167            left: Box::new(self),
168            op: BinaryOp::Ne,
169            right: Box::new(other),
170        }
171    }
172
173    /// Creates a less-than comparison (`<`).
174    #[must_use]
175    pub fn lt(self, other: Expr) -> Self {
176        Expr::Binary {
177            left: Box::new(self),
178            op: BinaryOp::Lt,
179            right: Box::new(other),
180        }
181    }
182
183    /// Creates a less-than-or-equal comparison (`<=`).
184    #[must_use]
185    pub fn le(self, other: Expr) -> Self {
186        Expr::Binary {
187            left: Box::new(self),
188            op: BinaryOp::Le,
189            right: Box::new(other),
190        }
191    }
192
193    /// Creates a greater-than comparison (`>`).
194    #[must_use]
195    pub fn gt(self, other: Expr) -> Self {
196        Expr::Binary {
197            left: Box::new(self),
198            op: BinaryOp::Gt,
199            right: Box::new(other),
200        }
201    }
202
203    /// Creates a greater-than-or-equal comparison (`>=`).
204    #[must_use]
205    pub fn ge(self, other: Expr) -> Self {
206        Expr::Binary {
207            left: Box::new(self),
208            op: BinaryOp::Ge,
209            right: Box::new(other),
210        }
211    }
212
213    /// Creates a logical AND.
214    #[must_use]
215    pub fn and(self, other: Expr) -> Self {
216        Expr::Binary {
217            left: Box::new(self),
218            op: BinaryOp::And,
219            right: Box::new(other),
220        }
221    }
222
223    /// Creates a logical OR.
224    #[must_use]
225    pub fn or(self, other: Expr) -> Self {
226        Expr::Binary {
227            left: Box::new(self),
228            op: BinaryOp::Or,
229            right: Box::new(other),
230        }
231    }
232
233    /// Creates a LIKE pattern match.
234    #[must_use]
235    pub fn like(self, pattern: Expr) -> Self {
236        Expr::Binary {
237            left: Box::new(self),
238            op: BinaryOp::Like,
239            right: Box::new(pattern),
240        }
241    }
242
243    /// Creates an IN list membership check.
244    #[must_use]
245    pub fn in_list(self, exprs: Vec<Expr>) -> Self {
246        Expr::Binary {
247            left: Box::new(self),
248            op: BinaryOp::In,
249            right: Box::new(Expr::List(exprs)),
250        }
251    }
252
253    /// Creates a NOT IN list membership check.
254    #[must_use]
255    pub fn not_in_list(self, exprs: Vec<Expr>) -> Self {
256        Expr::Binary {
257            left: Box::new(self),
258            op: BinaryOp::NotIn,
259            right: Box::new(Expr::List(exprs)),
260        }
261    }
262
263    /// Creates a function call expression.
264    pub fn function_call(name: impl Into<String>, args: Vec<Expr>) -> Self {
265        Expr::FunctionCall {
266            name: name.into(),
267            args,
268        }
269    }
270
271    /// Creates a json_agg() aggregate function.
272    pub fn json_agg(expr: Expr) -> Self {
273        Expr::FunctionCall {
274            name: "json_agg".to_string(),
275            args: vec![expr],
276        }
277    }
278
279    /// Creates a json_build_object() function with key-value pairs.
280    ///
281    /// Keys are emitted as SQL string literals (not bound parameters) because
282    /// `json_build_object` requires literal key names in all supported dialects.
283    ///
284    /// # Safety
285    ///
286    /// Keys must be static/compile-time strings. Never pass untrusted input as
287    /// a key — use [`Expr::param`] for that and handle it in application logic.
288    pub fn json_build_object(pairs: Vec<(String, Expr)>) -> Self {
289        let args: Vec<Expr> = pairs
290            .into_iter()
291            .flat_map(|(key, value)| vec![Expr::Literal(key), value])
292            .collect();
293
294        Expr::FunctionCall {
295            name: "json_build_object".to_string(),
296            args,
297        }
298    }
299
300    /// Creates a COALESCE() function to return first non-NULL value.
301    pub fn coalesce(exprs: Vec<Expr>) -> Self {
302        Expr::FunctionCall {
303            name: "COALESCE".to_string(),
304            args: exprs,
305        }
306    }
307
308    /// Creates an IS NOT NULL check — compiles to `expr IS NOT NULL`.
309    #[must_use]
310    pub fn is_not_null(self) -> Self {
311        Expr::IsNotNull(Box::new(self))
312    }
313
314    /// Creates an IS NULL check — compiles to `expr IS NULL`.
315    #[must_use]
316    pub fn is_null(self) -> Self {
317        Expr::IsNull(Box::new(self))
318    }
319
320    /// Adds a FILTER clause to an aggregate expression (PostgreSQL).
321    #[must_use]
322    pub fn filter(self, predicate: Expr) -> Self {
323        Expr::Filter {
324            expr: Box::new(self),
325            predicate: Box::new(predicate),
326        }
327    }
328
329    /// Creates an EXISTS subquery predicate.
330    pub fn exists(subquery: Select) -> Self {
331        Expr::Exists(Box::new(subquery))
332    }
333
334    /// Creates a NOT EXISTS subquery predicate.
335    pub fn not_exists(subquery: Select) -> Self {
336        Expr::NotExists(Box::new(subquery))
337    }
338
339    /// Creates a relation `some` predicate.
340    pub fn relation_some(
341        field: impl Into<String>,
342        parent_table: impl Into<String>,
343        target_table: impl Into<String>,
344        fk_db: impl Into<String>,
345        pk_db: impl Into<String>,
346        filter: Expr,
347    ) -> Self {
348        Expr::Relation {
349            op: RelationFilterOp::Some,
350            relation: Box::new(RelationFilter {
351                field: field.into(),
352                parent_table: parent_table.into(),
353                target_table: target_table.into(),
354                fk_db: fk_db.into(),
355                pk_db: pk_db.into(),
356                filter: Box::new(filter),
357            }),
358        }
359    }
360
361    /// Creates a relation `none` predicate.
362    pub fn relation_none(
363        field: impl Into<String>,
364        parent_table: impl Into<String>,
365        target_table: impl Into<String>,
366        fk_db: impl Into<String>,
367        pk_db: impl Into<String>,
368        filter: Expr,
369    ) -> Self {
370        Expr::Relation {
371            op: RelationFilterOp::None,
372            relation: Box::new(RelationFilter {
373                field: field.into(),
374                parent_table: parent_table.into(),
375                target_table: target_table.into(),
376                fk_db: fk_db.into(),
377                pk_db: pk_db.into(),
378                filter: Box::new(filter),
379            }),
380        }
381    }
382
383    /// Creates a relation `every` predicate.
384    pub fn relation_every(
385        field: impl Into<String>,
386        parent_table: impl Into<String>,
387        target_table: impl Into<String>,
388        fk_db: impl Into<String>,
389        pk_db: impl Into<String>,
390        filter: Expr,
391    ) -> Self {
392        Expr::Relation {
393            op: RelationFilterOp::Every,
394            relation: Box::new(RelationFilter {
395                field: field.into(),
396                parent_table: parent_table.into(),
397                target_table: target_table.into(),
398                fk_db: fk_db.into(),
399                pk_db: pk_db.into(),
400                filter: Box::new(filter),
401            }),
402        }
403    }
404
405    /// Creates a scalar subquery expression `(SELECT ...)`.
406    ///
407    /// The inner SELECT must return exactly one column and at most one row.
408    pub fn scalar_subquery(subquery: Select) -> Self {
409        Expr::ScalarSubquery(Box::new(subquery))
410    }
411
412    /// Creates a `CASE WHEN condition THEN result ELSE NULL END` expression.
413    pub fn case_when(condition: Expr, then: Expr) -> Self {
414        Expr::CaseWhen {
415            condition: Box::new(condition),
416            then: Box::new(then),
417        }
418    }
419
420    /// Creates the SQL wildcard `*` (for use in `COUNT(*)`).
421    pub fn star() -> Self {
422        Expr::Star
423    }
424}
425
426/// Implements the `!` operator for expressions, producing a SQL `NOT` clause.
427impl std::ops::Not for Expr {
428    type Output = Self;
429
430    fn not(self) -> Self::Output {
431        Expr::Not(Box::new(self))
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn test_column_expr() {
441        let expr = Expr::column("email");
442        match expr {
443            Expr::Column(name) => assert_eq!(name, "email"),
444            _ => panic!("Expected Column variant"),
445        }
446    }
447
448    #[test]
449    fn test_param_expr() {
450        let expr = Expr::param(42i64);
451        match expr {
452            Expr::Param(Value::I64(42)) => {}
453            _ => panic!("Expected Param with I64(42)"),
454        }
455    }
456
457    #[test]
458    fn test_binary_ops() {
459        let col = Expr::column("age");
460        let val = Expr::param(18i64);
461
462        let expr = col.ge(val);
463        match expr {
464            Expr::Binary { op, .. } => assert_eq!(op, BinaryOp::Ge),
465            _ => panic!("Expected Binary expression"),
466        }
467    }
468
469    #[test]
470    fn test_complex_expr() {
471        let expr = Expr::column("age")
472            .ge(Expr::param(18i64))
473            .and(Expr::column("email").like(Expr::param("%@gmail.com")));
474
475        match expr {
476            Expr::Binary { op, .. } => assert_eq!(op, BinaryOp::And),
477            _ => panic!("Expected Binary AND expression"),
478        }
479    }
480
481    #[test]
482    fn test_not_expr() {
483        let expr = !Expr::column("active").eq(Expr::param(true));
484        match expr {
485            Expr::Not(_) => {}
486            _ => panic!("Expected Not expression"),
487        }
488    }
489
490    #[test]
491    fn test_in_list() {
492        let expr = Expr::column("status").in_list(vec![
493            Expr::param(Value::String("active".to_string())),
494            Expr::param(Value::String("pending".to_string())),
495        ]);
496        match expr {
497            Expr::Binary { op, .. } => assert_eq!(op, BinaryOp::In),
498            _ => panic!("Expected Binary IN expression"),
499        }
500    }
501
502    #[test]
503    fn test_not_in_list() {
504        let expr = Expr::column("role").not_in_list(vec![
505            Expr::param(Value::String("admin".to_string())),
506            Expr::param(Value::String("superuser".to_string())),
507        ]);
508        match expr {
509            Expr::Binary { op, .. } => assert_eq!(op, BinaryOp::NotIn),
510            _ => panic!("Expected Binary NOT IN expression"),
511        }
512    }
513
514    #[test]
515    fn test_relation_predicate() {
516        let expr = Expr::relation_some(
517            "posts",
518            "users",
519            "posts",
520            "author_id",
521            "id",
522            Expr::column("posts__published").eq(Expr::param(true)),
523        );
524        match expr {
525            Expr::Relation { op, relation } => {
526                assert_eq!(op, RelationFilterOp::Some);
527                assert_eq!(relation.field, "posts");
528                assert_eq!(relation.parent_table, "users");
529                assert_eq!(relation.target_table, "posts");
530                assert_eq!(relation.fk_db, "author_id");
531                assert_eq!(relation.pk_db, "id");
532            }
533            _ => panic!("Expected relation predicate"),
534        }
535    }
536}