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}