Skip to main content

siderite_orm/expr/
mod.rs

1//! Typed expression AST (Django `Q` / `F` / lookup equivalents).
2//!
3//! Expressions are pure data: building them never performs I/O. Backends
4//! compile them into their own dialect.
5//!
6//! Two layers exist:
7//! * [`Expr`]: untyped tree stored inside a [`QueryPlan`].
8//! * [`Field<M, T>`]: a typed column handle. The `Model` derive generates one
9//!   associated constant per field (`User::name`), so lookups are checked at
10//!   compile time: `icontains` exists only on text fields, comparison values
11//!   must convert into the field's type. [`Calc`] (arithmetic results) and
12//!   [`Joined`] (fields reached through a foreign key) offer the same lookups.
13//!
14//! Function, aggregate and window constructors live in [`functions`],
15//! [`Count`]/[`Sum`]/... and [`RowNumber`]/[`Rank`]/....
16
17mod aggregate;
18mod field;
19pub mod functions;
20mod related;
21mod window;
22
23pub use aggregate::{
24    AggFunc, Aggregate, ArrayAgg, Avg, Count, Max, Min, StdDev, StringAgg, Sum, Variance,
25};
26pub use field::{Calc, DateLike, Field, Literal, Numeric, Operand, TextLike, TimeLike};
27pub use functions::{CaseBuilder, DatePart, Function};
28pub use related::{FkSlot, Joined, RelHop, Related, RelatedColumn, path_alias_of};
29pub use window::{
30    CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, Ntile, PercentRank, Rank, RowNumber,
31    Window, WindowFunc,
32};
33
34use crate::model::Model;
35use crate::plan::{OrderDirection, OrderExpr, QueryPlan};
36use crate::types::SqlType;
37use crate::value::Value;
38use std::borrow::Cow;
39
40/// Identifier type used for tables, aliases and columns.
41pub type Ident = Cow<'static, str>;
42
43/// A (optionally source-qualified) column reference.
44#[derive(Debug, Clone, PartialEq)]
45pub struct Column {
46    /// Table / alias the column belongs to. `None` means the plan's root source.
47    pub source: Option<Ident>,
48    /// Column name.
49    pub name: Ident,
50}
51
52impl Column {
53    /// Column on the root source.
54    pub fn new(name: impl Into<Ident>) -> Self {
55        Self {
56            source: None,
57            name: name.into(),
58        }
59    }
60
61    /// Column qualified by a table or join alias.
62    pub fn qualified(source: impl Into<Ident>, name: impl Into<Ident>) -> Self {
63        Self {
64            source: Some(source.into()),
65            name: name.into(),
66        }
67    }
68}
69
70/// Binary operators: comparisons, boolean connectives and arithmetic.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[allow(missing_docs)]
73pub enum BinaryOp {
74    Eq,
75    Ne,
76    Lt,
77    Le,
78    Gt,
79    Ge,
80    Add,
81    Sub,
82    Mul,
83    Div,
84    Mod,
85}
86
87/// Unary operators.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum UnaryOp {
90    /// Boolean negation.
91    Not,
92    /// Arithmetic negation.
93    Neg,
94}
95
96/// Lookups that are not plain binary comparisons.
97#[derive(Debug, Clone, PartialEq)]
98pub enum Lookup {
99    /// Case-insensitive equality.
100    IExact(Value),
101    /// Substring match; `case_insensitive` selects `icontains`.
102    Contains {
103        /// Pattern (literal text, not a LIKE pattern; backends escape it).
104        needle: String,
105        /// Case-insensitive?
106        case_insensitive: bool,
107    },
108    /// Prefix match.
109    StartsWith {
110        /// Prefix (literal text).
111        needle: String,
112        /// Case-insensitive?
113        case_insensitive: bool,
114    },
115    /// Suffix match.
116    EndsWith {
117        /// Suffix (literal text).
118        needle: String,
119        /// Case-insensitive?
120        case_insensitive: bool,
121    },
122    /// Regex match (capability-gated).
123    Regex(String),
124    /// Membership in a literal list.
125    In(Vec<Value>),
126    /// Membership in the single column produced by a subquery.
127    InSubquery(Box<QueryPlan>),
128    /// Inclusive range `lo <= x <= hi`.
129    Range(Value, Value),
130    /// `IS NULL` (true) / `IS NOT NULL` (false).
131    IsNull(bool),
132}
133
134/// Expression tree node.
135#[derive(Debug, Clone, PartialEq)]
136pub enum Expr {
137    /// Column reference (Django `F`).
138    Column(Column),
139    /// Literal, always bound as a parameter.
140    Value(Value),
141    /// Binary operation.
142    Binary {
143        /// Operator.
144        op: BinaryOp,
145        /// Left operand.
146        lhs: Box<Expr>,
147        /// Right operand.
148        rhs: Box<Expr>,
149    },
150    /// Unary operation.
151    Unary {
152        /// Operator.
153        op: UnaryOp,
154        /// Operand.
155        expr: Box<Expr>,
156    },
157    /// Conjunction of all children (empty = TRUE).
158    And(Vec<Expr>),
159    /// Disjunction of all children (empty = FALSE).
160    Or(Vec<Expr>),
161    /// Lookup applied to an expression.
162    Lookup {
163        /// Target expression.
164        expr: Box<Expr>,
165        /// Lookup.
166        lookup: Lookup,
167    },
168    /// `EXISTS (subquery)`.
169    Exists(Box<QueryPlan>),
170    /// Scalar subquery.
171    Subquery(Box<QueryPlan>),
172    /// Scalar function call.
173    Func {
174        /// Function.
175        func: Function,
176        /// Arguments.
177        args: Vec<Expr>,
178    },
179    /// `CAST(expr AS type)`.
180    Cast {
181        /// Operand.
182        expr: Box<Expr>,
183        /// Target type family.
184        ty: SqlType,
185    },
186    /// Searched `CASE WHEN .. THEN .. [ELSE ..] END`.
187    Case {
188        /// `(condition, result)` branches, tried in order.
189        branches: Vec<(Expr, Expr)>,
190        /// `ELSE` result (`NULL` when absent).
191        otherwise: Option<Box<Expr>>,
192    },
193    /// Aggregate function (`COUNT`, `SUM`, ...).
194    Aggregate(Aggregate),
195    /// Window function with its `OVER (..)` clause.
196    Window(Box<Window>),
197    /// Date or time component of a temporal expression.
198    DatePart {
199        /// Component to extract.
200        part: DatePart,
201        /// Temporal operand.
202        expr: Box<Expr>,
203    },
204    /// Column of the enclosing query, used inside a correlated subquery.
205    OuterRef(Column),
206    /// Column reached through foreign keys; resolved into joins by
207    /// [`QueryPlan::resolve_relations`].
208    Related(RelatedColumn),
209}
210
211impl Expr {
212    /// Column expression on the root source.
213    pub fn col(name: impl Into<Ident>) -> Self {
214        Expr::Column(Column::new(name))
215    }
216
217    /// Literal value expression.
218    pub fn val(v: impl Into<Value>) -> Self {
219        Expr::Value(v.into())
220    }
221
222    /// Reference to a column of the enclosing query (Django `OuterRef`).
223    pub fn outer(name: impl Into<Ident>) -> Self {
224        Expr::OuterRef(Column::new(name))
225    }
226
227    /// `EXISTS (plan)`.
228    pub fn exists(plan: QueryPlan) -> Self {
229        Expr::Exists(Box::new(plan))
230    }
231
232    /// Scalar subquery: `plan` must select exactly one column and one row.
233    pub fn subquery(plan: QueryPlan) -> Self {
234        Expr::Subquery(Box::new(plan))
235    }
236
237    fn binary(self, op: BinaryOp, rhs: impl Into<Expr>) -> Self {
238        Expr::Binary {
239            op,
240            lhs: Box::new(self),
241            rhs: Box::new(rhs.into()),
242        }
243    }
244
245    fn lookup(self, lookup: Lookup) -> Self {
246        Expr::Lookup {
247            expr: Box::new(self),
248            lookup,
249        }
250    }
251
252    /// `self = rhs`. A `NULL` right-hand side compiles to `IS NULL`.
253    pub fn eq(self, rhs: impl Into<Expr>) -> Self {
254        self.binary(BinaryOp::Eq, rhs)
255    }
256    /// `self <> rhs`. A `NULL` right-hand side compiles to `IS NOT NULL`.
257    pub fn ne(self, rhs: impl Into<Expr>) -> Self {
258        self.binary(BinaryOp::Ne, rhs)
259    }
260    /// `self < rhs`.
261    pub fn lt(self, rhs: impl Into<Expr>) -> Self {
262        self.binary(BinaryOp::Lt, rhs)
263    }
264    /// `self <= rhs`.
265    pub fn le(self, rhs: impl Into<Expr>) -> Self {
266        self.binary(BinaryOp::Le, rhs)
267    }
268    /// `self > rhs`.
269    pub fn gt(self, rhs: impl Into<Expr>) -> Self {
270        self.binary(BinaryOp::Gt, rhs)
271    }
272    /// `self >= rhs`.
273    pub fn ge(self, rhs: impl Into<Expr>) -> Self {
274        self.binary(BinaryOp::Ge, rhs)
275    }
276
277    /// Logical AND; flattens nested ANDs.
278    pub fn and(self, rhs: Expr) -> Self {
279        match (self, rhs) {
280            (Expr::And(mut a), Expr::And(b)) => {
281                a.extend(b);
282                Expr::And(a)
283            }
284            (Expr::And(mut a), b) => {
285                a.push(b);
286                Expr::And(a)
287            }
288            (a, b) => Expr::And(vec![a, b]),
289        }
290    }
291
292    /// Logical OR; flattens nested ORs.
293    pub fn or(self, rhs: Expr) -> Self {
294        match (self, rhs) {
295            (Expr::Or(mut a), Expr::Or(b)) => {
296                a.extend(b);
297                Expr::Or(a)
298            }
299            (Expr::Or(mut a), b) => {
300                a.push(b);
301                Expr::Or(a)
302            }
303            (a, b) => Expr::Or(vec![a, b]),
304        }
305    }
306
307    /// Membership test.
308    pub fn is_in<I, V>(self, values: I) -> Self
309    where
310        I: IntoIterator<Item = V>,
311        V: Into<Value>,
312    {
313        self.lookup(Lookup::In(values.into_iter().map(Into::into).collect()))
314    }
315
316    /// Membership in the single column selected by `plan`.
317    pub fn in_subquery(self, plan: QueryPlan) -> Self {
318        self.lookup(Lookup::InSubquery(Box::new(plan)))
319    }
320
321    /// Inclusive range.
322    pub fn range(self, lo: impl Into<Value>, hi: impl Into<Value>) -> Self {
323        self.lookup(Lookup::Range(lo.into(), hi.into()))
324    }
325
326    /// `IS NULL` / `IS NOT NULL`.
327    pub fn is_null(self, yes: bool) -> Self {
328        self.lookup(Lookup::IsNull(yes))
329    }
330
331    /// Case-insensitive equality (text).
332    pub fn iexact(self, v: impl Into<Value>) -> Self {
333        self.lookup(Lookup::IExact(v.into()))
334    }
335
336    /// Regex match (requires [`Feature::Regex`](crate::Feature::Regex)).
337    pub fn regex(self, pattern: impl Into<String>) -> Self {
338        self.lookup(Lookup::Regex(pattern.into()))
339    }
340
341    /// `CAST(self AS ty)`.
342    pub fn cast(self, ty: SqlType) -> Self {
343        Expr::Cast {
344            expr: Box::new(self),
345            ty,
346        }
347    }
348
349    /// Extract a date or time component.
350    pub fn date_part(self, part: DatePart) -> Self {
351        Expr::DatePart {
352            part,
353            expr: Box::new(self),
354        }
355    }
356
357    /// Ascending ordering term.
358    pub fn asc(self) -> OrderExpr {
359        OrderExpr {
360            expr: self,
361            direction: OrderDirection::Asc,
362        }
363    }
364
365    /// Descending ordering term.
366    pub fn desc(self) -> OrderExpr {
367        OrderExpr {
368            expr: self,
369            direction: OrderDirection::Desc,
370        }
371    }
372
373    /// Direct sub-expressions (subquery plans are reached through
374    /// [`subplan`](Self::subplan) instead).
375    pub fn children(&self) -> Vec<&Expr> {
376        match self {
377            Expr::Column(_)
378            | Expr::Value(_)
379            | Expr::OuterRef(_)
380            | Expr::Related(_)
381            | Expr::Exists(_)
382            | Expr::Subquery(_) => Vec::new(),
383            Expr::Binary { lhs, rhs, .. } => vec![lhs, rhs],
384            Expr::Unary { expr, .. }
385            | Expr::Lookup { expr, .. }
386            | Expr::Cast { expr, .. }
387            | Expr::DatePart { expr, .. } => vec![expr],
388            Expr::And(items) | Expr::Or(items) => items.iter().collect(),
389            Expr::Func { args, .. } => args.iter().collect(),
390            Expr::Case {
391                branches,
392                otherwise,
393            } => branches
394                .iter()
395                .flat_map(|(when, then)| [when, then])
396                .chain(otherwise.as_deref())
397                .collect(),
398            Expr::Aggregate(agg) => agg.children(),
399            Expr::Window(window) => window.children(),
400        }
401    }
402
403    /// Mutable counterpart of [`children`](Self::children).
404    pub fn children_mut(&mut self) -> Vec<&mut Expr> {
405        match self {
406            Expr::Column(_)
407            | Expr::Value(_)
408            | Expr::OuterRef(_)
409            | Expr::Related(_)
410            | Expr::Exists(_)
411            | Expr::Subquery(_) => Vec::new(),
412            Expr::Binary { lhs, rhs, .. } => vec![lhs, rhs],
413            Expr::Unary { expr, .. }
414            | Expr::Lookup { expr, .. }
415            | Expr::Cast { expr, .. }
416            | Expr::DatePart { expr, .. } => vec![expr],
417            Expr::And(items) | Expr::Or(items) => items.iter_mut().collect(),
418            Expr::Func { args, .. } => args.iter_mut().collect(),
419            Expr::Case {
420                branches,
421                otherwise,
422            } => branches
423                .iter_mut()
424                .flat_map(|(when, then)| [when, then])
425                .chain(otherwise.as_deref_mut())
426                .collect(),
427            Expr::Aggregate(agg) => agg.children_mut(),
428            Expr::Window(window) => window.children_mut(),
429        }
430    }
431
432    /// The nested query of `EXISTS`, a scalar subquery or `IN (subquery)`.
433    pub fn subplan(&self) -> Option<&QueryPlan> {
434        match self {
435            Expr::Exists(plan)
436            | Expr::Subquery(plan)
437            | Expr::Lookup {
438                lookup: Lookup::InSubquery(plan),
439                ..
440            } => Some(plan),
441            _ => None,
442        }
443    }
444
445    /// Mutable counterpart of [`subplan`](Self::subplan).
446    pub fn subplan_mut(&mut self) -> Option<&mut QueryPlan> {
447        match self {
448            Expr::Exists(plan)
449            | Expr::Subquery(plan)
450            | Expr::Lookup {
451                lookup: Lookup::InSubquery(plan),
452                ..
453            } => Some(plan),
454            _ => None,
455        }
456    }
457
458    /// Whether this expression computes an aggregate over the query's rows
459    /// (so a filter on it belongs in `HAVING`). Aggregates inside window
460    /// functions and subqueries do not count.
461    pub fn contains_aggregate(&self) -> bool {
462        matches!(self, Expr::Aggregate(_)) || self.children().iter().any(|c| c.contains_aggregate())
463    }
464
465    /// Whether this expression contains a window function.
466    pub fn contains_window(&self) -> bool {
467        matches!(self, Expr::Window(_)) || self.children().iter().any(|c| c.contains_window())
468    }
469}
470
471impl std::ops::Not for Expr {
472    type Output = Expr;
473    fn not(self) -> Expr {
474        Expr::Unary {
475            op: UnaryOp::Not,
476            expr: Box::new(self),
477        }
478    }
479}
480
481impl std::ops::Neg for Expr {
482    type Output = Expr;
483    fn neg(self) -> Expr {
484        Expr::Unary {
485            op: UnaryOp::Neg,
486            expr: Box::new(self),
487        }
488    }
489}
490
491macro_rules! arith_ops {
492    ($($trait:ident $method:ident $op:ident),*) => {$(
493        impl<R: Into<Expr>> std::ops::$trait<R> for Expr {
494            type Output = Expr;
495            fn $method(self, rhs: R) -> Expr { self.binary(BinaryOp::$op, rhs) }
496        }
497    )*};
498}
499arith_ops!(Add add Add, Sub sub Sub, Mul mul Mul, Div div Div, Rem rem Mod);
500
501impl<T: Into<Value>> From<T> for Expr {
502    fn from(v: T) -> Self {
503        Expr::Value(v.into())
504    }
505}
506
507/// Model whose primary key column is `M`'s; helper for generated code and
508/// relation handles.
509pub(crate) fn pk_column<M: Model>() -> Ident {
510    M::META.pk().map_or("id", |f| f.column).into()
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    struct User;
518    #[allow(non_upper_case_globals)]
519    impl User {
520        const name: Field<User, String> = Field::new("name");
521        const likes: Field<User, i64> = Field::new("likes");
522        const dislikes: Field<User, i64> = Field::new("dislikes");
523        const age: Field<User, Option<i32>> = Field::new("age");
524    }
525
526    #[test]
527    fn typed_comparisons_build_trees() {
528        let e = User::likes.gt(User::dislikes);
529        assert_eq!(e, Expr::col("likes").gt(Expr::col("dislikes")));
530        assert_eq!(User::likes.eq(3_i64), Expr::col("likes").eq(3_i64));
531        assert_eq!(User::name.eq("bob"), Expr::col("name").eq("bob"));
532        assert_eq!(User::age.eq(3), Expr::col("age").eq(3));
533    }
534
535    #[test]
536    fn boolean_composition_flattens() {
537        let e = User::name
538            .icontains("al")
539            .or(User::name.ends_with("x"))
540            .or(User::likes.is_null());
541        assert!(matches!(e, Expr::Or(ref v) if v.len() == 3));
542        assert!(matches!(
543            !e,
544            Expr::Unary {
545                op: UnaryOp::Not,
546                ..
547            }
548        ));
549    }
550
551    #[test]
552    fn arithmetic_is_typed_and_composable() {
553        let net = User::likes - User::dislikes;
554        let e = User::likes.eq(net.clone() * 2_i64);
555        assert!(matches!(
556            e,
557            Expr::Binary {
558                op: BinaryOp::Eq,
559                ..
560            }
561        ));
562        assert!(net.gt(0_i64).children().len() == 2);
563    }
564
565    #[test]
566    fn aggregates_are_found_outside_windows_and_subqueries() {
567        let sum = Sum::of(Expr::col("x"));
568        assert!(Expr::from(sum.clone()).gt(1_i64).contains_aggregate());
569        let windowed = Window::over(WindowFunc::Aggregate(sum));
570        assert!(!Expr::from(windowed).contains_aggregate());
571    }
572}