drizzle_core/expr/cond.rs
1//! Condition lists: tuples as conjunctions, plus the [`all`] and [`any`] combinators.
2//!
3//! A tuple of conditions *is* a condition. It renders as the parenthesized AND
4//! of its elements (`(a AND b AND c)`) and has the same nullability, aggregate
5//! kind and sources as a chain of [`and`](super::and) calls, so
6//! `and(a, and(b, c))` can be written `(a, b, c)` anywhere a condition is
7//! accepted (for example in `.r#where(...)`). Its SQL type is
8//! [`Conjunction`](crate::types::Conjunction) rather than the dialect's
9//! boolean. Bare tuples work up to 8 elements; use [`all`] or nested tuples
10//! for longer lists.
11//!
12//! ```rust
13//! # use drizzle_core::dialect::{Dialect, DialectTypes, SQLiteDialect as D};
14//! # use drizzle_core::{ColumnRef, SQL, SQLParam, expr::*};
15//! # #[derive(Clone, Debug)] struct Value(String);
16//! # impl SQLParam for Value { const DIALECT: Dialect = Dialect::SQLite; type DialectMarker = D; }
17//! # impl<X: ToString> From<X> for Value { fn from(v: X) -> Self { Value(v.to_string()) } }
18//! # impl From<Value> for std::borrow::Cow<'_, Value> { fn from(v: Value) -> Self { Self::Owned(v) } }
19//! # type C<X, N = NonNull> = &'static SQLExpr<'static, Value, X, N>;
20//! # fn col<X: drizzle_core::types::DataType, N: Nullability>(c: &'static str) -> C<X, N> { Box::leak(Box::new(SQLExpr::new(SQL::column(ColumnRef::sql("users", c))))) }
21//! # type Int = <D as DialectTypes>::Int; type Text = <D as DialectTypes>::Text; type Real = <D as DialectTypes>::Double;
22//! # struct Users { id: C<Int>, age: C<Int>, name: C<Text>, email: C<Text, Null>, score: C<Real, Null>, active: C<<D as DialectTypes>::Bool>, created_at: C<<D as DialectTypes>::Timestamp> }
23//! # let users = Users { id: col("id"), age: col("age"), name: col("name"), email: col("email"), score: col("score"), active: col("active"), created_at: col("created_at") };
24//! let filter = (gt(users.age, 18), is_not_null(users.email), like(users.name, "A%"));
25//! assert_eq!(
26//! filter.into_expr_sql().sql(),
27//! r#"("users"."age" > ? AND "users"."email" IS NOT NULL AND "users"."name" LIKE ?)"#
28//! );
29//!
30//! // OR lists use `any`.
31//! let staff = any((eq(users.name, "admin"), eq(users.name, "moderator")));
32//! assert_eq!(staff.sql(), r#"("users"."name" = ? OR "users"."name" = ?)"#);
33//! ```
34//!
35//! # Optional elements
36//!
37//! Any element may be an [`Option`]. `None` adds nothing to the SQL, which
38//! makes optional filters easy to build:
39//!
40//! ```rust
41//! # use drizzle_core::dialect::{Dialect, DialectTypes, SQLiteDialect as D};
42//! # use drizzle_core::{ColumnRef, SQL, SQLParam, expr::*};
43//! # #[derive(Clone, Debug)] struct Value(String);
44//! # impl SQLParam for Value { const DIALECT: Dialect = Dialect::SQLite; type DialectMarker = D; }
45//! # impl<X: ToString> From<X> for Value { fn from(v: X) -> Self { Value(v.to_string()) } }
46//! # impl From<Value> for std::borrow::Cow<'_, Value> { fn from(v: Value) -> Self { Self::Owned(v) } }
47//! # type C<X, N = NonNull> = &'static SQLExpr<'static, Value, X, N>;
48//! # fn col<X: drizzle_core::types::DataType, N: Nullability>(c: &'static str) -> C<X, N> { Box::leak(Box::new(SQLExpr::new(SQL::column(ColumnRef::sql("users", c))))) }
49//! # type Int = <D as DialectTypes>::Int; type Text = <D as DialectTypes>::Text; type Real = <D as DialectTypes>::Double;
50//! # struct Users { id: C<Int>, age: C<Int>, name: C<Text>, email: C<Text, Null>, score: C<Real, Null>, active: C<<D as DialectTypes>::Bool>, created_at: C<<D as DialectTypes>::Timestamp> }
51//! # let users = Users { id: col("id"), age: col("age"), name: col("name"), email: col("email"), score: col("score"), active: col("active"), created_at: col("created_at") };
52//! let name: Option<&str> = None;
53//! let filter = (gt(users.age, 18), name.map(|n| eq(users.name, n)));
54//! assert_eq!(filter.into_expr_sql().sql(), r#"("users"."age" > ?)"#);
55//! ```
56//!
57//! When *every* element is `None`, the list renders as the identity of its
58//! operator: `TRUE` for a conjunction (a tuple or [`all`]) and `FALSE` for a
59//! disjunction ([`any`]). An empty conjunction therefore filters nothing,
60//! while an empty disjunction matches no rows instead of silently matching
61//! every row.
62
63use crate::dialect::DialectTypes;
64use crate::sql::{SQL, Token};
65use crate::traits::SQLParam;
66use crate::types::BooleanLike;
67
68use super::{AggregateKind, Expr, Nullability, SQLExpr};
69
70/// Rendered SQL for a conjunction whose elements are all absent.
71const EMPTY_CONJUNCTION: &str = "TRUE";
72
73/// Rendered SQL for a disjunction whose elements are all absent.
74const EMPTY_DISJUNCTION: &str = "FALSE";
75
76mod sealed {
77 pub trait Sealed {}
78}
79
80// =============================================================================
81// ConditionSink
82// =============================================================================
83
84/// Collects rendered conditions while a [`ConditionList`] renders.
85/// Internal to the crate.
86#[doc(hidden)]
87#[derive(Debug)]
88pub struct ConditionSink<'a, V: SQLParam> {
89 sql: SQL<'a, V>,
90 separator: Token,
91 len: usize,
92}
93
94impl<'a, V: SQLParam + 'a> ConditionSink<'a, V> {
95 fn new(separator: Token) -> Self {
96 Self {
97 sql: SQL::empty(),
98 separator,
99 len: 0,
100 }
101 }
102
103 /// Append one rendered condition. `None` contributes nothing.
104 pub fn push(&mut self, condition: Option<SQL<'a, V>>) {
105 let Some(condition) = condition else { return };
106 if self.len > 0 {
107 self.sql.push_mut(self.separator);
108 }
109 self.sql.append_mut(condition);
110 self.len += 1;
111 }
112
113 fn finish(self, empty: &'static str) -> SQL<'a, V> {
114 if self.len == 0 {
115 SQL::raw(empty)
116 } else {
117 self.sql.parens()
118 }
119 }
120}
121
122// =============================================================================
123// ConditionList
124// =============================================================================
125
126/// A tuple of conditions that [`all`] and [`any`] can combine.
127///
128/// Implemented for tuples of 1 to 8 elements (16 with the `col16` feature,
129/// which is on by default). Each element is a boolean expression or an
130/// [`Option`] of one. The list is nullable if any element is nullable, and is
131/// an aggregate if any element is.
132///
133/// Tuples of up to 8 elements are also conditions themselves (they implement
134/// [`Expr`](super::Expr)). For longer lists, use [`all`] or [`any`], or nest
135/// tuples.
136///
137/// This trait is sealed.
138#[diagnostic::on_unimplemented(
139 message = "`{Self}` is not a list of SQL conditions",
140 label = "expected a tuple of boolean expressions",
141 note = "every element must be a boolean-typed expression, or an `Option` of one"
142)]
143pub trait ConditionList<'a, V: SQLParam>: sealed::Sealed + super::ExprSources {
144 /// Nullability folded across every element.
145 type Nullable: Nullability;
146
147 /// Aggregate kind folded across every element.
148 type Aggregate: AggregateKind;
149
150 /// Render every present element into `sink`, consuming the list.
151 #[doc(hidden)]
152 fn push_conditions(self, sink: &mut ConditionSink<'a, V>);
153
154 /// Render every present element into `sink`, borrowing the list.
155 #[doc(hidden)]
156 fn push_conditions_ref(&self, sink: &mut ConditionSink<'a, V>);
157}
158
159fn combine<'a, V, L>(conditions: L, separator: Token, empty: &'static str) -> SQL<'a, V>
160where
161 V: SQLParam + 'a,
162 L: ConditionList<'a, V>,
163{
164 let mut sink = ConditionSink::new(separator);
165 conditions.push_conditions(&mut sink);
166 sink.finish(empty)
167}
168
169fn combine_ref<'a, V, L>(conditions: &L, separator: Token, empty: &'static str) -> SQL<'a, V>
170where
171 V: SQLParam + 'a,
172 L: ConditionList<'a, V>,
173{
174 let mut sink = ConditionSink::new(separator);
175 conditions.push_conditions_ref(&mut sink);
176 sink.finish(empty)
177}
178
179// =============================================================================
180// all / any
181// =============================================================================
182
183/// Logical AND of every condition in a tuple.
184///
185/// Renders `(a AND b AND c)`. `None` elements are skipped, and a list with no
186/// present element renders as `TRUE`. The result is nullable if any element
187/// is, and is an aggregate if any element is.
188///
189/// A bare tuple means the same thing where a condition is expected. Use `all`
190/// where a tuple would be read as a list of columns instead (for example as a
191/// join's `ON` condition), or for lists longer than 8.
192///
193/// # Examples
194///
195/// ```rust
196/// # use drizzle_core::dialect::{Dialect, DialectTypes, SQLiteDialect as D};
197/// # use drizzle_core::{ColumnRef, SQL, SQLParam, expr::*};
198/// # #[derive(Clone, Debug)] struct Value(String);
199/// # impl SQLParam for Value { const DIALECT: Dialect = Dialect::SQLite; type DialectMarker = D; }
200/// # impl<X: ToString> From<X> for Value { fn from(v: X) -> Self { Value(v.to_string()) } }
201/// # impl From<Value> for std::borrow::Cow<'_, Value> { fn from(v: Value) -> Self { Self::Owned(v) } }
202/// # type C<X, N = NonNull> = &'static SQLExpr<'static, Value, X, N>;
203/// # fn col<X: drizzle_core::types::DataType, N: Nullability>(c: &'static str) -> C<X, N> { Box::leak(Box::new(SQLExpr::new(SQL::column(ColumnRef::sql("users", c))))) }
204/// # type Int = <D as DialectTypes>::Int; type Text = <D as DialectTypes>::Text; type Real = <D as DialectTypes>::Double;
205/// # struct Users { id: C<Int>, age: C<Int>, name: C<Text>, email: C<Text, Null>, score: C<Real, Null>, active: C<<D as DialectTypes>::Bool>, created_at: C<<D as DialectTypes>::Timestamp> }
206/// # let users = Users { id: col("id"), age: col("age"), name: col("name"), email: col("email"), score: col("score"), active: col("active"), created_at: col("created_at") };
207/// let cond = all((users.active, gt(users.age, 18)));
208/// assert_eq!(cond.sql(), r#"("users"."active" AND "users"."age" > ?)"#);
209///
210/// let nothing = all((None::<SQLExpr<'_, Value, Int>>,));
211/// assert_eq!(nothing.sql(), "TRUE");
212/// ```
213#[allow(clippy::type_complexity)]
214pub fn all<'a, V, L>(
215 conditions: L,
216) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, L::Nullable, L::Aggregate, L::Sources>
217where
218 V: SQLParam + 'a,
219 L: ConditionList<'a, V>,
220{
221 SQLExpr::new(combine(conditions, Token::AND, EMPTY_CONJUNCTION))
222}
223
224/// Logical OR of every condition in a tuple.
225///
226/// Renders `(a OR b OR c)`. `None` elements are skipped, and a list with no
227/// present element renders as `FALSE`, so an empty OR matches no rows. The
228/// result is nullable if any element is, and is an aggregate if any element
229/// is.
230///
231/// # Examples
232///
233/// ```rust
234/// # use drizzle_core::dialect::{Dialect, DialectTypes, SQLiteDialect as D};
235/// # use drizzle_core::{ColumnRef, SQL, SQLParam, expr::*};
236/// # #[derive(Clone, Debug)] struct Value(String);
237/// # impl SQLParam for Value { const DIALECT: Dialect = Dialect::SQLite; type DialectMarker = D; }
238/// # impl<X: ToString> From<X> for Value { fn from(v: X) -> Self { Value(v.to_string()) } }
239/// # impl From<Value> for std::borrow::Cow<'_, Value> { fn from(v: Value) -> Self { Self::Owned(v) } }
240/// # type C<X, N = NonNull> = &'static SQLExpr<'static, Value, X, N>;
241/// # fn col<X: drizzle_core::types::DataType, N: Nullability>(c: &'static str) -> C<X, N> { Box::leak(Box::new(SQLExpr::new(SQL::column(ColumnRef::sql("users", c))))) }
242/// # type Int = <D as DialectTypes>::Int; type Text = <D as DialectTypes>::Text; type Real = <D as DialectTypes>::Double;
243/// # struct Users { id: C<Int>, age: C<Int>, name: C<Text>, email: C<Text, Null>, score: C<Real, Null>, active: C<<D as DialectTypes>::Bool>, created_at: C<<D as DialectTypes>::Timestamp> }
244/// # let users = Users { id: col("id"), age: col("age"), name: col("name"), email: col("email"), score: col("score"), active: col("active"), created_at: col("created_at") };
245/// let cond = any((eq(users.name, "admin"), lt(users.age, 13)));
246/// assert_eq!(cond.sql(), r#"("users"."name" = ? OR "users"."age" < ?)"#);
247/// ```
248#[allow(clippy::type_complexity)]
249pub fn any<'a, V, L>(
250 conditions: L,
251) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, L::Nullable, L::Aggregate, L::Sources>
252where
253 V: SQLParam + 'a,
254 L: ConditionList<'a, V>,
255{
256 SQLExpr::new(combine(conditions, Token::OR, EMPTY_DISJUNCTION))
257}
258
259// =============================================================================
260// Tuple implementations
261// =============================================================================
262
263/// `ConditionList` for a 1-tuple: the markers are the single element's.
264macro_rules! impl_condition_one {
265 ($T:ident, $i:tt) => {
266 impl<'a, V, $T> ConditionList<'a, V> for ($T,)
267 where
268 V: SQLParam + 'a,
269 $T: Expr<'a, V>,
270 <$T as Expr<'a, V>>::SQLType: BooleanLike,
271 {
272 type Nullable = <$T as Expr<'a, V>>::Nullable;
273 type Aggregate = <$T as Expr<'a, V>>::Aggregate;
274
275 fn push_conditions(self, sink: &mut ConditionSink<'a, V>) {
276 sink.push(self.$i.into_condition_sql());
277 }
278
279 fn push_conditions_ref(&self, sink: &mut ConditionSink<'a, V>) {
280 sink.push(self.$i.to_condition_sql());
281 }
282 }
283 };
284}
285
286/// `ConditionList` for an N-tuple: delegate the marker fold to the
287/// (N-1)-tuple and combine it with the last element, mirroring how
288/// `RowColumnList` folds its column lists.
289macro_rules! impl_condition_many {
290 ([$($all:ident),+] [$($i:tt),+] [$($prev:ident),+] $last:ident) => {
291 impl<'a, V, $($all),+> ConditionList<'a, V> for ($($all,)+)
292 where
293 V: SQLParam + 'a,
294 $($all: Expr<'a, V>,)+
295 <$last as Expr<'a, V>>::SQLType: BooleanLike,
296 ($($prev,)+): ConditionList<'a, V>,
297 {
298 type Nullable = <<($($prev,)+) as ConditionList<'a, V>>::Nullable as Nullability>::Or<<$last as Expr<'a, V>>::Nullable>;
299 type Aggregate = <<($($prev,)+) as ConditionList<'a, V>>::Aggregate as AggregateKind>::Or<<$last as Expr<'a, V>>::Aggregate>;
300
301 fn push_conditions(self, sink: &mut ConditionSink<'a, V>) {
302 $( sink.push(self.$i.into_condition_sql()); )+
303 }
304
305 fn push_conditions_ref(&self, sink: &mut ConditionSink<'a, V>) {
306 $( sink.push(self.$i.to_condition_sql()); )+
307 }
308 }
309 };
310}
311
312/// Recursive accumulator splitting the last element off the type list while
313/// carrying the full type and index lists through to the impl.
314macro_rules! impl_condition_split {
315 ([$A:ident] [$i:tt] [] $only:ident) => {
316 impl_condition_one!($A, $i);
317 };
318 ([$($all:ident),+] [$($i:tt),+] [$($prev:ident),+] $last:ident) => {
319 impl_condition_many!([$($all),+] [$($i),+] [$($prev),+] $last);
320 };
321 ([$($all:ident),+] [$($i:tt),+] [] $head:ident, $($rest:ident),+) => {
322 impl_condition_split!([$($all),+] [$($i),+] [$head] $($rest),+);
323 };
324 ([$($all:ident),+] [$($i:tt),+] [$($prev:ident),+] $head:ident, $($rest:ident),+) => {
325 impl_condition_split!([$($all),+] [$($i),+] [$($prev),+, $head] $($rest),+);
326 };
327}
328
329/// `Expr` for a tuple of conditions: the tuple *is* the conjunction.
330///
331/// The tuple's `ToSQL` impl still renders a comma-separated list — that is what
332/// a SELECT or GROUP BY list needs — so the conjunction is produced by the
333/// expression-rendering hooks, which every condition site goes through.
334macro_rules! impl_condition_expr {
335 ($($T:ident),+) => {
336 impl<'a, V, $($T),+> Expr<'a, V> for ($($T,)+)
337 where
338 V: SQLParam + 'a,
339 Self: ConditionList<'a, V> + crate::traits::ToSQL<'a, V>,
340 {
341 type SQLType = crate::types::Conjunction;
342 type Nullable = <Self as ConditionList<'a, V>>::Nullable;
343 type Aggregate = <Self as ConditionList<'a, V>>::Aggregate;
344
345 fn to_expr_sql(&self) -> SQL<'a, V> {
346 combine_ref(self, Token::AND, EMPTY_CONJUNCTION)
347 }
348
349 fn into_expr_sql(self) -> SQL<'a, V> {
350 combine(self, Token::AND, EMPTY_CONJUNCTION)
351 }
352 }
353 };
354}
355
356/// Callback for `with_col_sizes_*!`: seals the tuple and generates its
357/// `ConditionList` impl.
358macro_rules! impl_condition_tuple {
359 ($($T:ident),+; $($i:tt),+) => {
360 impl<$($T),+> sealed::Sealed for ($($T,)+) {}
361 impl_condition_split!([$($T),+] [$($i),+] [] $($T),+);
362 };
363}
364
365/// Callback for `with_col_sizes_8!`: makes a tuple usable as an expression.
366macro_rules! impl_condition_tuple_expr {
367 ($($T:ident),+; $($i:tt),+) => {
368 impl_condition_expr!($($T),+);
369 };
370}
371
372with_col_sizes_8!(impl_condition_tuple);
373
374// Only the first ladder rung gets an `Expr` impl. `Expr` sits at the centre of
375// the trait graph — every literal, reference, `Option`, and column competes as
376// a candidate — and adding tuple candidates past arity 8 makes trait selection
377// blow past any usable memory budget while rustc well-formedness-checks the
378// nested marker fold. Longer lists still combine through `all`/`any`, which
379// need only `ConditionList`, or by nesting tuples.
380with_col_sizes_8!(impl_condition_tuple_expr);
381
382// The ladder stops at 16 even when `col32` and above are enabled. The marker
383// fold nests one projection per rung, and past 16 rungs rustc exhausts memory
384// well-formedness-checking the impls. Column lists need the higher rungs
385// because tables get wide; condition lists do not — `all`/`any` and nested
386// tuples cover anything longer, and produce the same flat AND.
387#[cfg(any(
388 feature = "col16",
389 feature = "col32",
390 feature = "col64",
391 feature = "col128",
392 feature = "col200"
393))]
394with_col_sizes_16!(impl_condition_tuple);