Skip to main content

chain_builder/
where_.rs

1//! WHERE-clause predicate model and the nested-group accumulator.
2//!
3//! [`Predicate`] is the dialect-agnostic AST for a single WHERE condition.
4//! [`WhereBuilder`] is a thin accumulator used by `and_where`/`or_where` to
5//! collect a nested group of predicates without threading the whole
6//! [`QueryBuilder`](crate::QueryBuilder) into the closure.
7
8use core::marker::PhantomData;
9
10use crate::builder::QueryBuilder;
11use crate::dialect::Dialect;
12use crate::value::{IntoBind, Value};
13
14/// How a [`Predicate::Group`] attaches to the clause that precedes it.
15///
16/// This is the *outer* conjunction (the separator written before the group:
17/// `AND (...)` vs `OR (...)`). It does NOT control how predicates *inside* the
18/// group are joined — in M1 inner group predicates are ALWAYS joined by `AND`
19/// (see [`WhereBuilder`] and `compile::write_pred`).
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Conj {
22    /// Attach the group with `AND (...)`.
23    And,
24    /// Attach the group with `OR (...)`.
25    Or,
26}
27
28/// A single WHERE-clause predicate.
29///
30/// Generic over the [`Dialect`] marker `D` because subquery-bearing variants
31/// ([`Predicate::Exists`] / [`Predicate::InSubquery`]) embed a
32/// [`QueryBuilder<D>`]; the recursion is broken with a `Box`.
33#[derive(Debug, Clone, PartialEq)]
34pub enum Predicate<D: Dialect> {
35    /// `col op value`, e.g. `"age" > $1`.
36    Binary {
37        /// Raw identifier; escaped in compile.rs.
38        col: String,
39        /// SQL operator token (`=`, `!=`, `LIKE`, …).
40        op: &'static str,
41        /// Bound value.
42        val: Value,
43    },
44    /// `col [NOT ]IN (...)`.
45    In {
46        /// Raw identifier; escaped in compile.rs.
47        col: String,
48        /// Whether this is a `NOT IN`.
49        neg: bool,
50        /// Bound values; empty yields a constant true/false predicate.
51        vals: Vec<Value>,
52    },
53    /// `col IS [NOT ]NULL`.
54    Null {
55        /// Raw identifier; escaped in compile.rs.
56        col: String,
57        /// Whether this is `IS NOT NULL`.
58        neg: bool,
59    },
60    /// `col BETWEEN lo AND hi`.
61    Between {
62        /// Raw identifier; escaped in compile.rs.
63        col: String,
64        /// Lower bound.
65        lo: Value,
66        /// Upper bound.
67        hi: Value,
68    },
69    /// `col ILIKE val` — dialect-aware case-insensitive match.
70    ///
71    /// Postgres emits native `{col} ILIKE {ph}`; MySQL/SQLite emit
72    /// `LOWER({col}) LIKE LOWER({ph})`. Dispatched in `compile::write_pred`.
73    ILike {
74        /// Raw identifier; escaped in compile.rs.
75        col: String,
76        /// Bound value.
77        val: Value,
78    },
79    /// `col @> val` — JSONB containment (Postgres-oriented; `@>` emitted
80    /// verbatim for all dialects).
81    JsonContains {
82        /// Raw identifier; escaped in compile.rs.
83        col: String,
84        /// Bound value (JSON text or `Value::Json`).
85        val: Value,
86    },
87    /// Raw SQL fragment with its own binds, emitted verbatim.
88    Raw {
89        /// Verbatim SQL.
90        sql: String,
91        /// Bound values appended in order.
92        binds: Vec<Value>,
93    },
94    /// A parenthesized group of predicates.
95    ///
96    /// `outer_conj` controls how the group attaches to the preceding clause
97    /// (`AND (...)` vs `OR (...)`). Inner predicates are joined with `AND`,
98    /// except that a nested `Group` carries its own `outer_conj` (so `or_where`
99    /// inside a group emits ` OR (...)`) — groups nest arbitrarily.
100    Group {
101        /// How this group attaches to the preceding clause (outer separator).
102        outer_conj: Conj,
103        /// Inner predicates; rendered like the top level, so nested groups with
104        /// `outer_conj: Or` introduce `OR` within this group.
105        preds: Vec<Predicate<D>>,
106    },
107    /// `lhs op rhs` — both sides are column identifiers (escaped at compile
108    /// time), no bind. Backs `where_column`.
109    Column {
110        /// Raw left identifier; escaped in compile.rs.
111        lhs: String,
112        /// SQL operator token (`=`, `!=`, …).
113        op: &'static str,
114        /// Raw right identifier; escaped in compile.rs.
115        rhs: String,
116    },
117    /// `[NOT ]EXISTS (subquery)`.
118    Exists {
119        /// Whether this is `NOT EXISTS`.
120        neg: bool,
121        /// The embedded sub-query, compiled with placeholder continuity.
122        sub: Box<QueryBuilder<D>>,
123    },
124    /// `col [NOT ]IN (subquery)`.
125    InSubquery {
126        /// Raw identifier; escaped in compile.rs.
127        col: String,
128        /// Whether this is `NOT IN`.
129        neg: bool,
130        /// The embedded sub-query, compiled with placeholder continuity.
131        sub: Box<QueryBuilder<D>>,
132    },
133}
134
135/// Accumulator passed to `and_where`/`or_where` closures.
136///
137/// It owns its own `Vec<Predicate>`; the closure consumes it and returns it,
138/// and the caller wraps the collected predicates into a [`Predicate::Group`].
139/// The `PhantomData<D>` keeps the dialect in scope without threading the full
140/// builder through the closure.
141///
142/// Siblings added directly are `AND`-joined; call `and_where`/`or_where` inside
143/// the closure to nest a further parenthesized group (and `or_where` introduces
144/// `OR` within the group). Nesting is arbitrary-depth.
145pub struct WhereBuilder<D: Dialect> {
146    preds: Vec<Predicate<D>>,
147    _marker: PhantomData<D>,
148}
149
150impl<D: Dialect> Default for WhereBuilder<D> {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl<D: Dialect> WhereBuilder<D> {
157    /// Create an empty accumulator.
158    pub fn new() -> Self {
159        Self {
160            preds: Vec::new(),
161            _marker: PhantomData,
162        }
163    }
164
165    /// Consume the accumulator and return the collected predicates.
166    pub(crate) fn into_preds(self) -> Vec<Predicate<D>> {
167        self.preds
168    }
169
170    fn group(
171        mut self,
172        outer_conj: Conj,
173        f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>,
174    ) -> Self {
175        let preds = f(WhereBuilder::new()).into_preds();
176        self.preds.push(Predicate::Group { outer_conj, preds });
177        self
178    }
179
180    /// Add a nested parenthesized `AND (...)` group built by the closure.
181    pub fn and_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
182        self.group(Conj::And, f)
183    }
184
185    /// Add a nested parenthesized `OR (...)` group built by the closure.
186    pub fn or_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
187        self.group(Conj::Or, f)
188    }
189
190    fn binary(mut self, col: &str, op: &'static str, val: impl IntoBind) -> Self {
191        self.preds.push(Predicate::Binary {
192            col: col.to_owned(),
193            op,
194            val: val.into_bind(),
195        });
196        self
197    }
198
199    /// `col = val`.
200    pub fn where_eq(self, col: &str, val: impl IntoBind) -> Self {
201        self.binary(col, "=", val)
202    }
203
204    /// `col != val`.
205    pub fn where_ne(self, col: &str, val: impl IntoBind) -> Self {
206        self.binary(col, "!=", val)
207    }
208
209    /// `col > val`.
210    pub fn where_gt(self, col: &str, val: impl IntoBind) -> Self {
211        self.binary(col, ">", val)
212    }
213
214    /// `col >= val`.
215    pub fn where_gte(self, col: &str, val: impl IntoBind) -> Self {
216        self.binary(col, ">=", val)
217    }
218
219    /// `col < val`.
220    pub fn where_lt(self, col: &str, val: impl IntoBind) -> Self {
221        self.binary(col, "<", val)
222    }
223
224    /// `col <= val`.
225    pub fn where_lte(self, col: &str, val: impl IntoBind) -> Self {
226        self.binary(col, "<=", val)
227    }
228
229    /// `col LIKE val`.
230    pub fn where_like(self, col: &str, val: impl IntoBind) -> Self {
231        self.binary(col, "LIKE", val)
232    }
233
234    /// `col ILIKE val` — dialect-aware case-insensitive match (see
235    /// [`QueryBuilder::where_ilike`](crate::QueryBuilder::where_ilike)).
236    pub fn where_ilike(mut self, col: &str, val: impl IntoBind) -> Self {
237        self.preds.push(Predicate::ILike {
238            col: col.to_owned(),
239            val: val.into_bind(),
240        });
241        self
242    }
243
244    /// `col @> val` — JSONB containment (Postgres-oriented; see
245    /// [`QueryBuilder::where_jsonb_contains`](crate::QueryBuilder::where_jsonb_contains)).
246    pub fn where_jsonb_contains(mut self, col: &str, val: impl IntoBind) -> Self {
247        self.preds.push(Predicate::JsonContains {
248            col: col.to_owned(),
249            val: val.into_bind(),
250        });
251        self
252    }
253
254    fn in_(mut self, col: &str, neg: bool, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
255        self.preds.push(Predicate::In {
256            col: col.to_owned(),
257            neg,
258            vals: vals.into_iter().map(IntoBind::into_bind).collect(),
259        });
260        self
261    }
262
263    /// `col IN (...)`.
264    pub fn where_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
265        self.in_(col, false, vals)
266    }
267
268    /// `col NOT IN (...)`.
269    pub fn where_not_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
270        self.in_(col, true, vals)
271    }
272
273    fn null(mut self, col: &str, neg: bool) -> Self {
274        self.preds.push(Predicate::Null {
275            col: col.to_owned(),
276            neg,
277        });
278        self
279    }
280
281    /// `col IS NULL`.
282    pub fn where_null(self, col: &str) -> Self {
283        self.null(col, false)
284    }
285
286    /// `col IS NOT NULL`.
287    pub fn where_not_null(self, col: &str) -> Self {
288        self.null(col, true)
289    }
290
291    /// `col BETWEEN lo AND hi`.
292    pub fn where_between(mut self, col: &str, lo: impl IntoBind, hi: impl IntoBind) -> Self {
293        self.preds.push(Predicate::Between {
294            col: col.to_owned(),
295            lo: lo.into_bind(),
296            hi: hi.into_bind(),
297        });
298        self
299    }
300
301    /// Raw SQL predicate with its own binds — the verbatim escape hatch.
302    ///
303    /// # Warning: positional placeholder contract
304    ///
305    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
306    /// `binds` are appended to the running bind list in order. For
307    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
308    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
309    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
310    /// produces a malformed query.
311    pub fn where_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
312        self.preds.push(Predicate::Raw {
313            sql: sql.to_owned(),
314            binds,
315        });
316        self
317    }
318
319    /// `lhs op rhs` — compare two column identifiers (both escaped at compile
320    /// time), no bind. See
321    /// [`QueryBuilder::where_column`](crate::QueryBuilder::where_column).
322    pub fn where_column(mut self, lhs: &str, op: &'static str, rhs: &str) -> Self {
323        self.preds.push(Predicate::Column {
324            lhs: lhs.to_owned(),
325            op,
326            rhs: rhs.to_owned(),
327        });
328        self
329    }
330}