rust_ef/query/predicate.rs
1//! Composable query predicates — `AND` / `OR` / `NOT` over expression trees.
2//!
3//! `linq!(|b: T| ...)` produces a self-contained [`Predicate<T>`] that
4//! already carries its bound values, so predicates can be built condition by
5//! condition and combined before they are applied — the rust-ef equivalent of
6//! EF Core's expression-tree `PredicateBuilder`:
7//!
8//! - [`Predicate::always`] / [`Predicate::never`] seed an `AND` / `OR` chain,
9//! - [`Predicate::and`] / [`Predicate::or`] / [`Predicate::not`] nest freely,
10//! with [`Predicate::and_opt`] / [`Predicate::or_opt`] for optional filters,
11//! - [`QueryBuilder::filter`] applies the result, appending it with `AND` and
12//! registering its values in tree order.
13//!
14//! A predicate is a value: it can be stored, cloned, and reused across the
15//! `count()` / page / export queries of the same request, and combined before it
16//! is ever applied. [`BoolExpr`] is the encoded tree behind it; use
17//! [`Predicate::into_expr`] when an API asks for a bare expression.
18
19use std::marker::PhantomData;
20
21use crate::entity::IEntityType;
22
23use super::ast::BoolExpr;
24
25/// A composable WHERE predicate for entity type `T`.
26///
27/// `T` tags the predicate so that fragments written for different entities do
28/// not silently combine or get applied to the wrong query builder.
29///
30/// ```
31/// use rust_ef::prelude::*;
32/// use rust_ef::query::QueryBuilder;
33///
34/// #[derive(Debug, Clone, EntityType)]
35/// #[table("products")]
36/// struct Product {
37/// #[primary_key]
38/// id: i32,
39/// name: String,
40/// description: String,
41/// price: f64,
42/// is_deleted: bool,
43/// }
44///
45/// fn product_filter(keyword: Option<&str>, min_price: Option<f64>) -> Predicate<Product> {
46/// let keyword_group = keyword.map(|keyword| {
47/// linq!(|p: Product| p.name.contains(keyword))
48/// .or(linq!(|p: Product| p.description.contains(keyword)))
49/// });
50/// let min_price = min_price.map(|min_price| linq!(|p: Product| p.price >= min_price));
51///
52/// // always() 是 AND 链起点,本身不产生 SQL;
53/// // and_opt 让「可选条件」不需要额外 if let。
54/// Predicate::<Product>::always()
55/// .and(linq!(|p: Product| !p.is_deleted))
56/// .and_opt(keyword_group)
57/// .and_opt(min_price)
58/// }
59///
60/// let predicate = product_filter(Some("rust"), Some(9.9));
61/// let sql = QueryBuilder::<Product>::new("products")
62/// .filter(predicate)
63/// .to_sql();
64/// // NOT (is_deleted = ?) AND ((name LIKE ?) OR (description LIKE ?)) AND (price >= ?)
65/// assert!(sql.contains("(name LIKE ?) OR (description LIKE ?)"));
66/// ```
67#[derive(Debug, Clone)]
68pub struct Predicate<T: IEntityType> {
69 kind: PredicateKind,
70 _marker: PhantomData<fn() -> T>,
71}
72
73#[derive(Debug, Clone)]
74enum PredicateKind {
75 /// Matches every row (`1=1`) — the identity of `AND`.
76 Always,
77 /// Matches no row (`1=0`) — the identity of `OR`.
78 Never,
79 /// A self-contained expression tree, e.g. from `linq!(|b: T| ...)`.
80 Expr(BoolExpr),
81}
82
83impl<T: IEntityType> Predicate<T> {
84 /// A predicate that matches every row — the seed of an `AND` chain.
85 ///
86 /// `Predicate::always().and(p)` is `p`, so seeding with it adds no SQL.
87 pub fn always() -> Self {
88 Self::with_kind(PredicateKind::Always)
89 }
90
91 /// A predicate that matches no row — the seed of an `OR` chain.
92 ///
93 /// `Predicate::never().or(p)` is `p`, so seeding with it adds no SQL.
94 pub fn never() -> Self {
95 Self::with_kind(PredicateKind::Never)
96 }
97
98 /// Wraps an expression tree as a predicate.
99 ///
100 /// Prefer `linq!(|b: T| ...)`, which is already typed and
101 /// self-contained. This is the unchecked door for reusing a `BoolExpr`
102 /// produced elsewhere (e.g. a filter read back from `ModelBuilder`).
103 pub fn from_expr(expr: BoolExpr) -> Self {
104 Self::with_kind(PredicateKind::Expr(expr))
105 }
106
107 fn with_kind(kind: PredicateKind) -> Self {
108 Self {
109 kind,
110 _marker: PhantomData,
111 }
112 }
113
114 /// Whether the predicate matches every row (the `AND` identity).
115 pub fn is_always(&self) -> bool {
116 matches!(self.kind, PredicateKind::Always)
117 }
118
119 /// Whether the predicate matches no row.
120 pub fn is_never(&self) -> bool {
121 matches!(self.kind, PredicateKind::Never)
122 }
123
124 /// `self AND other`.
125 ///
126 /// `always()` is the identity (`always().and(p) == p`) and `never()` is
127 /// absorbing (`never().and(p) == never()`), so identities never leak into
128 /// the generated SQL.
129 pub fn and(self, other: impl Into<Predicate<T>>) -> Self {
130 let rhs = other.into().kind;
131 let kind = match (self.kind, rhs) {
132 (PredicateKind::Never, _) | (_, PredicateKind::Never) => PredicateKind::Never,
133 (PredicateKind::Always, rhs) => rhs,
134 (lhs, PredicateKind::Always) => lhs,
135 (PredicateKind::Expr(lhs), PredicateKind::Expr(rhs)) => {
136 PredicateKind::Expr(BoolExpr::And(Box::new(lhs), Box::new(rhs)))
137 }
138 };
139 Self::with_kind(kind)
140 }
141
142 /// `self AND other` when `other` is `Some` — the one-liner for an optional
143 /// filter. `None` leaves the predicate unchanged.
144 pub fn and_opt(self, other: Option<impl Into<Predicate<T>>>) -> Self {
145 match other {
146 Some(other) => self.and(other),
147 None => self,
148 }
149 }
150
151 /// `self OR other`.
152 ///
153 /// `never()` is the identity (`never().or(p) == p`) and `always()` is
154 /// absorbing (`always().or(p) == always()`).
155 pub fn or(self, other: impl Into<Predicate<T>>) -> Self {
156 let rhs = other.into().kind;
157 let kind = match (self.kind, rhs) {
158 (PredicateKind::Always, _) | (_, PredicateKind::Always) => PredicateKind::Always,
159 (PredicateKind::Never, rhs) => rhs,
160 (lhs, PredicateKind::Never) => lhs,
161 (PredicateKind::Expr(lhs), PredicateKind::Expr(rhs)) => {
162 PredicateKind::Expr(BoolExpr::Or(Box::new(lhs), Box::new(rhs)))
163 }
164 };
165 Self::with_kind(kind)
166 }
167
168 /// `self OR other` when `other` is `Some`; `None` leaves the predicate
169 /// unchanged.
170 pub fn or_opt(self, other: Option<impl Into<Predicate<T>>>) -> Self {
171 match other {
172 Some(other) => self.or(other),
173 None => self,
174 }
175 }
176
177 /// `NOT self`.
178 #[allow(clippy::should_implement_trait)]
179 pub fn not(self) -> Self {
180 let kind = match self.kind {
181 PredicateKind::Always => PredicateKind::Never,
182 PredicateKind::Never => PredicateKind::Always,
183 PredicateKind::Expr(expr) => PredicateKind::Expr(BoolExpr::Not(Box::new(expr))),
184 };
185 Self::with_kind(kind)
186 }
187
188 /// Converts the predicate to a [`BoolExpr`] — for APIs that take an
189 /// expression directly, such as `ModelBuilder::has_query_filter`.
190 ///
191 /// `always()` and `never()` become the literal fragments `1=1` / `1=0`.
192 pub fn into_expr(self) -> BoolExpr {
193 match self.kind {
194 PredicateKind::Always => BoolExpr::raw("1=1"),
195 PredicateKind::Never => BoolExpr::raw("1=0"),
196 PredicateKind::Expr(expr) => expr,
197 }
198 }
199
200 /// WHERE-clause view: `None` for the `AND` identity (`always()`), which
201 /// needs no SQL at all.
202 pub(crate) fn into_where_expr(self) -> Option<BoolExpr> {
203 match self.kind {
204 PredicateKind::Always => None,
205 PredicateKind::Never => Some(BoolExpr::raw("1=0")),
206 PredicateKind::Expr(expr) => Some(expr),
207 }
208 }
209}
210
211impl<T: IEntityType> From<BoolExpr> for Predicate<T> {
212 /// Lets `BoolExpr` fragments (e.g. a stored global filter) be combined
213 /// directly. This conversion is unchecked: prefer `linq!(...)`.
214 fn from(expr: BoolExpr) -> Self {
215 Self::from_expr(expr)
216 }
217}
218
219impl<T: IEntityType> From<Predicate<T>> for BoolExpr {
220 fn from(predicate: Predicate<T>) -> Self {
221 predicate.into_expr()
222 }
223}