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