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}