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;
10use crate::dialect::Dialect;
11use crate::value::{IntoBind, Value};
12use crate::where_::{Conj, Predicate, WhereBuilder};
13
14/// Sort direction for an `ORDER BY` column.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Order {
17    /// Ascending (`ASC`).
18    Asc,
19    /// Descending (`DESC`).
20    Desc,
21}
22
23/// The kind of SQL `JOIN`.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum JoinKind {
26    /// `INNER JOIN`.
27    Inner,
28    /// `LEFT JOIN`.
29    Left,
30    /// `RIGHT JOIN`.
31    Right,
32    /// `FULL OUTER JOIN`.
33    FullOuter,
34    /// `CROSS JOIN` (no `ON`).
35    Cross,
36}
37
38/// A single `ON` condition of a [`Join`].
39///
40/// Columns in `On`/`OnVal` are stored raw and escaped at compile time. `OnRaw`
41/// is the verbatim escape hatch (see [`JoinClause::on_raw`]).
42#[derive(Debug, Clone, PartialEq)]
43pub enum JoinCond {
44    /// `lhs op rhs` — both sides are columns (escaped at compile time).
45    On(String, &'static str, String),
46    /// `col op ?` — `col` escaped, the value is bound.
47    OnVal(String, &'static str, Value),
48    /// Verbatim SQL with its own binds.
49    OnRaw(String, Vec<Value>),
50}
51
52/// A `JOIN` clause: a kind, a target table, and zero or more `ON` conditions.
53#[derive(Debug, Clone, PartialEq)]
54pub struct Join {
55    /// The join kind (`INNER`, `LEFT`, …).
56    pub kind: JoinKind,
57    /// Raw target table identifier (escaped at compile time).
58    pub table: String,
59    /// `ON` conditions, joined by `AND`. Empty for `CROSS JOIN`.
60    pub on: Vec<JoinCond>,
61}
62
63/// A `HAVING` condition (SELECT-only, rendered after `GROUP BY`).
64#[derive(Debug, Clone, PartialEq)]
65pub enum Having {
66    /// `col op ?` — `col` is a real column/alias (escaped); value bound.
67    Col {
68        /// Raw column identifier (escaped at compile time).
69        col: String,
70        /// SQL operator token (`>`, `=`, …).
71        op: String,
72        /// Bound value.
73        val: Value,
74    },
75    /// Verbatim aggregate expression with its own binds (e.g. `COUNT(*) > ?`).
76    Raw {
77        /// Verbatim SQL.
78        sql: String,
79        /// Bound values appended in order.
80        binds: Vec<Value>,
81    },
82}
83
84/// A common table expression (`WITH` / `WITH RECURSIVE`).
85#[derive(Debug, Clone, PartialEq)]
86pub struct Cte<D: Dialect> {
87    /// Raw CTE name (escaped at compile time).
88    pub name: String,
89    /// Whether this CTE forces the single `WITH` to carry `RECURSIVE`.
90    pub recursive: bool,
91    /// The sub-query compiled into the CTE body.
92    pub query: QueryBuilder<D>,
93}
94
95/// Accumulator passed to `join`/`left_join`/… closures to build `ON` conditions.
96///
97/// The closure receives an empty `JoinClause`, chains `on`/`on_val`/`on_raw`
98/// calls, and returns it; the builder stores the collected conditions.
99pub struct JoinClause<D: Dialect> {
100    conds: Vec<JoinCond>,
101    _marker: PhantomData<D>,
102}
103
104impl<D: Dialect> Default for JoinClause<D> {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl<D: Dialect> JoinClause<D> {
111    /// Create an empty accumulator.
112    pub fn new() -> Self {
113        Self {
114            conds: Vec::new(),
115            _marker: PhantomData,
116        }
117    }
118
119    fn into_conds(self) -> Vec<JoinCond> {
120        self.conds
121    }
122
123    /// `lhs op rhs` — both sides are columns (each escaped at compile time).
124    pub fn on(mut self, col: &str, op: &'static str, col2: &str) -> Self {
125        self.conds
126            .push(JoinCond::On(col.to_owned(), op, col2.to_owned()));
127        self
128    }
129
130    /// `col op ?` — `col` escaped, the value bound as a placeholder.
131    pub fn on_val(mut self, col: &str, op: &'static str, val: impl IntoBind) -> Self {
132        self.conds
133            .push(JoinCond::OnVal(col.to_owned(), op, val.into_bind()));
134        self
135    }
136
137    /// Raw `ON` SQL fragment with its own binds — the verbatim escape hatch.
138    ///
139    /// # Warning: positional placeholder contract
140    ///
141    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
142    /// `binds` are appended to the running bind list in order. For
143    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
144    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
145    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
146    /// produces a malformed query.
147    pub fn on_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
148        self.conds.push(JoinCond::OnRaw(sql.to_owned(), binds));
149        self
150    }
151}
152
153/// What to do when an `INSERT` hits a conflict (see [`OnConflict`]).
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum ConflictAction {
156    /// Skip the conflicting row (`DO NOTHING` / `INSERT IGNORE`).
157    DoNothing,
158    /// Update the non-target inserted columns from the proposed row
159    /// (`DO UPDATE SET … = EXCLUDED.…` / `ON DUPLICATE KEY UPDATE …`).
160    Merge,
161}
162
163/// An `ON CONFLICT` specification attached to an `INSERT`.
164///
165/// `targets` are the raw conflict-target column identifiers (escaped at compile
166/// time). They are honored by Postgres / SQLite (`OnConflict` style) and
167/// **ignored** by MySQL (`OnDuplicateKey` style), which relies on its own
168/// unique/primary keys.
169#[derive(Debug, Clone, PartialEq)]
170pub struct OnConflict {
171    /// Raw conflict-target column identifiers.
172    pub targets: Vec<String>,
173    /// What to do on conflict.
174    pub action: ConflictAction,
175}
176
177/// Which kind of statement is being built.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
179pub enum Method {
180    /// `SELECT`.
181    #[default]
182    Select,
183    /// `INSERT`.
184    Insert,
185    /// `UPDATE`.
186    Update,
187    /// `DELETE`.
188    Delete,
189}
190
191/// Typed, dialect-aware SQL query builder.
192#[derive(Debug, Clone, PartialEq)]
193pub struct QueryBuilder<D: Dialect> {
194    pub(crate) table: String,
195    /// Optional database/schema qualifier (multi-tenant: one connection, many DBs).
196    /// When set, prefixes the main table and join tables: `"db"."table"`.
197    pub(crate) db: Option<String>,
198    pub(crate) select_cols: Vec<String>,
199    /// Raw `SELECT` expressions (verbatim, NOT escaped) with their own binds,
200    /// appended after `select_cols`. Backs `select_raw`.
201    pub(crate) select_raw: Vec<(String, Vec<Value>)>,
202    /// Subquery `SELECT` columns: `(alias, sub)` → `(<sub>) AS {esc alias}`,
203    /// appended after `select_cols` / `select_raw`. Backs `select_subquery`.
204    pub(crate) select_subqueries: Vec<(String, Box<QueryBuilder<D>>)>,
205    /// `SELECT DISTINCT` flag (raw; off by default for M1 byte-identity).
206    pub(crate) distinct: bool,
207    /// `SELECT DISTINCT ON (cols)` columns (raw; Postgres-only).
208    pub(crate) distinct_on: Vec<String>,
209    pub(crate) wheres: Vec<Predicate<D>>,
210    pub(crate) method: Method,
211    pub(crate) set: Vec<(String, Value)>,
212    /// Multi-row `INSERT` rows (empty unless `insert_many` was used). Each row is
213    /// a `(column, value)` list; columns come from the first row's sorted keys.
214    pub(crate) insert_rows: Vec<Vec<(String, Value)>>,
215    pub(crate) joins: Vec<Join>,
216    pub(crate) groups: Vec<String>,
217    /// Raw `GROUP BY` fragment (verbatim) with its own binds, appended after any
218    /// structured `groups`.
219    pub(crate) group_by_raw: Option<(String, Vec<Value>)>,
220    pub(crate) havings: Vec<Having>,
221    pub(crate) orders: Vec<(String, Order)>,
222    /// Raw `ORDER BY` fragment (verbatim) with its own binds, appended after any
223    /// structured `orders`.
224    pub(crate) order_by_raw: Option<(String, Vec<Value>)>,
225    pub(crate) limit: Option<i64>,
226    pub(crate) offset: Option<i64>,
227    pub(crate) ctes: Vec<Cte<D>>,
228    pub(crate) unions: Vec<(bool, QueryBuilder<D>)>,
229    /// `ON CONFLICT` spec for `INSERT` (ignored on UPDATE/DELETE).
230    pub(crate) on_conflict: Option<OnConflict>,
231    /// `RETURNING` column list (raw; `"*"` emitted unescaped).
232    pub(crate) returning: Vec<String>,
233    _marker: PhantomData<D>,
234}
235
236impl<D: Dialect> QueryBuilder<D> {
237    /// Start a query against `name`.
238    pub fn table(name: &str) -> Self {
239        Self {
240            table: name.to_owned(),
241            db: None,
242            select_cols: Vec::new(),
243            select_raw: Vec::new(),
244            select_subqueries: Vec::new(),
245            distinct: false,
246            distinct_on: Vec::new(),
247            wheres: Vec::new(),
248            method: Method::Select,
249            set: Vec::new(),
250            insert_rows: Vec::new(),
251            joins: Vec::new(),
252            groups: Vec::new(),
253            group_by_raw: None,
254            havings: Vec::new(),
255            orders: Vec::new(),
256            order_by_raw: None,
257            limit: None,
258            offset: None,
259            ctes: Vec::new(),
260            unions: Vec::new(),
261            on_conflict: None,
262            returning: Vec::new(),
263            _marker: PhantomData,
264        }
265    }
266
267    /// Set the database/schema qualifier (multi-tenant: one connection, many DBs).
268    ///
269    /// The name prefixes the main table and every join table, escaped per dialect:
270    /// `QueryBuilder::<Postgres>::table("users").db("mydb")` →
271    /// `… FROM "mydb"."users"`. Matches 1.x `db()`.
272    pub fn db(mut self, name: &str) -> Self {
273        self.db = Some(name.to_owned());
274        self
275    }
276
277    /// Restrict the selected columns. An empty list selects `*`.
278    pub fn select<I, S>(mut self, cols: I) -> Self
279    where
280        I: IntoIterator<Item = S>,
281        S: AsRef<str>,
282    {
283        self.select_cols = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
284        self
285    }
286
287    /// Add a raw `SELECT` expression (verbatim, NOT escaped) with optional binds
288    /// — the escape hatch for aggregates/functions like `COUNT(*)`.
289    ///
290    /// Appended to the column list after any [`Self::select`] columns. Multiple
291    /// calls accumulate.
292    ///
293    /// # Warning: positional placeholder contract
294    ///
295    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and any
296    /// `binds` are appended to the running bind list in order. For **Postgres**,
297    /// the caller MUST write `$N` numbers matching the actual bind position. For
298    /// MySQL/SQLite use `?`.
299    pub fn select_raw(mut self, sql: &str, binds: Option<Vec<Value>>) -> Self {
300        self.select_raw
301            .push((sql.to_owned(), binds.unwrap_or_default()));
302        self
303    }
304
305    /// Add a subquery `SELECT` column: emits `(<sub>) AS {alias}` after the
306    /// regular columns and any [`Self::select_raw`] expressions.
307    ///
308    /// The subquery is compiled with placeholder continuity (its binds appear in
309    /// `$N` order at the point it is emitted — before the `WHERE` clause, since
310    /// the SELECT list is rendered first). SELECT-only.
311    pub fn select_subquery(mut self, alias: &str, sub: QueryBuilder<D>) -> Self {
312        self.select_subqueries
313            .push((alias.to_owned(), Box::new(sub)));
314        self
315    }
316
317    /// Emit `SELECT DISTINCT …` (all dialects).
318    pub fn distinct(mut self) -> Self {
319        self.distinct = true;
320        self
321    }
322
323    /// Emit `SELECT DISTINCT ON (cols) …` — **Postgres only**.
324    ///
325    /// `cols` are raw identifiers (escaped at compile time). Compiling against a
326    /// dialect without `DISTINCT ON` support panics
327    /// (`DISTINCT ON requires PostgreSQL`).
328    pub fn distinct_on<I, S>(mut self, cols: I) -> Self
329    where
330        I: IntoIterator<Item = S>,
331        S: AsRef<str>,
332    {
333        self.distinct_on = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
334        self.distinct = true;
335        self
336    }
337
338    /// `col ILIKE val` — dialect-aware case-insensitive match.
339    ///
340    /// On **Postgres** this compiles to the native `{col} ILIKE {ph}`. On
341    /// MySQL/SQLite (no native `ILIKE`) it compiles to
342    /// `LOWER({col}) LIKE LOWER({ph})`.
343    pub fn where_ilike(mut self, col: &str, val: impl IntoBind) -> Self {
344        self.wheres.push(Predicate::ILike {
345            col: col.to_owned(),
346            val: val.into_bind(),
347        });
348        self
349    }
350
351    /// `col @> val` — JSONB containment.
352    ///
353    /// **Postgres-specific:** the `@>` operator is emitted verbatim for all
354    /// dialects, but is only meaningful on Postgres `jsonb` columns. `val` is
355    /// typically a JSON text string (or `Value::Json` behind the `json`
356    /// feature).
357    pub fn where_jsonb_contains(mut self, col: &str, val: impl IntoBind) -> Self {
358        self.wheres.push(Predicate::JsonContains {
359            col: col.to_owned(),
360            val: val.into_bind(),
361        });
362        self
363    }
364
365    fn binary(mut self, col: &str, op: &'static str, val: impl IntoBind) -> Self {
366        self.wheres.push(Predicate::Binary {
367            col: col.to_owned(),
368            op,
369            val: val.into_bind(),
370        });
371        self
372    }
373
374    /// `col = val`.
375    pub fn where_eq(self, col: &str, val: impl IntoBind) -> Self {
376        self.binary(col, "=", val)
377    }
378
379    /// `col != val`.
380    pub fn where_ne(self, col: &str, val: impl IntoBind) -> Self {
381        self.binary(col, "!=", val)
382    }
383
384    /// `col > val`.
385    pub fn where_gt(self, col: &str, val: impl IntoBind) -> Self {
386        self.binary(col, ">", val)
387    }
388
389    /// `col >= val`.
390    pub fn where_gte(self, col: &str, val: impl IntoBind) -> Self {
391        self.binary(col, ">=", val)
392    }
393
394    /// `col < val`.
395    pub fn where_lt(self, col: &str, val: impl IntoBind) -> Self {
396        self.binary(col, "<", val)
397    }
398
399    /// `col <= val`.
400    pub fn where_lte(self, col: &str, val: impl IntoBind) -> Self {
401        self.binary(col, "<=", val)
402    }
403
404    /// `col LIKE val`.
405    pub fn where_like(self, col: &str, val: impl IntoBind) -> Self {
406        self.binary(col, "LIKE", val)
407    }
408
409    fn in_(mut self, col: &str, neg: bool, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
410        self.wheres.push(Predicate::In {
411            col: col.to_owned(),
412            neg,
413            vals: vals.into_iter().map(IntoBind::into_bind).collect(),
414        });
415        self
416    }
417
418    /// `col IN (...)`.
419    pub fn where_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
420        self.in_(col, false, vals)
421    }
422
423    /// `col NOT IN (...)`.
424    pub fn where_not_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
425        self.in_(col, true, vals)
426    }
427
428    fn null(mut self, col: &str, neg: bool) -> Self {
429        self.wheres.push(Predicate::Null {
430            col: col.to_owned(),
431            neg,
432        });
433        self
434    }
435
436    /// `col IS NULL`.
437    pub fn where_null(self, col: &str) -> Self {
438        self.null(col, false)
439    }
440
441    /// `col IS NOT NULL`.
442    pub fn where_not_null(self, col: &str) -> Self {
443        self.null(col, true)
444    }
445
446    /// `col BETWEEN lo AND hi`.
447    pub fn where_between(mut self, col: &str, lo: impl IntoBind, hi: impl IntoBind) -> Self {
448        self.wheres.push(Predicate::Between {
449            col: col.to_owned(),
450            lo: lo.into_bind(),
451            hi: hi.into_bind(),
452        });
453        self
454    }
455
456    /// Raw SQL predicate with its own binds — the verbatim escape hatch.
457    ///
458    /// # Warning: positional placeholder contract
459    ///
460    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
461    /// `binds` are appended to the running bind list in order. For
462    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
463    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
464    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
465    /// produces a malformed query.
466    pub fn where_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
467        self.wheres.push(Predicate::Raw {
468            sql: sql.to_owned(),
469            binds,
470        });
471        self
472    }
473
474    /// `lhs op rhs` — compare two column identifiers (both escaped at compile
475    /// time), no bind. e.g. `where_column("orders.user_id", "=", "users.id")`.
476    pub fn where_column(mut self, lhs: &str, op: &'static str, rhs: &str) -> Self {
477        self.wheres.push(Predicate::Column {
478            lhs: lhs.to_owned(),
479            op,
480            rhs: rhs.to_owned(),
481        });
482        self
483    }
484
485    /// `EXISTS (subquery)` — takes an already-built sub-builder by value
486    /// (mirrors [`Self::union`] / [`Self::with`]). The sub-query is compiled
487    /// with placeholder continuity.
488    pub fn where_exists(mut self, sub: QueryBuilder<D>) -> Self {
489        self.wheres.push(Predicate::Exists {
490            neg: false,
491            sub: Box::new(sub),
492        });
493        self
494    }
495
496    /// `NOT EXISTS (subquery)`. See [`Self::where_exists`].
497    pub fn where_not_exists(mut self, sub: QueryBuilder<D>) -> Self {
498        self.wheres.push(Predicate::Exists {
499            neg: true,
500            sub: Box::new(sub),
501        });
502        self
503    }
504
505    /// `col IN (subquery)` — takes an already-built sub-builder by value. The
506    /// sub-query is compiled with placeholder continuity.
507    pub fn where_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
508        self.wheres.push(Predicate::InSubquery {
509            col: col.to_owned(),
510            neg: false,
511            sub: Box::new(sub),
512        });
513        self
514    }
515
516    /// `col NOT IN (subquery)`. See [`Self::where_in_subquery`].
517    pub fn where_not_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
518        self.wheres.push(Predicate::InSubquery {
519            col: col.to_owned(),
520            neg: true,
521            sub: Box::new(sub),
522        });
523        self
524    }
525
526    fn group(
527        mut self,
528        outer_conj: Conj,
529        f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>,
530    ) -> Self {
531        let preds = f(WhereBuilder::new()).into_preds();
532        self.wheres.push(Predicate::Group { outer_conj, preds });
533        self
534    }
535
536    /// Add a parenthesized `AND (...)` group built by the closure.
537    pub fn and_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
538        self.group(Conj::And, f)
539    }
540
541    /// Add a parenthesized `OR (...)` group built by the closure.
542    pub fn or_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
543        self.group(Conj::Or, f)
544    }
545
546    /// Build an `INSERT` from a single row of `(column, value)` pairs.
547    pub fn insert<K, V, I>(mut self, row: I) -> Self
548    where
549        K: AsRef<str>,
550        V: IntoBind,
551        I: IntoIterator<Item = (K, V)>,
552    {
553        self.method = Method::Insert;
554        self.set = row
555            .into_iter()
556            .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
557            .collect();
558        self
559    }
560
561    /// Build a multi-row `INSERT` from an iterator of rows, each a sequence of
562    /// `(column, value)` pairs.
563    ///
564    /// The inserted column set is taken from the **first** row's keys (sorted, as
565    /// with [`Self::insert`]). For each subsequent row, a value is bound for every
566    /// column in that set; a key **missing** in a later row binds `Value::Null`
567    /// rather than panicking (DoS-safe, matching the 1.x hardening). Composes with
568    /// `on_conflict_*` and `returning`.
569    pub fn insert_many<K, V, R, Rows>(mut self, rows: Rows) -> Self
570    where
571        K: AsRef<str>,
572        V: IntoBind,
573        R: IntoIterator<Item = (K, V)>,
574        Rows: IntoIterator<Item = R>,
575    {
576        self.method = Method::Insert;
577        self.insert_rows = rows
578            .into_iter()
579            .map(|row| {
580                row.into_iter()
581                    .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
582                    .collect()
583            })
584            .collect();
585        self
586    }
587
588    /// Build an `UPDATE` from `(column, value)` pairs. WHERE still applies.
589    pub fn update<K, V, I>(mut self, set: I) -> Self
590    where
591        K: AsRef<str>,
592        V: IntoBind,
593        I: IntoIterator<Item = (K, V)>,
594    {
595        self.method = Method::Update;
596        self.set = set
597            .into_iter()
598            .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
599            .collect();
600        self
601    }
602
603    /// Build a `DELETE`. WHERE still applies.
604    pub fn delete(mut self) -> Self {
605        self.method = Method::Delete;
606        self
607    }
608
609    /// On conflict, skip the row (`INSERT`-only; ignored on UPDATE/DELETE).
610    ///
611    /// `targets` are the conflict-target columns (may be empty).
612    ///
613    /// - **Postgres / SQLite:** emits `ON CONFLICT ({targets}) DO NOTHING`, or
614    ///   bare `ON CONFLICT DO NOTHING` when `targets` is empty.
615    /// - **MySQL:** emits `INSERT IGNORE INTO …` (no trailing clause). Note that
616    ///   `IGNORE` suppresses *more* than duplicate-key errors (also truncation
617    ///   and bad-value coercion) — broader than pg/sqlite `DO NOTHING`.
618    pub fn on_conflict_do_nothing<I, S>(mut self, targets: I) -> Self
619    where
620        I: IntoIterator<Item = S>,
621        S: AsRef<str>,
622    {
623        self.on_conflict = Some(OnConflict {
624            targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
625            action: ConflictAction::DoNothing,
626        });
627        self
628    }
629
630    /// On conflict, update the non-target inserted columns from the proposed row
631    /// (`INSERT`-only; ignored on UPDATE/DELETE).
632    ///
633    /// - **Postgres / SQLite:** emits
634    ///   `ON CONFLICT ({targets}) DO UPDATE SET {c} = EXCLUDED.{c}, …` for every
635    ///   inserted column *except* the conflict targets. If `targets` is empty or
636    ///   covers all inserted columns (empty SET list), falls back to the
637    ///   `DO NOTHING` rendering (pg/sqlite require a target for `DO UPDATE`).
638    /// - **MySQL:** the explicit `targets` are **ignored** (MySQL uses its own
639    ///   unique/primary keys); emits
640    ///   `ON DUPLICATE KEY UPDATE {c} = VALUES({c}), …` for *all* inserted
641    ///   columns. `VALUES()` is used for MySQL 5.7/8.x compatibility. Including a
642    ///   PK column in the insert set yields a redundant-but-harmless
643    ///   `pk = VALUES(pk)`.
644    pub fn on_conflict_merge<I, S>(mut self, targets: I) -> Self
645    where
646        I: IntoIterator<Item = S>,
647        S: AsRef<str>,
648    {
649        self.on_conflict = Some(OnConflict {
650            targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
651            action: ConflictAction::Merge,
652        });
653        self
654    }
655
656    /// Add a `RETURNING` column list. Works on INSERT / UPDATE / DELETE for
657    /// Postgres and SQLite; a `"*"` column is emitted unescaped (`RETURNING *`).
658    ///
659    /// On **MySQL** this is a silent no-op (MySQL has no `RETURNING`). On
660    /// **SQLite** `RETURNING` requires SQLite ≥ 3.35.0 (2021); `supports_returning()`
661    /// is a compile-time dialect flag, not a runtime version check.
662    pub fn returning<I, S>(mut self, cols: I) -> Self
663    where
664        I: IntoIterator<Item = S>,
665        S: AsRef<str>,
666    {
667        self.returning = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
668        self
669    }
670
671    /// Add `GROUP BY` columns (raw owned identifiers, escaped at compile time).
672    ///
673    /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
674    pub fn group_by<I, S>(mut self, cols: I) -> Self
675    where
676        I: IntoIterator<Item = S>,
677        S: AsRef<str>,
678    {
679        self.groups
680            .extend(cols.into_iter().map(|c| c.as_ref().to_owned()));
681        self
682    }
683
684    /// Add a raw `GROUP BY` fragment with its own binds — the verbatim escape
685    /// hatch. SELECT-only.
686    ///
687    /// The fragment is appended after any structured [`Self::group_by`] columns
688    /// within the same `GROUP BY` clause (e.g. `GROUP BY "a", <raw>`); if no
689    /// structured columns are present it becomes the whole `GROUP BY <raw>`.
690    ///
691    /// # Warning: positional placeholder contract
692    ///
693    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
694    /// `binds` are appended to the running bind list in order. For
695    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
696    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
697    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
698    /// produces a malformed query.
699    pub fn group_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
700        self.group_by_raw = Some((sql.to_owned(), binds));
701        self
702    }
703
704    /// Add a raw `ORDER BY` fragment with its own binds — the verbatim escape
705    /// hatch. SELECT-only.
706    ///
707    /// The fragment is appended after any structured [`Self::order_by`] terms
708    /// within the same `ORDER BY` clause (e.g. `ORDER BY "a" ASC, <raw>`); if no
709    /// structured terms are present it becomes the whole `ORDER BY <raw>`.
710    ///
711    /// # Warning: positional placeholder contract
712    ///
713    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
714    /// `binds` are appended to the running bind list in order. For
715    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
716    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
717    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
718    /// produces a malformed query.
719    pub fn order_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
720        self.order_by_raw = Some((sql.to_owned(), binds));
721        self
722    }
723
724    /// Add an `ORDER BY col <ord>` term. SELECT-only.
725    pub fn order_by(mut self, col: &str, ord: Order) -> Self {
726        self.orders.push((col.to_owned(), ord));
727        self
728    }
729
730    /// Add an `ORDER BY col ASC` term. SELECT-only.
731    pub fn order_by_asc(self, col: &str) -> Self {
732        self.order_by(col, Order::Asc)
733    }
734
735    /// Add an `ORDER BY col DESC` term. SELECT-only.
736    pub fn order_by_desc(self, col: &str) -> Self {
737        self.order_by(col, Order::Desc)
738    }
739
740    /// Set `LIMIT n` (bound as a placeholder). SELECT-only.
741    pub fn limit(mut self, n: i64) -> Self {
742        self.limit = Some(n);
743        self
744    }
745
746    /// Set `OFFSET n` (bound as a placeholder). SELECT-only.
747    ///
748    /// `offset` requires `limit`: compiling an offset without a limit panics
749    /// (`offset(...) requires limit(...)`), uniform across dialects since MySQL
750    /// rejects a bare `OFFSET`.
751    pub fn offset(mut self, n: i64) -> Self {
752        self.offset = Some(n);
753        self
754    }
755
756    fn push_join(
757        mut self,
758        kind: JoinKind,
759        table: &str,
760        f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
761    ) -> Self {
762        let on = f(JoinClause::new()).into_conds();
763        self.joins.push(Join {
764            kind,
765            table: table.to_owned(),
766            on,
767        });
768        self
769    }
770
771    /// `INNER JOIN table ON …` — conditions built by the closure.
772    ///
773    /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
774    pub fn join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
775        self.push_join(JoinKind::Inner, table, f)
776    }
777
778    /// `LEFT JOIN table ON …`. SELECT-only.
779    pub fn left_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
780        self.push_join(JoinKind::Left, table, f)
781    }
782
783    /// `RIGHT JOIN table ON …`. SELECT-only.
784    pub fn right_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
785        self.push_join(JoinKind::Right, table, f)
786    }
787
788    /// `FULL OUTER JOIN table ON …`. SELECT-only.
789    pub fn full_outer_join(
790        self,
791        table: &str,
792        f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
793    ) -> Self {
794        self.push_join(JoinKind::FullOuter, table, f)
795    }
796
797    /// `CROSS JOIN table` — takes **no** `ON` closure (a cross join has no
798    /// condition). SELECT-only.
799    pub fn cross_join(mut self, table: &str) -> Self {
800        self.joins.push(Join {
801            kind: JoinKind::Cross,
802            table: table.to_owned(),
803            on: Vec::new(),
804        });
805        self
806    }
807
808    /// `HAVING col op ?` — `col` is a real column/alias (escaped); value bound.
809    ///
810    /// For aggregate expressions like `COUNT(*) > ?`, use [`Self::having_raw`].
811    /// SELECT-only: ignored for INSERT/UPDATE/DELETE. Multiple HAVING terms are
812    /// joined by `AND`.
813    pub fn having(mut self, col: &str, op: &str, val: impl IntoBind) -> Self {
814        self.havings.push(Having::Col {
815            col: col.to_owned(),
816            op: op.to_owned(),
817            val: val.into_bind(),
818        });
819        self
820    }
821
822    /// Raw `HAVING` expression with its own binds — the verbatim escape hatch
823    /// for aggregates (e.g. `having_raw("COUNT(*) > ?", …)`).
824    ///
825    /// # Warning: positional placeholder contract
826    ///
827    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
828    /// `binds` are appended to the running bind list in order. For
829    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
830    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
831    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
832    /// produces a malformed query.
833    pub fn having_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
834        self.havings.push(Having::Raw {
835            sql: sql.to_owned(),
836            binds,
837        });
838        self
839    }
840
841    /// Add a `WITH name AS (query)` common table expression. SELECT-only.
842    ///
843    /// CTE bodies are compiled before the main query, so their binds (and pg
844    /// `$N` numbers) appear first.
845    pub fn with(mut self, name: &str, query: QueryBuilder<D>) -> Self {
846        self.ctes.push(Cte {
847            name: name.to_owned(),
848            recursive: false,
849            query,
850        });
851        self
852    }
853
854    /// Add a recursive CTE. If any CTE is recursive, the single `WITH` carries
855    /// `RECURSIVE`. SELECT-only.
856    pub fn with_recursive(mut self, name: &str, query: QueryBuilder<D>) -> Self {
857        self.ctes.push(Cte {
858            name: name.to_owned(),
859            recursive: true,
860            query,
861        });
862        self
863    }
864
865    /// Append a `UNION query` arm. SELECT-only.
866    pub fn union(mut self, query: QueryBuilder<D>) -> Self {
867        self.unions.push((false, query));
868        self
869    }
870
871    /// Append a `UNION ALL query` arm. SELECT-only.
872    pub fn union_all(mut self, query: QueryBuilder<D>) -> Self {
873        self.unions.push((true, query));
874        self
875    }
876
877    /// Conditionally apply `f` to the builder, keeping the chain intact.
878    ///
879    /// Returns `f(self)` when `cond` is true, otherwise `self` unchanged. This
880    /// lets you add clauses based on a runtime flag without breaking the
881    /// by-value chain.
882    ///
883    /// ```
884    /// # #[cfg(feature = "v2")] {
885    /// use chain_builder::v2::{Postgres, QueryBuilder};
886    /// let only_active = true;
887    /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
888    ///     .select(["id"])
889    ///     .when(only_active, |q| q.where_eq("status", "active"))
890    ///     .to_sql();
891    /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
892    /// # }
893    /// ```
894    pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
895        if cond {
896            f(self)
897        } else {
898            self
899        }
900    }
901
902    /// Apply `if_true` when `cond` holds, otherwise `if_false`, keeping the
903    /// chain intact.
904    ///
905    /// ```
906    /// # #[cfg(feature = "v2")] {
907    /// use chain_builder::v2::{Postgres, QueryBuilder};
908    /// let active = false;
909    /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
910    ///     .select(["id"])
911    ///     .when_else(
912    ///         active,
913    ///         |q| q.where_eq("status", "active"),
914    ///         |q| q.where_eq("status", "inactive"),
915    ///     )
916    ///     .to_sql();
917    /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
918    /// # }
919    /// ```
920    pub fn when_else(
921        self,
922        cond: bool,
923        if_true: impl FnOnce(Self) -> Self,
924        if_false: impl FnOnce(Self) -> Self,
925    ) -> Self {
926        if cond {
927            if_true(self)
928        } else {
929            if_false(self)
930        }
931    }
932
933    /// Apply `LIMIT`/`OFFSET` for a **1-based** page: row window
934    /// `[(page-1) * per_page, page * per_page)`.
935    ///
936    /// Equivalent to `self.limit(per_page).offset((page - 1).max(0) * per_page)`.
937    /// A `page < 1` is treated as page 1 (offset 0), so callers never get a
938    /// negative offset. SELECT-only, like [`Self::limit`] / [`Self::offset`].
939    ///
940    /// ```
941    /// # #[cfg(feature = "v2")] {
942    /// use chain_builder::v2::{Postgres, QueryBuilder, Value};
943    /// let (sql, binds) = QueryBuilder::<Postgres>::table("users")
944    ///     .select(["id"])
945    ///     .paginate(2, 10)
946    ///     .to_sql();
947    /// assert_eq!(sql, r#"SELECT "id" FROM "users" LIMIT $1 OFFSET $2"#);
948    /// assert_eq!(binds, vec![Value::I64(10), Value::I64(10)]);
949    /// # }
950    /// ```
951    pub fn paginate(self, page: i64, per_page: i64) -> Self {
952        self.limit(per_page).offset((page - 1).max(0) * per_page)
953    }
954
955    /// Compile to `(sql, binds)`.
956    pub fn to_sql(&self) -> (String, Vec<Value>) {
957        compile(self)
958    }
959}