Skip to main content

umbral_core/orm/
expr.rs

1//! `F`-expressions and `Q`-objects — composable predicates and column references.
2//!
3//! # F-expressions
4//!
5//! [`F`] wraps a column name as a first-class value so it can appear on the
6//! right-hand side of a comparison (column-vs-column WHERE) or inside an
7//! atomic update expression.
8//!
9//! ## Column-vs-column WHERE
10//!
11//! ```rust,ignore
12//! use umbral::orm::F;
13//!
14//! // WHERE author = editor
15//! Post::objects()
16//!     .filter(post::AUTHOR.eq_f(F::col("editor")))
17//!     .fetch()
18//!     .await?;
19//! ```
20//!
21//! ## Atomic update arithmetic
22//!
23//! ```rust,ignore
24//! use umbral::orm::{F, FExpr};
25//!
26//! // SET views = views + 1
27//! Post::objects()
28//!     .filter(post::ID.eq(42))
29//!     .update_expr("views", F::col("views").add(1))
30//!     .await?;
31//! ```
32//!
33//! # Q-objects
34//!
35//! [`Q`] composes predicates with explicit AND, OR, and NOT so complex
36//! boolean trees can be built before being handed to `.filter()`. The
37//! existing `&` / `|` operators on `Predicate<T>` continue to work; `Q` adds
38//! named constructors and `Q::not` for single-predicate negation.
39//!
40//! ```rust,ignore
41//! use umbral::orm::Q;
42//!
43//! Post::objects()
44//!     .filter(Q::or(post::PUBLISHED.eq(true), post::AUTHOR.eq(user_id)))
45//!     .filter(Q::not(post::AUTHOR.eq(spam_user_id)))
46//!     .fetch()
47//!     .await?;
48//! ```
49
50use sea_query::{Alias, Expr as SqExpr};
51
52use super::Predicate;
53
54// ===========================================================================
55// F-expressions
56// ===========================================================================
57
58/// A reference to a column on the current model's table.
59///
60/// Use this wherever you need to compare two columns on the same row or
61/// reference a column on the right-hand side of an UPDATE `SET` clause.
62///
63/// # Construction
64///
65/// ```rust,ignore
66/// use umbral::orm::F;
67///
68/// let col_ref = F::col("views");
69/// ```
70///
71/// # Arithmetic
72///
73/// [`F::col`] returns an [`FExpr`] that supports `.add(n)`, `.sub(n)`,
74/// `.mul(n)`, and `.div(n)` so you can express `SET views = views + 1`
75/// without string formatting.
76/// `F` is a namespace for the `col` factory method; it has no instance state.
77pub struct F;
78
79impl F {
80    /// Create an F-expression referencing the column with the given name.
81    ///
82    /// The name must be a column that exists on the current model's table;
83    /// unknown names produce a database-level error at runtime, not a
84    /// compile-time one.
85    pub fn col(name: impl Into<String>) -> FExpr {
86        FExpr {
87            inner: FExprInner::Column(name.into()),
88        }
89    }
90}
91
92/// The internal representation of an F-expression tree.
93#[derive(Clone, Debug)]
94enum FExprInner {
95    /// A bare column reference: `col_name`.
96    Column(String),
97    /// `lhs + rhs`.
98    Add(Box<FExpr>, Box<FExpr>),
99    /// `lhs - rhs`.
100    Sub(Box<FExpr>, Box<FExpr>),
101    /// `lhs * rhs`.
102    Mul(Box<FExpr>, Box<FExpr>),
103    /// `lhs / rhs`.
104    Div(Box<FExpr>, Box<FExpr>),
105    /// A literal integer constant.
106    LitI64(i64),
107}
108
109/// An expression that can appear in a `SET col = <expr>` clause.
110///
111/// Built via [`F::col`] and the arithmetic methods on this type. Passed to
112/// [`QuerySet::update_expr`] to perform atomic column updates.
113#[derive(Clone, Debug)]
114pub struct FExpr {
115    inner: FExprInner,
116}
117
118impl FExpr {
119    /// Render this expression as a `sea_query::SimpleExpr` for use in an
120    /// UPDATE's SET clause or a WHERE condition.
121    pub(crate) fn to_simple_expr(&self) -> sea_query::SimpleExpr {
122        match &self.inner {
123            FExprInner::Column(name) => SqExpr::col(Alias::new(name.as_str())).into(),
124            FExprInner::Add(lhs, rhs) => {
125                let l = lhs.to_simple_expr();
126                let r = rhs.to_simple_expr();
127                l.add(r)
128            }
129            FExprInner::Sub(lhs, rhs) => {
130                let l = lhs.to_simple_expr();
131                let r = rhs.to_simple_expr();
132                l.sub(r)
133            }
134            FExprInner::Mul(lhs, rhs) => {
135                let l = lhs.to_simple_expr();
136                let r = rhs.to_simple_expr();
137                l.mul(r)
138            }
139            FExprInner::Div(lhs, rhs) => {
140                let l = lhs.to_simple_expr();
141                let r = rhs.to_simple_expr();
142                l.div(r)
143            }
144            FExprInner::LitI64(n) => {
145                sea_query::SimpleExpr::Value(sea_query::Value::BigInt(Some(*n)))
146            }
147        }
148    }
149
150    /// `self + n` — produces `col + n` in the generated SQL.
151    #[allow(clippy::should_implement_trait)]
152    pub fn add(self, n: i64) -> FExpr {
153        FExpr {
154            inner: FExprInner::Add(Box::new(self), Box::new(FExpr::lit_i64(n))),
155        }
156    }
157
158    /// `self - n` — produces `col - n` in the generated SQL.
159    #[allow(clippy::should_implement_trait)]
160    pub fn sub(self, n: i64) -> FExpr {
161        FExpr {
162            inner: FExprInner::Sub(Box::new(self), Box::new(FExpr::lit_i64(n))),
163        }
164    }
165
166    /// `self * n` — produces `col * n` in the generated SQL.
167    #[allow(clippy::should_implement_trait)]
168    pub fn mul(self, n: i64) -> FExpr {
169        FExpr {
170            inner: FExprInner::Mul(Box::new(self), Box::new(FExpr::lit_i64(n))),
171        }
172    }
173
174    /// `self / n` — produces `col / n` in the generated SQL.
175    #[allow(clippy::should_implement_trait)]
176    pub fn div(self, n: i64) -> FExpr {
177        FExpr {
178            inner: FExprInner::Div(Box::new(self), Box::new(FExpr::lit_i64(n))),
179        }
180    }
181
182    fn lit_i64(n: i64) -> FExpr {
183        FExpr {
184            inner: FExprInner::LitI64(n),
185        }
186    }
187}
188
189// ===========================================================================
190// F-expression predicates on column types.
191//
192// `IntCol` and `ForeignKeyCol` gain `.eq_f(FExpr)` / `.ne_f(FExpr)` so a
193// column-vs-column WHERE is spelled naturally. The `_f` suffix avoids
194// collision with the existing `.eq(i64)` methods. The impl lives here in
195// expr.rs rather than in column.rs so the column module doesn't need to
196// depend on FExpr and the dependency graph stays clean.
197// ===========================================================================
198
199/// Extension trait that adds `eq_f` / `ne_f` to column handles so a column
200/// can be compared against an F-expression (another column or arithmetic).
201///
202/// Implemented for the integer and foreign-key column types. String and
203/// datetime columns gain this too when a consumer surfaces the need.
204pub trait FColExt<T> {
205    /// `WHERE <col> = <expr>`.
206    fn eq_f(&self, expr: FExpr) -> Predicate<T>;
207    /// `WHERE <col> <> <expr>`.
208    fn ne_f(&self, expr: FExpr) -> Predicate<T>;
209}
210
211impl<T> FColExt<T> for crate::orm::column::IntCol<T> {
212    fn eq_f(&self, expr: FExpr) -> Predicate<T> {
213        Predicate::new(SqExpr::col(Alias::new(self.name)).eq(expr.to_simple_expr()))
214    }
215    fn ne_f(&self, expr: FExpr) -> Predicate<T> {
216        Predicate::new(SqExpr::col(Alias::new(self.name)).ne(expr.to_simple_expr()))
217    }
218}
219
220impl<T> FColExt<T> for crate::orm::column::ForeignKeyCol<T> {
221    fn eq_f(&self, expr: FExpr) -> Predicate<T> {
222        Predicate::new(SqExpr::col(Alias::new(self.name)).eq(expr.to_simple_expr()))
223    }
224    fn ne_f(&self, expr: FExpr) -> Predicate<T> {
225        Predicate::new(SqExpr::col(Alias::new(self.name)).ne(expr.to_simple_expr()))
226    }
227}
228
229impl<T> FColExt<T> for crate::orm::column::StrCol<T> {
230    fn eq_f(&self, expr: FExpr) -> Predicate<T> {
231        Predicate::new(SqExpr::col(Alias::new(self.name)).eq(expr.to_simple_expr()))
232    }
233    fn ne_f(&self, expr: FExpr) -> Predicate<T> {
234        Predicate::new(SqExpr::col(Alias::new(self.name)).ne(expr.to_simple_expr()))
235    }
236}
237
238// ===========================================================================
239// Q-objects
240// ===========================================================================
241
242/// A composable predicate builder.
243///
244/// `Q` provides named constructors for the three logical connectives — AND,
245/// OR, and NOT — so complex WHERE trees can be expressed without reaching
246/// for the `&` / `|` operator overloads. Both styles coexist: `Q::and(a, b)`
247/// is the same as `a & b`; pick whichever reads better for your query.
248///
249/// ```rust,ignore
250/// use umbral::orm::Q;
251///
252/// // OR: published OR authored by this user
253/// Post::objects()
254///     .filter(Q::or(
255///         post::PUBLISHED.eq(true),
256///         post::AUTHOR.eq(user_id),
257///     ))
258///     .fetch()
259///     .await?;
260///
261/// // NOT: exclude spam author
262/// Post::objects()
263///     .filter(Q::not(post::AUTHOR.eq(spam_id)))
264///     .fetch()
265///     .await?;
266///
267/// // Nesting: (published AND title contains "rust") OR (author = me)
268/// Post::objects()
269///     .filter(Q::or(
270///         Q::and(post::PUBLISHED.eq(true), post::TITLE.contains("rust")),
271///         post::AUTHOR.eq(my_id),
272///     ))
273///     .fetch()
274///     .await?;
275/// ```
276pub struct Q;
277
278impl Q {
279    /// Combine two predicates with logical AND.
280    ///
281    /// `Q::and(a, b)` is equivalent to `a & b`. Use this form when
282    /// building the condition dynamically or when the infix notation
283    /// reduces readability (e.g. deeply nested trees).
284    pub fn and<T>(a: Predicate<T>, b: Predicate<T>) -> Predicate<T> {
285        a & b
286    }
287
288    /// Combine two predicates with logical OR.
289    ///
290    /// `Q::or(a, b)` is equivalent to `a | b`.
291    pub fn or<T>(a: Predicate<T>, b: Predicate<T>) -> Predicate<T> {
292        a | b
293    }
294
295    /// Negate a predicate with logical NOT.
296    ///
297    /// Wraps the condition's `sea_query::SimpleExpr` in a NOT() wrapper.
298    /// Both the default (`cond`) and optional SQLite override
299    /// (`cond_sqlite`) are negated element-wise so backend routing stays
300    /// correct.
301    pub fn not<T>(p: Predicate<T>) -> Predicate<T> {
302        use std::marker::PhantomData;
303        let negated_cond = p.cond.not();
304        let negated_sqlite = p.cond_sqlite.map(|c| c.not());
305        Predicate {
306            cond: negated_cond,
307            cond_sqlite: negated_sqlite,
308            _phantom: PhantomData,
309        }
310    }
311}