Skip to main content

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