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