Skip to main content

chain_builder/
builder.rs

1//! Typed, dialect-aware query builder.
2//!
3//! [`QueryBuilder`] is parameterized over a [`Dialect`] marker and uses
4//! by-value (`self`) chaining: every mutator takes and returns `Self`. The
5//! terminal [`QueryBuilder::to_sql`] compiles to `(sql, binds)`.
6
7use core::marker::PhantomData;
8
9use crate::compile::{compile, try_compile};
10use crate::dialect::Dialect;
11use crate::error::BuildError;
12use crate::value::{IntoBind, Value};
13use crate::where_::{Conj, Predicate, WhereBuilder};
14
15/// Sort direction for an `ORDER BY` column.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Order {
18    /// Ascending (`ASC`).
19    Asc,
20    /// Descending (`DESC`).
21    Desc,
22}
23
24/// The kind of SQL `JOIN`.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum JoinKind {
27    /// `INNER JOIN`.
28    Inner,
29    /// `LEFT JOIN`.
30    Left,
31    /// `RIGHT JOIN`.
32    Right,
33    /// `FULL OUTER JOIN`.
34    FullOuter,
35    /// `CROSS JOIN` (no `ON`).
36    Cross,
37}
38
39/// A single `ON` condition of a [`Join`].
40///
41/// Columns in `On`/`OnVal` are stored raw and escaped at compile time. `OnRaw`
42/// is the verbatim escape hatch (see [`JoinClause::on_raw`]).
43#[derive(Debug, Clone, PartialEq)]
44pub enum JoinCond {
45    /// `lhs op rhs` — both sides are columns (escaped at compile time).
46    On(String, &'static str, String),
47    /// `col op ?` — `col` escaped, the value is bound.
48    OnVal(String, &'static str, Value),
49    /// Verbatim SQL with its own binds.
50    OnRaw(String, Vec<Value>),
51}
52
53/// A `JOIN` clause: a kind, a target table, and zero or more `ON` conditions.
54#[derive(Debug, Clone, PartialEq)]
55pub struct Join {
56    /// The join kind (`INNER`, `LEFT`, …).
57    pub kind: JoinKind,
58    /// Raw target table identifier (escaped at compile time).
59    pub table: String,
60    /// `ON` conditions, joined by `AND`. Empty for `CROSS JOIN`.
61    pub on: Vec<JoinCond>,
62}
63
64/// A `HAVING` condition (SELECT-only, rendered after `GROUP BY`).
65#[derive(Debug, Clone, PartialEq)]
66pub enum Having {
67    /// `col op ?` — `col` is a real column/alias (escaped); value bound.
68    Col {
69        /// Raw column identifier (escaped at compile time).
70        col: String,
71        /// SQL operator token (`>`, `=`, …).
72        op: String,
73        /// Bound value.
74        val: Value,
75    },
76    /// Verbatim aggregate expression with its own binds (e.g. `COUNT(*) > ?`).
77    Raw {
78        /// Verbatim SQL.
79        sql: String,
80        /// Bound values appended in order.
81        binds: Vec<Value>,
82    },
83}
84
85/// A common table expression (`WITH` / `WITH RECURSIVE`).
86#[derive(Debug, Clone, PartialEq)]
87pub struct Cte<D: Dialect> {
88    /// Raw CTE name (escaped at compile time).
89    pub name: String,
90    /// Whether this CTE forces the single `WITH` to carry `RECURSIVE`.
91    pub recursive: bool,
92    /// The sub-query compiled into the CTE body.
93    pub query: QueryBuilder<D>,
94}
95
96/// Accumulator passed to `join`/`left_join`/… closures to build `ON` conditions.
97///
98/// The closure receives an empty `JoinClause`, chains `on`/`on_val`/`on_raw`
99/// calls, and returns it; the builder stores the collected conditions.
100pub struct JoinClause<D: Dialect> {
101    conds: Vec<JoinCond>,
102    _marker: PhantomData<D>,
103}
104
105impl<D: Dialect> Default for JoinClause<D> {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl<D: Dialect> JoinClause<D> {
112    /// Create an empty accumulator.
113    pub fn new() -> Self {
114        Self {
115            conds: Vec::new(),
116            _marker: PhantomData,
117        }
118    }
119
120    fn into_conds(self) -> Vec<JoinCond> {
121        self.conds
122    }
123
124    /// `lhs op rhs` — both sides are columns (each escaped at compile time).
125    pub fn on(mut self, col: &str, op: &'static str, col2: &str) -> Self {
126        self.conds
127            .push(JoinCond::On(col.to_owned(), op, col2.to_owned()));
128        self
129    }
130
131    /// `col op ?` — `col` escaped, the value bound as a placeholder.
132    pub fn on_val(mut self, col: &str, op: &'static str, val: impl IntoBind) -> Self {
133        self.conds
134            .push(JoinCond::OnVal(col.to_owned(), op, val.into_bind()));
135        self
136    }
137
138    /// Raw `ON` SQL fragment with its own binds — the verbatim escape hatch.
139    ///
140    /// # Warning: positional placeholder contract
141    ///
142    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
143    /// `binds` are appended to the running bind list in order. For
144    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
145    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
146    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
147    /// produces a malformed query.
148    pub fn on_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
149        self.conds.push(JoinCond::OnRaw(sql.to_owned(), binds));
150        self
151    }
152}
153
154/// What to do when an `INSERT` hits a conflict (see [`OnConflict`]).
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum ConflictAction {
157    /// Skip the conflicting row (`DO NOTHING` / `INSERT IGNORE`).
158    DoNothing,
159    /// Update the non-target inserted columns from the proposed row
160    /// (`DO UPDATE SET … = EXCLUDED.…` / `ON DUPLICATE KEY UPDATE …`).
161    Merge,
162}
163
164/// An `ON CONFLICT` specification attached to an `INSERT`.
165///
166/// `targets` are the raw conflict-target column identifiers (escaped at compile
167/// time). They are honored by Postgres / SQLite (`OnConflict` style) and
168/// **ignored** by MySQL (`OnDuplicateKey` style), which relies on its own
169/// unique/primary keys.
170#[derive(Debug, Clone, PartialEq)]
171pub struct OnConflict {
172    /// Raw conflict-target column identifiers.
173    pub targets: Vec<String>,
174    /// What to do on conflict.
175    pub action: ConflictAction,
176}
177
178/// A SQL aggregate function for the `select_*` aggregate helpers.
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum AggFn {
181    /// `COUNT(...)`.
182    Count,
183    /// `SUM(...)`.
184    Sum,
185    /// `AVG(...)`.
186    Avg,
187    /// `MIN(...)`.
188    Min,
189    /// `MAX(...)`.
190    Max,
191}
192
193impl AggFn {
194    /// The uppercase SQL keyword for this function.
195    pub fn as_str(&self) -> &'static str {
196        match self {
197            AggFn::Count => "COUNT",
198            AggFn::Sum => "SUM",
199            AggFn::Avg => "AVG",
200            AggFn::Min => "MIN",
201            AggFn::Max => "MAX",
202        }
203    }
204}
205
206/// A structured `SELECT`-list expression (aggregate or aliased column).
207///
208/// The `col`/`alias` identifiers are stored raw and escaped at compile time
209/// (a `*` column is emitted unescaped, e.g. `COUNT(*)`). Backs the `select_count`
210/// / `select_sum` / … and `select_as` helpers.
211#[derive(Debug, Clone, PartialEq)]
212pub enum SelectExpr {
213    /// `FUNC(col)` with an optional `AS alias` — e.g. `COUNT(*) AS "total"`.
214    Agg {
215        /// The aggregate function.
216        func: AggFn,
217        /// Raw column identifier (escaped at compile time; `*` passed through).
218        col: String,
219        /// Optional alias (escaped at compile time).
220        alias: Option<String>,
221    },
222    /// `col AS alias` — both identifiers escaped at compile time.
223    ColAs {
224        /// Raw column identifier (escaped at compile time).
225        col: String,
226        /// Alias (escaped at compile time).
227        alias: String,
228    },
229}
230
231/// A structured or raw `SET` expression for UPDATE. Backs
232/// [`QueryBuilder::set_raw`] / [`QueryBuilder::increment`] /
233/// [`QueryBuilder::decrement`]. Rendered after the (sorted) structured
234/// `update()` columns, in call order.
235#[derive(Debug, Clone, PartialEq)]
236pub(crate) enum SetExpr {
237    /// `{esc col} = {expr verbatim}` with its own binds (NOT escaped or
238    /// renumbered — the `where_raw` contract).
239    Raw {
240        /// Column identifier; escaped in compile.rs.
241        col: String,
242        /// Verbatim RHS expression.
243        expr: String,
244        /// Binds appended to the running bind list in order.
245        binds: Vec<Value>,
246    },
247    /// `{esc col} = {esc col} {+|-} {placeholder}` — structured step.
248    Step {
249        /// Column identifier; escaped in compile.rs.
250        col: String,
251        /// The step amount, bound via the normal placeholder machinery.
252        by: Value,
253        /// `false` → `+` (increment); `true` → `-` (decrement).
254        neg: bool,
255    },
256}
257
258/// The strength of a row-locking clause.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum LockStrength {
261    /// `FOR UPDATE` — exclusive lock.
262    Update,
263    /// `FOR SHARE` — shared lock.
264    Share,
265}
266
267/// The optional wait behavior of a row-locking clause.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum LockWait {
270    /// `SKIP LOCKED` — skip rows already locked.
271    SkipLocked,
272    /// `NOWAIT` — error immediately if a row is already locked.
273    NoWait,
274}
275
276/// A row-locking clause appended to a `SELECT` (`FOR UPDATE` / `FOR SHARE`,
277/// optionally `SKIP LOCKED` / `NOWAIT`).
278///
279/// Honored by Postgres / MySQL; a **silent no-op on SQLite** (see
280/// [`Dialect::supports_row_locking`](crate::Dialect::supports_row_locking)).
281/// Compiling panics if a lock is attached to a non-`SELECT` statement (a
282/// dangerous silent no-op otherwise) or combined with `UNION` on a locking
283/// dialect (invalid SQL).
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub struct Lock {
286    /// `FOR UPDATE` vs `FOR SHARE`.
287    pub strength: LockStrength,
288    /// Optional `SKIP LOCKED` / `NOWAIT` modifier.
289    pub wait: Option<LockWait>,
290}
291
292/// Which kind of statement is being built.
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
294pub enum Method {
295    /// `SELECT`.
296    #[default]
297    Select,
298    /// `INSERT`.
299    Insert,
300    /// `UPDATE`.
301    Update,
302    /// `DELETE`.
303    Delete,
304}
305
306/// Typed, dialect-aware SQL query builder.
307#[derive(Debug, Clone, PartialEq)]
308pub struct QueryBuilder<D: Dialect> {
309    pub(crate) table: String,
310    /// Optional database/schema qualifier (multi-tenant: one connection, many DBs).
311    /// When set, prefixes the main table and join tables: `"db"."table"`.
312    pub(crate) db: Option<String>,
313    pub(crate) select_cols: Vec<String>,
314    /// Structured `SELECT` expressions (aggregates / aliased columns), escaped at
315    /// compile time. Rendered after `select_cols`. Backs `select_count`/… /
316    /// `select_as`.
317    pub(crate) select_exprs: Vec<SelectExpr>,
318    /// Raw `SELECT` expressions (verbatim, NOT escaped) with their own binds,
319    /// appended after `select_cols`. Backs `select_raw`.
320    pub(crate) select_raw: Vec<(String, Vec<Value>)>,
321    /// Subquery `SELECT` columns: `(alias, sub)` → `(<sub>) AS {esc alias}`,
322    /// appended after `select_cols` / `select_raw`. Backs `select_subquery`.
323    pub(crate) select_subqueries: Vec<(String, Box<QueryBuilder<D>>)>,
324    /// `SELECT DISTINCT` flag (raw; off by default for M1 byte-identity).
325    pub(crate) distinct: bool,
326    /// `SELECT DISTINCT ON (cols)` columns (raw; Postgres-only).
327    pub(crate) distinct_on: Vec<String>,
328    pub(crate) wheres: Vec<Predicate<D>>,
329    pub(crate) method: Method,
330    pub(crate) set: Vec<(String, Value)>,
331    /// UPDATE `SET` expressions, rendered after the sorted `set` pairs in
332    /// call order. Backs `set_raw`/`increment`/`decrement`.
333    pub(crate) set_exprs: Vec<SetExpr>,
334    /// Multi-row `INSERT` rows (empty unless `insert_many` was used). Each row is
335    /// a `(column, value)` list; columns come from the first row's sorted keys.
336    pub(crate) insert_rows: Vec<Vec<(String, Value)>>,
337    pub(crate) joins: Vec<Join>,
338    pub(crate) groups: Vec<String>,
339    /// Raw `GROUP BY` fragment (verbatim) with its own binds, appended after any
340    /// structured `groups`.
341    pub(crate) group_by_raw: Option<(String, Vec<Value>)>,
342    pub(crate) havings: Vec<Having>,
343    pub(crate) orders: Vec<(String, Order)>,
344    /// Raw `ORDER BY` fragment (verbatim) with its own binds, appended after any
345    /// structured `orders`.
346    pub(crate) order_by_raw: Option<(String, Vec<Value>)>,
347    pub(crate) limit: Option<i64>,
348    pub(crate) offset: Option<i64>,
349    pub(crate) ctes: Vec<Cte<D>>,
350    pub(crate) unions: Vec<(bool, QueryBuilder<D>)>,
351    /// `ON CONFLICT` spec for `INSERT` (ignored on UPDATE/DELETE).
352    pub(crate) on_conflict: Option<OnConflict>,
353    /// `RETURNING` column list (raw; `"*"` emitted unescaped).
354    pub(crate) returning: Vec<String>,
355    /// Row-locking clause (`FOR UPDATE`/`FOR SHARE`); SELECT-only, no-op on SQLite.
356    pub(crate) lock: Option<Lock>,
357    /// First misuse detected by a builder method (e.g. a disallowed `having()`
358    /// operator). Deferred instead of panicking mid-chain; surfaced by
359    /// [`Self::try_to_sql`] as `Err` (and by [`Self::to_sql`] as a panic).
360    pub(crate) error: Option<BuildError>,
361    _marker: PhantomData<D>,
362}
363
364impl<D: Dialect> QueryBuilder<D> {
365    /// Start a query against `name`.
366    pub fn table(name: &str) -> Self {
367        Self {
368            table: name.to_owned(),
369            db: None,
370            select_cols: Vec::new(),
371            select_exprs: Vec::new(),
372            select_raw: Vec::new(),
373            select_subqueries: Vec::new(),
374            distinct: false,
375            distinct_on: Vec::new(),
376            wheres: Vec::new(),
377            method: Method::Select,
378            set: Vec::new(),
379            set_exprs: Vec::new(),
380            insert_rows: Vec::new(),
381            joins: Vec::new(),
382            groups: Vec::new(),
383            group_by_raw: None,
384            havings: Vec::new(),
385            orders: Vec::new(),
386            order_by_raw: None,
387            limit: None,
388            offset: None,
389            ctes: Vec::new(),
390            unions: Vec::new(),
391            on_conflict: None,
392            returning: Vec::new(),
393            lock: None,
394            error: None,
395            _marker: PhantomData,
396        }
397    }
398
399    /// Set the database/schema qualifier (multi-tenant: one connection, many DBs).
400    ///
401    /// The name prefixes the main table and every join table, escaped per dialect:
402    /// `QueryBuilder::<Postgres>::table("users").db("mydb")` →
403    /// `… FROM "mydb"."users"`. Matches 1.x `db()`.
404    pub fn db(mut self, name: &str) -> Self {
405        self.db = Some(name.to_owned());
406        self
407    }
408
409    /// Restrict the selected columns. An empty list selects `*`.
410    pub fn select<I, S>(mut self, cols: I) -> Self
411    where
412        I: IntoIterator<Item = S>,
413        S: AsRef<str>,
414    {
415        self.select_cols = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
416        self
417    }
418
419    /// Add a raw `SELECT` expression (verbatim, NOT escaped) with optional binds
420    /// — the escape hatch for aggregates/functions like `COUNT(*)`.
421    ///
422    /// Appended to the column list after any [`Self::select`] columns. Multiple
423    /// calls accumulate.
424    ///
425    /// # Warning: positional placeholder contract
426    ///
427    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and any
428    /// `binds` are appended to the running bind list in order. For **Postgres**,
429    /// the caller MUST write `$N` numbers matching the actual bind position. For
430    /// MySQL/SQLite use `?`.
431    pub fn select_raw(mut self, sql: &str, binds: Option<Vec<Value>>) -> Self {
432        self.select_raw
433            .push((sql.to_owned(), binds.unwrap_or_default()));
434        self
435    }
436
437    /// Add a subquery `SELECT` column: emits `(<sub>) AS {alias}` after the
438    /// regular columns and any [`Self::select_raw`] expressions.
439    ///
440    /// The subquery is compiled with placeholder continuity (its binds appear in
441    /// `$N` order at the point it is emitted — before the `WHERE` clause, since
442    /// the SELECT list is rendered first). SELECT-only.
443    pub fn select_subquery(mut self, alias: &str, sub: QueryBuilder<D>) -> Self {
444        self.select_subqueries
445            .push((alias.to_owned(), Box::new(sub)));
446        self
447    }
448
449    fn push_agg(mut self, func: AggFn, col: &str, alias: Option<&str>) -> Self {
450        self.select_exprs.push(SelectExpr::Agg {
451            func,
452            col: col.to_owned(),
453            alias: alias.map(|a| a.to_owned()),
454        });
455        self
456    }
457
458    /// Add `COUNT(col)` to the SELECT list (`col == "*"` → `COUNT(*)`).
459    pub fn select_count(self, col: &str) -> Self {
460        self.push_agg(AggFn::Count, col, None)
461    }
462
463    /// Add `COUNT(col) AS alias` (both identifiers escaped; `*` passed through).
464    pub fn select_count_as(self, col: &str, alias: &str) -> Self {
465        self.push_agg(AggFn::Count, col, Some(alias))
466    }
467
468    /// Add `SUM(col)` to the SELECT list.
469    pub fn select_sum(self, col: &str) -> Self {
470        self.push_agg(AggFn::Sum, col, None)
471    }
472
473    /// Add `SUM(col) AS alias`.
474    pub fn select_sum_as(self, col: &str, alias: &str) -> Self {
475        self.push_agg(AggFn::Sum, col, Some(alias))
476    }
477
478    /// Add `AVG(col)` to the SELECT list.
479    pub fn select_avg(self, col: &str) -> Self {
480        self.push_agg(AggFn::Avg, col, None)
481    }
482
483    /// Add `AVG(col) AS alias`.
484    pub fn select_avg_as(self, col: &str, alias: &str) -> Self {
485        self.push_agg(AggFn::Avg, col, Some(alias))
486    }
487
488    /// Add `MIN(col)` to the SELECT list.
489    pub fn select_min(self, col: &str) -> Self {
490        self.push_agg(AggFn::Min, col, None)
491    }
492
493    /// Add `MIN(col) AS alias`.
494    pub fn select_min_as(self, col: &str, alias: &str) -> Self {
495        self.push_agg(AggFn::Min, col, Some(alias))
496    }
497
498    /// Add `MAX(col)` to the SELECT list.
499    pub fn select_max(self, col: &str) -> Self {
500        self.push_agg(AggFn::Max, col, None)
501    }
502
503    /// Add `MAX(col) AS alias`.
504    pub fn select_max_as(self, col: &str, alias: &str) -> Self {
505        self.push_agg(AggFn::Max, col, Some(alias))
506    }
507
508    /// Add `col AS alias` to the SELECT list (both identifiers escaped).
509    pub fn select_as(mut self, col: &str, alias: &str) -> Self {
510        self.select_exprs.push(SelectExpr::ColAs {
511            col: col.to_owned(),
512            alias: alias.to_owned(),
513        });
514        self
515    }
516
517    /// Emit `SELECT DISTINCT …` (all dialects).
518    pub fn distinct(mut self) -> Self {
519        self.distinct = true;
520        self
521    }
522
523    /// Emit `SELECT DISTINCT ON (cols) …` — **Postgres only**.
524    ///
525    /// `cols` are raw identifiers (escaped at compile time). Compiling against a
526    /// dialect without `DISTINCT ON` support panics
527    /// (`DISTINCT ON requires PostgreSQL`).
528    pub fn distinct_on<I, S>(mut self, cols: I) -> Self
529    where
530        I: IntoIterator<Item = S>,
531        S: AsRef<str>,
532    {
533        self.distinct_on = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
534        self.distinct = true;
535        self
536    }
537
538    /// `col ILIKE val` — dialect-aware case-insensitive match.
539    ///
540    /// On **Postgres** this compiles to the native `{col} ILIKE {ph}`. On
541    /// MySQL/SQLite (no native `ILIKE`) it compiles to
542    /// `LOWER({col}) LIKE LOWER({ph})`.
543    pub fn where_ilike(mut self, col: &str, val: impl IntoBind) -> Self {
544        self.wheres.push(Predicate::ILike {
545            col: col.to_owned(),
546            val: val.into_bind(),
547        });
548        self
549    }
550
551    /// `col @> val` — JSONB containment.
552    ///
553    /// **Postgres-specific:** the `@>` operator is emitted verbatim for all
554    /// dialects, but is only meaningful on Postgres `jsonb` columns. `val` is
555    /// typically a JSON text string (or `Value::Json` behind the `json`
556    /// feature).
557    pub fn where_jsonb_contains(mut self, col: &str, val: impl IntoBind) -> Self {
558        self.wheres.push(Predicate::JsonContains {
559            col: col.to_owned(),
560            val: val.into_bind(),
561        });
562        self
563    }
564
565    fn binary(mut self, col: &str, op: &'static str, val: impl IntoBind) -> Self {
566        self.wheres.push(Predicate::Binary {
567            col: col.to_owned(),
568            op,
569            val: val.into_bind(),
570        });
571        self
572    }
573
574    /// `col = val`.
575    pub fn where_eq(self, col: &str, val: impl IntoBind) -> Self {
576        self.binary(col, "=", val)
577    }
578
579    /// `col != val`.
580    pub fn where_ne(self, col: &str, val: impl IntoBind) -> Self {
581        self.binary(col, "!=", val)
582    }
583
584    /// `col > val`.
585    pub fn where_gt(self, col: &str, val: impl IntoBind) -> Self {
586        self.binary(col, ">", val)
587    }
588
589    /// `col >= val`.
590    pub fn where_gte(self, col: &str, val: impl IntoBind) -> Self {
591        self.binary(col, ">=", val)
592    }
593
594    /// `col < val`.
595    pub fn where_lt(self, col: &str, val: impl IntoBind) -> Self {
596        self.binary(col, "<", val)
597    }
598
599    /// `col <= val`.
600    pub fn where_lte(self, col: &str, val: impl IntoBind) -> Self {
601        self.binary(col, "<=", val)
602    }
603
604    /// `col LIKE val`.
605    pub fn where_like(self, col: &str, val: impl IntoBind) -> Self {
606        self.binary(col, "LIKE", val)
607    }
608
609    fn in_(mut self, col: &str, neg: bool, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
610        self.wheres.push(Predicate::In {
611            col: col.to_owned(),
612            neg,
613            vals: vals.into_iter().map(IntoBind::into_bind).collect(),
614        });
615        self
616    }
617
618    /// `col IN (...)`.
619    pub fn where_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
620        self.in_(col, false, vals)
621    }
622
623    /// `col NOT IN (...)`.
624    pub fn where_not_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
625        self.in_(col, true, vals)
626    }
627
628    fn null(mut self, col: &str, neg: bool) -> Self {
629        self.wheres.push(Predicate::Null {
630            col: col.to_owned(),
631            neg,
632        });
633        self
634    }
635
636    /// `col IS NULL`.
637    pub fn where_null(self, col: &str) -> Self {
638        self.null(col, false)
639    }
640
641    /// `col IS NOT NULL`.
642    pub fn where_not_null(self, col: &str) -> Self {
643        self.null(col, true)
644    }
645
646    /// `col BETWEEN lo AND hi`.
647    pub fn where_between(mut self, col: &str, lo: impl IntoBind, hi: impl IntoBind) -> Self {
648        self.wheres.push(Predicate::Between {
649            col: col.to_owned(),
650            lo: lo.into_bind(),
651            hi: hi.into_bind(),
652        });
653        self
654    }
655
656    /// Raw SQL predicate with its own binds — the verbatim escape hatch.
657    ///
658    /// # Warning: positional placeholder contract
659    ///
660    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
661    /// `binds` are appended to the running bind list in order. For
662    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
663    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
664    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
665    /// produces a malformed query.
666    pub fn where_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
667        self.wheres.push(Predicate::Raw {
668            sql: sql.to_owned(),
669            binds,
670        });
671        self
672    }
673
674    /// `lhs op rhs` — compare two column identifiers (both escaped at compile
675    /// time), no bind. e.g. `where_column("orders.user_id", "=", "users.id")`.
676    pub fn where_column(mut self, lhs: &str, op: &'static str, rhs: &str) -> Self {
677        self.wheres.push(Predicate::Column {
678            lhs: lhs.to_owned(),
679            op,
680            rhs: rhs.to_owned(),
681        });
682        self
683    }
684
685    /// `EXISTS (subquery)` — takes an already-built sub-builder by value
686    /// (mirrors [`Self::union`] / [`Self::with`]). The sub-query is compiled
687    /// with placeholder continuity.
688    pub fn where_exists(mut self, sub: QueryBuilder<D>) -> Self {
689        self.wheres.push(Predicate::Exists {
690            neg: false,
691            sub: Box::new(sub),
692        });
693        self
694    }
695
696    /// `NOT EXISTS (subquery)`. See [`Self::where_exists`].
697    pub fn where_not_exists(mut self, sub: QueryBuilder<D>) -> Self {
698        self.wheres.push(Predicate::Exists {
699            neg: true,
700            sub: Box::new(sub),
701        });
702        self
703    }
704
705    /// `col IN (subquery)` — takes an already-built sub-builder by value. The
706    /// sub-query is compiled with placeholder continuity.
707    pub fn where_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
708        self.wheres.push(Predicate::InSubquery {
709            col: col.to_owned(),
710            neg: false,
711            sub: Box::new(sub),
712        });
713        self
714    }
715
716    /// `col NOT IN (subquery)`. See [`Self::where_in_subquery`].
717    pub fn where_not_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
718        self.wheres.push(Predicate::InSubquery {
719            col: col.to_owned(),
720            neg: true,
721            sub: Box::new(sub),
722        });
723        self
724    }
725
726    fn group(
727        mut self,
728        outer_conj: Conj,
729        f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>,
730    ) -> Self {
731        let preds = f(WhereBuilder::new()).into_preds();
732        self.wheres.push(Predicate::Group { outer_conj, preds });
733        self
734    }
735
736    /// Add a parenthesized `AND (...)` group built by the closure.
737    pub fn and_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
738        self.group(Conj::And, f)
739    }
740
741    /// Add a parenthesized `OR (...)` group built by the closure.
742    pub fn or_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
743        self.group(Conj::Or, f)
744    }
745
746    /// Build an `INSERT` from a single row of `(column, value)` pairs.
747    pub fn insert<K, V, I>(mut self, row: I) -> Self
748    where
749        K: AsRef<str>,
750        V: IntoBind,
751        I: IntoIterator<Item = (K, V)>,
752    {
753        self.method = Method::Insert;
754        self.set = row
755            .into_iter()
756            .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
757            .collect();
758        self
759    }
760
761    /// Build a multi-row `INSERT` from an iterator of rows, each a sequence of
762    /// `(column, value)` pairs.
763    ///
764    /// The inserted column set is taken from the **first** row's keys (sorted, as
765    /// with [`Self::insert`]). For each subsequent row, a value is bound for every
766    /// column in that set; a key **missing** in a later row binds `Value::Null`
767    /// rather than panicking (DoS-safe, matching the 1.x hardening). Composes with
768    /// `on_conflict_*` and `returning`.
769    pub fn insert_many<K, V, R, Rows>(mut self, rows: Rows) -> Self
770    where
771        K: AsRef<str>,
772        V: IntoBind,
773        R: IntoIterator<Item = (K, V)>,
774        Rows: IntoIterator<Item = R>,
775    {
776        self.method = Method::Insert;
777        self.insert_rows = rows
778            .into_iter()
779            .map(|row| {
780                row.into_iter()
781                    .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
782                    .collect()
783            })
784            .collect();
785        self
786    }
787
788    /// Build an `UPDATE` from `(column, value)` pairs. WHERE still applies.
789    pub fn update<K, V, I>(mut self, set: I) -> Self
790    where
791        K: AsRef<str>,
792        V: IntoBind,
793        I: IntoIterator<Item = (K, V)>,
794    {
795        self.method = Method::Update;
796        self.set = set
797            .into_iter()
798            .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
799            .collect();
800        self
801    }
802
803    /// Set a column to a **verbatim** SQL expression — the UPDATE escape
804    /// hatch. Switches the builder to UPDATE, like [`Self::update`] (and
805    /// like every method-selecting call, last one wins: switching away from
806    /// UPDATE afterwards leaves the expressions ignored).
807    ///
808    /// `col` is identifier-escaped; `expr` is emitted verbatim with `binds`
809    /// appended. Expressions render after the (sorted) structured
810    /// [`Self::update`] columns, in call order. Duplicate target columns are
811    /// not detected — the database reports them.
812    ///
813    /// # Warning: positional placeholder contract
814    ///
815    /// `expr` is NOT escaped or renumbered. For **Postgres**, hand-write
816    /// `$N` matching the actual bind position at the point this expression
817    /// is reached — `SET` precedes `WHERE`, so that is `(number of
818    /// structured SET binds) + (binds of all earlier expressions, counting
819    /// 1 per preceding `increment`/`decrement`) + 1`, `+2`, … For
820    /// MySQL/SQLite use `?`. A wrong `$N` produces a malformed query.
821    ///
822    /// ```
823    /// use chain_builder::{Postgres, QueryBuilder, Value};
824    /// let (sql, binds) = QueryBuilder::<Postgres>::table("t")
825    ///     .update([("name", "x")])
826    ///     .set_raw("updated_at", "NOW()", vec![])
827    ///     .to_sql();
828    /// assert_eq!(sql, r#"UPDATE "t" SET "name" = $1, "updated_at" = NOW()"#);
829    /// assert_eq!(binds, vec![Value::Text("x".into())]);
830    /// ```
831    pub fn set_raw(mut self, col: &str, expr: &str, binds: Vec<Value>) -> Self {
832        self.method = Method::Update;
833        self.set_exprs.push(SetExpr::Raw {
834            col: col.to_owned(),
835            expr: expr.to_owned(),
836            binds,
837        });
838        self
839    }
840
841    /// `col = col + value` — atomic counter increment. Structured (column
842    /// escaped, value bound); switches the builder to UPDATE, so
843    /// `qb.increment("views", 1)` alone is a valid UPDATE.
844    ///
845    /// ```
846    /// use chain_builder::{Postgres, QueryBuilder, Value};
847    /// let (sql, binds) = QueryBuilder::<Postgres>::table("t")
848    ///     .increment("views", 1i64)
849    ///     .to_sql();
850    /// assert_eq!(sql, r#"UPDATE "t" SET "views" = "views" + $1"#);
851    /// assert_eq!(binds, vec![Value::I64(1)]);
852    /// ```
853    pub fn increment(mut self, col: &str, by: impl IntoBind) -> Self {
854        self.method = Method::Update;
855        self.set_exprs.push(SetExpr::Step {
856            col: col.to_owned(),
857            by: by.into_bind(),
858            neg: false,
859        });
860        self
861    }
862
863    /// `col = col - value` — atomic counter decrement. See
864    /// [`Self::increment`].
865    pub fn decrement(mut self, col: &str, by: impl IntoBind) -> Self {
866        self.method = Method::Update;
867        self.set_exprs.push(SetExpr::Step {
868            col: col.to_owned(),
869            by: by.into_bind(),
870            neg: true,
871        });
872        self
873    }
874
875    /// Build a `DELETE`. WHERE still applies.
876    pub fn delete(mut self) -> Self {
877        self.method = Method::Delete;
878        self
879    }
880
881    /// On conflict, skip the row (`INSERT`-only; ignored on UPDATE/DELETE).
882    ///
883    /// `targets` are the conflict-target columns (may be empty).
884    ///
885    /// - **Postgres / SQLite:** emits `ON CONFLICT ({targets}) DO NOTHING`, or
886    ///   bare `ON CONFLICT DO NOTHING` when `targets` is empty.
887    /// - **MySQL:** emits `INSERT IGNORE INTO …` (no trailing clause). Note that
888    ///   `IGNORE` suppresses *more* than duplicate-key errors (also truncation
889    ///   and bad-value coercion) — broader than pg/sqlite `DO NOTHING`.
890    pub fn on_conflict_do_nothing<I, S>(mut self, targets: I) -> Self
891    where
892        I: IntoIterator<Item = S>,
893        S: AsRef<str>,
894    {
895        self.on_conflict = Some(OnConflict {
896            targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
897            action: ConflictAction::DoNothing,
898        });
899        self
900    }
901
902    /// On conflict, update the non-target inserted columns from the proposed row
903    /// (`INSERT`-only; ignored on UPDATE/DELETE).
904    ///
905    /// - **Postgres / SQLite:** emits
906    ///   `ON CONFLICT ({targets}) DO UPDATE SET {c} = EXCLUDED.{c}, …` for every
907    ///   inserted column *except* the conflict targets. If `targets` is empty or
908    ///   covers all inserted columns (empty SET list), falls back to the
909    ///   `DO NOTHING` rendering (pg/sqlite require a target for `DO UPDATE`).
910    /// - **MySQL:** the explicit `targets` are **ignored** (MySQL uses its own
911    ///   unique/primary keys); emits
912    ///   `ON DUPLICATE KEY UPDATE {c} = VALUES({c}), …` for *all* inserted
913    ///   columns. `VALUES()` is used for MySQL 5.7/8.x compatibility. Including a
914    ///   PK column in the insert set yields a redundant-but-harmless
915    ///   `pk = VALUES(pk)`.
916    pub fn on_conflict_merge<I, S>(mut self, targets: I) -> Self
917    where
918        I: IntoIterator<Item = S>,
919        S: AsRef<str>,
920    {
921        self.on_conflict = Some(OnConflict {
922            targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
923            action: ConflictAction::Merge,
924        });
925        self
926    }
927
928    /// Add a `RETURNING` column list. Works on INSERT / UPDATE / DELETE for
929    /// Postgres and SQLite; a `"*"` column is emitted unescaped (`RETURNING *`).
930    ///
931    /// On **MySQL** this is a silent no-op (MySQL has no `RETURNING`). On
932    /// **SQLite** `RETURNING` requires SQLite ≥ 3.35.0 (2021); `supports_returning()`
933    /// is a compile-time dialect flag, not a runtime version check.
934    pub fn returning<I, S>(mut self, cols: I) -> Self
935    where
936        I: IntoIterator<Item = S>,
937        S: AsRef<str>,
938    {
939        self.returning = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
940        self
941    }
942
943    /// Add `GROUP BY` columns (raw owned identifiers, escaped at compile time).
944    ///
945    /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
946    pub fn group_by<I, S>(mut self, cols: I) -> Self
947    where
948        I: IntoIterator<Item = S>,
949        S: AsRef<str>,
950    {
951        self.groups
952            .extend(cols.into_iter().map(|c| c.as_ref().to_owned()));
953        self
954    }
955
956    /// Add a raw `GROUP BY` fragment with its own binds — the verbatim escape
957    /// hatch. SELECT-only.
958    ///
959    /// The fragment is appended after any structured [`Self::group_by`] columns
960    /// within the same `GROUP BY` clause (e.g. `GROUP BY "a", <raw>`); if no
961    /// structured columns are present it becomes the whole `GROUP BY <raw>`.
962    ///
963    /// # Warning: positional placeholder contract
964    ///
965    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
966    /// `binds` are appended to the running bind list in order. For
967    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
968    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
969    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
970    /// produces a malformed query.
971    pub fn group_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
972        self.group_by_raw = Some((sql.to_owned(), binds));
973        self
974    }
975
976    /// Add a raw `ORDER BY` fragment with its own binds — the verbatim escape
977    /// hatch. SELECT-only.
978    ///
979    /// The fragment is appended after any structured [`Self::order_by`] terms
980    /// within the same `ORDER BY` clause (e.g. `ORDER BY "a" ASC, <raw>`); if no
981    /// structured terms are present it becomes the whole `ORDER BY <raw>`.
982    ///
983    /// # Warning: positional placeholder contract
984    ///
985    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
986    /// `binds` are appended to the running bind list in order. For
987    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
988    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
989    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
990    /// produces a malformed query.
991    pub fn order_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
992        self.order_by_raw = Some((sql.to_owned(), binds));
993        self
994    }
995
996    /// Add an `ORDER BY col <ord>` term. SELECT-only.
997    pub fn order_by(mut self, col: &str, ord: Order) -> Self {
998        self.orders.push((col.to_owned(), ord));
999        self
1000    }
1001
1002    /// Add an `ORDER BY col ASC` term. SELECT-only.
1003    pub fn order_by_asc(self, col: &str) -> Self {
1004        self.order_by(col, Order::Asc)
1005    }
1006
1007    /// Add an `ORDER BY col DESC` term. SELECT-only.
1008    pub fn order_by_desc(self, col: &str) -> Self {
1009        self.order_by(col, Order::Desc)
1010    }
1011
1012    /// Set `LIMIT n` (bound as a placeholder). SELECT-only.
1013    pub fn limit(mut self, n: i64) -> Self {
1014        self.limit = Some(n);
1015        self
1016    }
1017
1018    /// Set `OFFSET n` (bound as a placeholder). SELECT-only.
1019    ///
1020    /// `offset` requires `limit`: compiling an offset without a limit panics
1021    /// (`offset(...) requires limit(...)`), uniform across dialects since MySQL
1022    /// rejects a bare `OFFSET`.
1023    pub fn offset(mut self, n: i64) -> Self {
1024        self.offset = Some(n);
1025        self
1026    }
1027
1028    /// Lock selected rows with `FOR UPDATE`.
1029    ///
1030    /// Honored by Postgres / MySQL; a **silent no-op on SQLite**. Preserves any
1031    /// `SKIP LOCKED` / `NOWAIT` modifier already set. **SELECT-only:** compiling
1032    /// panics if attached to INSERT/UPDATE/DELETE or combined with `UNION`.
1033    pub fn for_update(mut self) -> Self {
1034        let wait = self.lock.and_then(|l| l.wait);
1035        self.lock = Some(Lock {
1036            strength: LockStrength::Update,
1037            wait,
1038        });
1039        self
1040    }
1041
1042    /// Lock selected rows with `FOR SHARE`.
1043    ///
1044    /// Honored by Postgres / MySQL; a **silent no-op on SQLite**. Preserves any
1045    /// `SKIP LOCKED` / `NOWAIT` modifier already set. **SELECT-only:** compiling
1046    /// panics if attached to INSERT/UPDATE/DELETE or combined with `UNION`.
1047    pub fn for_share(mut self) -> Self {
1048        let wait = self.lock.and_then(|l| l.wait);
1049        self.lock = Some(Lock {
1050            strength: LockStrength::Share,
1051            wait,
1052        });
1053        self
1054    }
1055
1056    /// Add `SKIP LOCKED` to the row-locking clause (skip already-locked rows).
1057    ///
1058    /// If no lock strength was set yet, defaults to `FOR UPDATE`. SELECT-only;
1059    /// no-op on SQLite.
1060    pub fn skip_locked(mut self) -> Self {
1061        let strength = self
1062            .lock
1063            .map(|l| l.strength)
1064            .unwrap_or(LockStrength::Update);
1065        self.lock = Some(Lock {
1066            strength,
1067            wait: Some(LockWait::SkipLocked),
1068        });
1069        self
1070    }
1071
1072    /// Add `NOWAIT` to the row-locking clause (error if a row is already locked).
1073    ///
1074    /// If no lock strength was set yet, defaults to `FOR UPDATE`. SELECT-only;
1075    /// no-op on SQLite.
1076    pub fn no_wait(mut self) -> Self {
1077        let strength = self
1078            .lock
1079            .map(|l| l.strength)
1080            .unwrap_or(LockStrength::Update);
1081        self.lock = Some(Lock {
1082            strength,
1083            wait: Some(LockWait::NoWait),
1084        });
1085        self
1086    }
1087
1088    fn push_join(
1089        mut self,
1090        kind: JoinKind,
1091        table: &str,
1092        f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
1093    ) -> Self {
1094        let on = f(JoinClause::new()).into_conds();
1095        self.joins.push(Join {
1096            kind,
1097            table: table.to_owned(),
1098            on,
1099        });
1100        self
1101    }
1102
1103    /// `INNER JOIN table ON …` — conditions built by the closure.
1104    ///
1105    /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
1106    pub fn join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
1107        self.push_join(JoinKind::Inner, table, f)
1108    }
1109
1110    /// `LEFT JOIN table ON …`. SELECT-only.
1111    pub fn left_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
1112        self.push_join(JoinKind::Left, table, f)
1113    }
1114
1115    /// `RIGHT JOIN table ON …`. SELECT-only.
1116    pub fn right_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
1117        self.push_join(JoinKind::Right, table, f)
1118    }
1119
1120    /// `FULL OUTER JOIN table ON …`. SELECT-only.
1121    pub fn full_outer_join(
1122        self,
1123        table: &str,
1124        f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
1125    ) -> Self {
1126        self.push_join(JoinKind::FullOuter, table, f)
1127    }
1128
1129    /// `CROSS JOIN table` — takes **no** `ON` closure (a cross join has no
1130    /// condition). SELECT-only.
1131    pub fn cross_join(mut self, table: &str) -> Self {
1132        self.joins.push(Join {
1133            kind: JoinKind::Cross,
1134            table: table.to_owned(),
1135            on: Vec::new(),
1136        });
1137        self
1138    }
1139
1140    /// `HAVING col op ?` — `col` is a real column/alias (escaped); value bound.
1141    ///
1142    /// For aggregate expressions like `COUNT(*) > ?`, use [`Self::having_raw`].
1143    /// SELECT-only: ignored for INSERT/UPDATE/DELETE. Multiple HAVING terms are
1144    /// joined by `AND`.
1145    ///
1146    /// # Operator allowlist (injection guard)
1147    ///
1148    /// Unlike `where_eq`/`where_column`/`JoinClause::on`, which take
1149    /// `op: &'static str` (so only compile-time literals are accepted), this
1150    /// method takes `op: &str` for ergonomics. Because `op` is emitted
1151    /// **verbatim** into the SQL (it is not a bound value and cannot be escaped
1152    /// without changing its meaning), an attacker-controlled operator would be a
1153    /// SQL-injection vector. To prevent that, `op` is validated against a fixed
1154    /// set of comparison operators. A disallowed operator records a deferred
1155    /// [`BuildError::InvalidHavingOperator`] (the chain stays intact):
1156    /// [`Self::try_to_sql`] returns it as `Err`, while [`Self::to_sql`] panics
1157    /// with the same message (fail-loud, like the `offset`/`distinct_on`/lock
1158    /// guards). The operator is matched case-insensitively and stored trimmed.
1159    /// For anything outside this set — arbitrary aggregate expressions, custom
1160    /// operators — use [`Self::having_raw`], the documented verbatim escape
1161    /// hatch.
1162    ///
1163    /// Allowed: `=`, `!=`, `<>`, `>`, `>=`, `<`, `<=`, `LIKE`, `NOT LIKE`.
1164    pub fn having(mut self, col: &str, op: &str, val: impl IntoBind) -> Self {
1165        const ALLOWED_HAVING_OPS: &[&str] =
1166            &["=", "!=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"];
1167        let normalized = op.trim();
1168        if !ALLOWED_HAVING_OPS
1169            .iter()
1170            .any(|allowed| allowed.eq_ignore_ascii_case(normalized))
1171        {
1172            // Keep the FIRST error: it points at the original misuse, and a
1173            // later mistake must not mask it.
1174            if self.error.is_none() {
1175                self.error = Some(BuildError::InvalidHavingOperator(op.to_owned()));
1176            }
1177            return self;
1178        }
1179        self.havings.push(Having::Col {
1180            col: col.to_owned(),
1181            op: normalized.to_owned(),
1182            val: val.into_bind(),
1183        });
1184        self
1185    }
1186
1187    /// Raw `HAVING` expression with its own binds — the verbatim escape hatch
1188    /// for aggregates (e.g. `having_raw("COUNT(*) > ?", …)`).
1189    ///
1190    /// # Warning: positional placeholder contract
1191    ///
1192    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
1193    /// `binds` are appended to the running bind list in order. For
1194    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
1195    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
1196    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
1197    /// produces a malformed query.
1198    pub fn having_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
1199        self.havings.push(Having::Raw {
1200            sql: sql.to_owned(),
1201            binds,
1202        });
1203        self
1204    }
1205
1206    /// Add a `WITH name AS (query)` common table expression. SELECT-only.
1207    ///
1208    /// CTE bodies are compiled before the main query, so their binds (and pg
1209    /// `$N` numbers) appear first.
1210    pub fn with(mut self, name: &str, query: QueryBuilder<D>) -> Self {
1211        self.ctes.push(Cte {
1212            name: name.to_owned(),
1213            recursive: false,
1214            query,
1215        });
1216        self
1217    }
1218
1219    /// Add a recursive CTE. If any CTE is recursive, the single `WITH` carries
1220    /// `RECURSIVE`. SELECT-only.
1221    pub fn with_recursive(mut self, name: &str, query: QueryBuilder<D>) -> Self {
1222        self.ctes.push(Cte {
1223            name: name.to_owned(),
1224            recursive: true,
1225            query,
1226        });
1227        self
1228    }
1229
1230    /// Append a `UNION query` arm. SELECT-only.
1231    pub fn union(mut self, query: QueryBuilder<D>) -> Self {
1232        self.unions.push((false, query));
1233        self
1234    }
1235
1236    /// Append a `UNION ALL query` arm. SELECT-only.
1237    pub fn union_all(mut self, query: QueryBuilder<D>) -> Self {
1238        self.unions.push((true, query));
1239        self
1240    }
1241
1242    /// Conditionally apply `f` to the builder, keeping the chain intact.
1243    ///
1244    /// Returns `f(self)` when `cond` is true, otherwise `self` unchanged. This
1245    /// lets you add clauses based on a runtime flag without breaking the
1246    /// by-value chain.
1247    ///
1248    /// ```
1249    /// use chain_builder::{Postgres, QueryBuilder};
1250    /// let only_active = true;
1251    /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
1252    ///     .select(["id"])
1253    ///     .when(only_active, |q| q.where_eq("status", "active"))
1254    ///     .to_sql();
1255    /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
1256    /// ```
1257    pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
1258        if cond {
1259            f(self)
1260        } else {
1261            self
1262        }
1263    }
1264
1265    /// Apply `if_true` when `cond` holds, otherwise `if_false`, keeping the
1266    /// chain intact.
1267    ///
1268    /// ```
1269    /// use chain_builder::{Postgres, QueryBuilder};
1270    /// let active = false;
1271    /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
1272    ///     .select(["id"])
1273    ///     .when_else(
1274    ///         active,
1275    ///         |q| q.where_eq("status", "active"),
1276    ///         |q| q.where_eq("status", "inactive"),
1277    ///     )
1278    ///     .to_sql();
1279    /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
1280    /// ```
1281    pub fn when_else(
1282        self,
1283        cond: bool,
1284        if_true: impl FnOnce(Self) -> Self,
1285        if_false: impl FnOnce(Self) -> Self,
1286    ) -> Self {
1287        if cond {
1288            if_true(self)
1289        } else {
1290            if_false(self)
1291        }
1292    }
1293
1294    /// Apply `LIMIT`/`OFFSET` for a **1-based** page: row window
1295    /// `[(page-1) * per_page, page * per_page)`.
1296    ///
1297    /// Equivalent to `self.limit(per_page).offset((page - 1).max(0) * per_page)`.
1298    /// A `page < 1` is treated as page 1 (offset 0), so callers never get a
1299    /// negative offset. SELECT-only, like [`Self::limit`] / [`Self::offset`].
1300    ///
1301    /// ```
1302    /// use chain_builder::{Postgres, QueryBuilder, Value};
1303    /// let (sql, binds) = QueryBuilder::<Postgres>::table("users")
1304    ///     .select(["id"])
1305    ///     .paginate(2, 10)
1306    ///     .to_sql();
1307    /// assert_eq!(sql, r#"SELECT "id" FROM "users" LIMIT $1 OFFSET $2"#);
1308    /// assert_eq!(binds, vec![Value::I64(10), Value::I64(10)]);
1309    /// ```
1310    pub fn paginate(self, page: i64, per_page: i64) -> Self {
1311        self.limit(per_page).offset((page - 1).max(0) * per_page)
1312    }
1313
1314    /// Compile to `(sql, binds)`, panicking on an invalid builder.
1315    ///
1316    /// Panicking twin of [`Self::try_to_sql`]; the panic message is the
1317    /// [`BuildError`]'s `Display` text. Prefer [`Self::try_to_sql`] when any
1318    /// part of the query is driven by runtime input (e.g. an HTTP request), so
1319    /// misuse maps to an error response instead of a crash.
1320    pub fn to_sql(&self) -> (String, Vec<Value>) {
1321        compile(self)
1322    }
1323
1324    /// Compile to `(sql, binds)`, or return the [`BuildError`] describing why
1325    /// the query cannot be rendered (invalid construction such as `offset()`
1326    /// without `limit()`, an empty `insert()`/`update()`, a row lock outside
1327    /// SELECT, or a disallowed `having()` operator).
1328    ///
1329    /// Errors recorded by builder methods (deferred, chain-preserving) and
1330    /// errors detected during compilation — including inside nested builders
1331    /// (CTEs, UNION arms, subqueries) — all surface here.
1332    pub fn try_to_sql(&self) -> Result<(String, Vec<Value>), BuildError> {
1333        try_compile(self)
1334    }
1335
1336    /// Render the compiled query as a human-readable string for logs and
1337    /// debugging: the SQL, then one indented line per bind.
1338    ///
1339    /// ```
1340    /// use chain_builder::{Postgres, QueryBuilder};
1341    /// let qb = QueryBuilder::<Postgres>::table("users")
1342    ///     .select(["id"])
1343    ///     .where_eq("status", "active");
1344    /// assert_eq!(
1345    ///     qb.to_sql_pretty(),
1346    ///     "SELECT \"id\" FROM \"users\" WHERE \"status\" = $1\nbinds:\n  $1 = Text(\"active\")"
1347    /// );
1348    /// ```
1349    ///
1350    /// Bind labels are the dialect placeholder (`$1`, `$2`, … on Postgres);
1351    /// dialects whose placeholder is a bare `?` get a 1-based ordinal
1352    /// appended for readability (`?1`, `?2`, …) — the SQL itself still uses
1353    /// `?`. With zero binds the output is the SQL line only. The output
1354    /// format is for humans and is **not** a stability contract.
1355    ///
1356    /// # Warning
1357    ///
1358    /// The output includes the **literal bind values**. Do not log it if
1359    /// any bind may contain sensitive data (passwords, tokens, PII).
1360    ///
1361    /// Panicking twin of [`Self::try_to_sql_pretty`]; the panic message is
1362    /// the [`BuildError`]'s `Display` text (same policy as
1363    /// [`Self::to_sql`]).
1364    pub fn to_sql_pretty(&self) -> String {
1365        self.try_to_sql_pretty().unwrap_or_else(|e| panic!("{e}"))
1366    }
1367
1368    /// Fallible twin of [`Self::to_sql_pretty`]; surfaces the same
1369    /// [`BuildError`] as [`Self::try_to_sql`]. The same sensitive-data
1370    /// warning applies.
1371    pub fn try_to_sql_pretty(&self) -> Result<String, BuildError> {
1372        use std::fmt::Write as _;
1373        let (sql, binds) = try_compile(self)?;
1374        let mut out = sql;
1375        if !binds.is_empty() {
1376            out.push_str("\nbinds:");
1377            let mut label = String::with_capacity(4);
1378            for (i, b) in binds.iter().enumerate() {
1379                label.clear();
1380                D::write_placeholder(&mut label, i + 1);
1381                if label == "?" {
1382                    // Infallible for String.
1383                    let _ = write!(label, "{}", i + 1);
1384                }
1385                out.push_str("\n  ");
1386                out.push_str(&label);
1387                out.push_str(" = ");
1388                let _ = write!(out, "{b:?}");
1389            }
1390        }
1391        Ok(out)
1392    }
1393}