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/// A SQL aggregate function for the `select_*` aggregate helpers.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum AggFn {
180    /// `COUNT(...)`.
181    Count,
182    /// `SUM(...)`.
183    Sum,
184    /// `AVG(...)`.
185    Avg,
186    /// `MIN(...)`.
187    Min,
188    /// `MAX(...)`.
189    Max,
190}
191
192impl AggFn {
193    /// The uppercase SQL keyword for this function.
194    pub fn as_str(&self) -> &'static str {
195        match self {
196            AggFn::Count => "COUNT",
197            AggFn::Sum => "SUM",
198            AggFn::Avg => "AVG",
199            AggFn::Min => "MIN",
200            AggFn::Max => "MAX",
201        }
202    }
203}
204
205/// A structured `SELECT`-list expression (aggregate or aliased column).
206///
207/// The `col`/`alias` identifiers are stored raw and escaped at compile time
208/// (a `*` column is emitted unescaped, e.g. `COUNT(*)`). Backs the `select_count`
209/// / `select_sum` / … and `select_as` helpers.
210#[derive(Debug, Clone, PartialEq)]
211pub enum SelectExpr {
212    /// `FUNC(col)` with an optional `AS alias` — e.g. `COUNT(*) AS "total"`.
213    Agg {
214        /// The aggregate function.
215        func: AggFn,
216        /// Raw column identifier (escaped at compile time; `*` passed through).
217        col: String,
218        /// Optional alias (escaped at compile time).
219        alias: Option<String>,
220    },
221    /// `col AS alias` — both identifiers escaped at compile time.
222    ColAs {
223        /// Raw column identifier (escaped at compile time).
224        col: String,
225        /// Alias (escaped at compile time).
226        alias: String,
227    },
228}
229
230/// The strength of a row-locking clause.
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub enum LockStrength {
233    /// `FOR UPDATE` — exclusive lock.
234    Update,
235    /// `FOR SHARE` — shared lock.
236    Share,
237}
238
239/// The optional wait behavior of a row-locking clause.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum LockWait {
242    /// `SKIP LOCKED` — skip rows already locked.
243    SkipLocked,
244    /// `NOWAIT` — error immediately if a row is already locked.
245    NoWait,
246}
247
248/// A row-locking clause appended to a `SELECT` (`FOR UPDATE` / `FOR SHARE`,
249/// optionally `SKIP LOCKED` / `NOWAIT`).
250///
251/// Honored by Postgres / MySQL; a **silent no-op on SQLite** (see
252/// [`Dialect::supports_row_locking`](crate::Dialect::supports_row_locking)).
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub struct Lock {
255    /// `FOR UPDATE` vs `FOR SHARE`.
256    pub strength: LockStrength,
257    /// Optional `SKIP LOCKED` / `NOWAIT` modifier.
258    pub wait: Option<LockWait>,
259}
260
261/// Which kind of statement is being built.
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
263pub enum Method {
264    /// `SELECT`.
265    #[default]
266    Select,
267    /// `INSERT`.
268    Insert,
269    /// `UPDATE`.
270    Update,
271    /// `DELETE`.
272    Delete,
273}
274
275/// Typed, dialect-aware SQL query builder.
276#[derive(Debug, Clone, PartialEq)]
277pub struct QueryBuilder<D: Dialect> {
278    pub(crate) table: String,
279    /// Optional database/schema qualifier (multi-tenant: one connection, many DBs).
280    /// When set, prefixes the main table and join tables: `"db"."table"`.
281    pub(crate) db: Option<String>,
282    pub(crate) select_cols: Vec<String>,
283    /// Structured `SELECT` expressions (aggregates / aliased columns), escaped at
284    /// compile time. Rendered after `select_cols`. Backs `select_count`/… /
285    /// `select_as`.
286    pub(crate) select_exprs: Vec<SelectExpr>,
287    /// Raw `SELECT` expressions (verbatim, NOT escaped) with their own binds,
288    /// appended after `select_cols`. Backs `select_raw`.
289    pub(crate) select_raw: Vec<(String, Vec<Value>)>,
290    /// Subquery `SELECT` columns: `(alias, sub)` → `(<sub>) AS {esc alias}`,
291    /// appended after `select_cols` / `select_raw`. Backs `select_subquery`.
292    pub(crate) select_subqueries: Vec<(String, Box<QueryBuilder<D>>)>,
293    /// `SELECT DISTINCT` flag (raw; off by default for M1 byte-identity).
294    pub(crate) distinct: bool,
295    /// `SELECT DISTINCT ON (cols)` columns (raw; Postgres-only).
296    pub(crate) distinct_on: Vec<String>,
297    pub(crate) wheres: Vec<Predicate<D>>,
298    pub(crate) method: Method,
299    pub(crate) set: Vec<(String, Value)>,
300    /// Multi-row `INSERT` rows (empty unless `insert_many` was used). Each row is
301    /// a `(column, value)` list; columns come from the first row's sorted keys.
302    pub(crate) insert_rows: Vec<Vec<(String, Value)>>,
303    pub(crate) joins: Vec<Join>,
304    pub(crate) groups: Vec<String>,
305    /// Raw `GROUP BY` fragment (verbatim) with its own binds, appended after any
306    /// structured `groups`.
307    pub(crate) group_by_raw: Option<(String, Vec<Value>)>,
308    pub(crate) havings: Vec<Having>,
309    pub(crate) orders: Vec<(String, Order)>,
310    /// Raw `ORDER BY` fragment (verbatim) with its own binds, appended after any
311    /// structured `orders`.
312    pub(crate) order_by_raw: Option<(String, Vec<Value>)>,
313    pub(crate) limit: Option<i64>,
314    pub(crate) offset: Option<i64>,
315    pub(crate) ctes: Vec<Cte<D>>,
316    pub(crate) unions: Vec<(bool, QueryBuilder<D>)>,
317    /// `ON CONFLICT` spec for `INSERT` (ignored on UPDATE/DELETE).
318    pub(crate) on_conflict: Option<OnConflict>,
319    /// `RETURNING` column list (raw; `"*"` emitted unescaped).
320    pub(crate) returning: Vec<String>,
321    /// Row-locking clause (`FOR UPDATE`/`FOR SHARE`); SELECT-only, no-op on SQLite.
322    pub(crate) lock: Option<Lock>,
323    _marker: PhantomData<D>,
324}
325
326impl<D: Dialect> QueryBuilder<D> {
327    /// Start a query against `name`.
328    pub fn table(name: &str) -> Self {
329        Self {
330            table: name.to_owned(),
331            db: None,
332            select_cols: Vec::new(),
333            select_exprs: Vec::new(),
334            select_raw: Vec::new(),
335            select_subqueries: Vec::new(),
336            distinct: false,
337            distinct_on: Vec::new(),
338            wheres: Vec::new(),
339            method: Method::Select,
340            set: Vec::new(),
341            insert_rows: Vec::new(),
342            joins: Vec::new(),
343            groups: Vec::new(),
344            group_by_raw: None,
345            havings: Vec::new(),
346            orders: Vec::new(),
347            order_by_raw: None,
348            limit: None,
349            offset: None,
350            ctes: Vec::new(),
351            unions: Vec::new(),
352            on_conflict: None,
353            returning: Vec::new(),
354            lock: None,
355            _marker: PhantomData,
356        }
357    }
358
359    /// Set the database/schema qualifier (multi-tenant: one connection, many DBs).
360    ///
361    /// The name prefixes the main table and every join table, escaped per dialect:
362    /// `QueryBuilder::<Postgres>::table("users").db("mydb")` →
363    /// `… FROM "mydb"."users"`. Matches 1.x `db()`.
364    pub fn db(mut self, name: &str) -> Self {
365        self.db = Some(name.to_owned());
366        self
367    }
368
369    /// Restrict the selected columns. An empty list selects `*`.
370    pub fn select<I, S>(mut self, cols: I) -> Self
371    where
372        I: IntoIterator<Item = S>,
373        S: AsRef<str>,
374    {
375        self.select_cols = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
376        self
377    }
378
379    /// Add a raw `SELECT` expression (verbatim, NOT escaped) with optional binds
380    /// — the escape hatch for aggregates/functions like `COUNT(*)`.
381    ///
382    /// Appended to the column list after any [`Self::select`] columns. Multiple
383    /// calls accumulate.
384    ///
385    /// # Warning: positional placeholder contract
386    ///
387    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and any
388    /// `binds` are appended to the running bind list in order. For **Postgres**,
389    /// the caller MUST write `$N` numbers matching the actual bind position. For
390    /// MySQL/SQLite use `?`.
391    pub fn select_raw(mut self, sql: &str, binds: Option<Vec<Value>>) -> Self {
392        self.select_raw
393            .push((sql.to_owned(), binds.unwrap_or_default()));
394        self
395    }
396
397    /// Add a subquery `SELECT` column: emits `(<sub>) AS {alias}` after the
398    /// regular columns and any [`Self::select_raw`] expressions.
399    ///
400    /// The subquery is compiled with placeholder continuity (its binds appear in
401    /// `$N` order at the point it is emitted — before the `WHERE` clause, since
402    /// the SELECT list is rendered first). SELECT-only.
403    pub fn select_subquery(mut self, alias: &str, sub: QueryBuilder<D>) -> Self {
404        self.select_subqueries
405            .push((alias.to_owned(), Box::new(sub)));
406        self
407    }
408
409    fn push_agg(mut self, func: AggFn, col: &str, alias: Option<&str>) -> Self {
410        self.select_exprs.push(SelectExpr::Agg {
411            func,
412            col: col.to_owned(),
413            alias: alias.map(|a| a.to_owned()),
414        });
415        self
416    }
417
418    /// Add `COUNT(col)` to the SELECT list (`col == "*"` → `COUNT(*)`).
419    pub fn select_count(self, col: &str) -> Self {
420        self.push_agg(AggFn::Count, col, None)
421    }
422
423    /// Add `COUNT(col) AS alias` (both identifiers escaped; `*` passed through).
424    pub fn select_count_as(self, col: &str, alias: &str) -> Self {
425        self.push_agg(AggFn::Count, col, Some(alias))
426    }
427
428    /// Add `SUM(col)` to the SELECT list.
429    pub fn select_sum(self, col: &str) -> Self {
430        self.push_agg(AggFn::Sum, col, None)
431    }
432
433    /// Add `SUM(col) AS alias`.
434    pub fn select_sum_as(self, col: &str, alias: &str) -> Self {
435        self.push_agg(AggFn::Sum, col, Some(alias))
436    }
437
438    /// Add `AVG(col)` to the SELECT list.
439    pub fn select_avg(self, col: &str) -> Self {
440        self.push_agg(AggFn::Avg, col, None)
441    }
442
443    /// Add `AVG(col) AS alias`.
444    pub fn select_avg_as(self, col: &str, alias: &str) -> Self {
445        self.push_agg(AggFn::Avg, col, Some(alias))
446    }
447
448    /// Add `MIN(col)` to the SELECT list.
449    pub fn select_min(self, col: &str) -> Self {
450        self.push_agg(AggFn::Min, col, None)
451    }
452
453    /// Add `MIN(col) AS alias`.
454    pub fn select_min_as(self, col: &str, alias: &str) -> Self {
455        self.push_agg(AggFn::Min, col, Some(alias))
456    }
457
458    /// Add `MAX(col)` to the SELECT list.
459    pub fn select_max(self, col: &str) -> Self {
460        self.push_agg(AggFn::Max, col, None)
461    }
462
463    /// Add `MAX(col) AS alias`.
464    pub fn select_max_as(self, col: &str, alias: &str) -> Self {
465        self.push_agg(AggFn::Max, col, Some(alias))
466    }
467
468    /// Add `col AS alias` to the SELECT list (both identifiers escaped).
469    pub fn select_as(mut self, col: &str, alias: &str) -> Self {
470        self.select_exprs.push(SelectExpr::ColAs {
471            col: col.to_owned(),
472            alias: alias.to_owned(),
473        });
474        self
475    }
476
477    /// Emit `SELECT DISTINCT …` (all dialects).
478    pub fn distinct(mut self) -> Self {
479        self.distinct = true;
480        self
481    }
482
483    /// Emit `SELECT DISTINCT ON (cols) …` — **Postgres only**.
484    ///
485    /// `cols` are raw identifiers (escaped at compile time). Compiling against a
486    /// dialect without `DISTINCT ON` support panics
487    /// (`DISTINCT ON requires PostgreSQL`).
488    pub fn distinct_on<I, S>(mut self, cols: I) -> Self
489    where
490        I: IntoIterator<Item = S>,
491        S: AsRef<str>,
492    {
493        self.distinct_on = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
494        self.distinct = true;
495        self
496    }
497
498    /// `col ILIKE val` — dialect-aware case-insensitive match.
499    ///
500    /// On **Postgres** this compiles to the native `{col} ILIKE {ph}`. On
501    /// MySQL/SQLite (no native `ILIKE`) it compiles to
502    /// `LOWER({col}) LIKE LOWER({ph})`.
503    pub fn where_ilike(mut self, col: &str, val: impl IntoBind) -> Self {
504        self.wheres.push(Predicate::ILike {
505            col: col.to_owned(),
506            val: val.into_bind(),
507        });
508        self
509    }
510
511    /// `col @> val` — JSONB containment.
512    ///
513    /// **Postgres-specific:** the `@>` operator is emitted verbatim for all
514    /// dialects, but is only meaningful on Postgres `jsonb` columns. `val` is
515    /// typically a JSON text string (or `Value::Json` behind the `json`
516    /// feature).
517    pub fn where_jsonb_contains(mut self, col: &str, val: impl IntoBind) -> Self {
518        self.wheres.push(Predicate::JsonContains {
519            col: col.to_owned(),
520            val: val.into_bind(),
521        });
522        self
523    }
524
525    fn binary(mut self, col: &str, op: &'static str, val: impl IntoBind) -> Self {
526        self.wheres.push(Predicate::Binary {
527            col: col.to_owned(),
528            op,
529            val: val.into_bind(),
530        });
531        self
532    }
533
534    /// `col = val`.
535    pub fn where_eq(self, col: &str, val: impl IntoBind) -> Self {
536        self.binary(col, "=", val)
537    }
538
539    /// `col != val`.
540    pub fn where_ne(self, col: &str, val: impl IntoBind) -> Self {
541        self.binary(col, "!=", val)
542    }
543
544    /// `col > val`.
545    pub fn where_gt(self, col: &str, val: impl IntoBind) -> Self {
546        self.binary(col, ">", val)
547    }
548
549    /// `col >= val`.
550    pub fn where_gte(self, col: &str, val: impl IntoBind) -> Self {
551        self.binary(col, ">=", val)
552    }
553
554    /// `col < val`.
555    pub fn where_lt(self, col: &str, val: impl IntoBind) -> Self {
556        self.binary(col, "<", val)
557    }
558
559    /// `col <= val`.
560    pub fn where_lte(self, col: &str, val: impl IntoBind) -> Self {
561        self.binary(col, "<=", val)
562    }
563
564    /// `col LIKE val`.
565    pub fn where_like(self, col: &str, val: impl IntoBind) -> Self {
566        self.binary(col, "LIKE", val)
567    }
568
569    fn in_(mut self, col: &str, neg: bool, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
570        self.wheres.push(Predicate::In {
571            col: col.to_owned(),
572            neg,
573            vals: vals.into_iter().map(IntoBind::into_bind).collect(),
574        });
575        self
576    }
577
578    /// `col IN (...)`.
579    pub fn where_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
580        self.in_(col, false, vals)
581    }
582
583    /// `col NOT IN (...)`.
584    pub fn where_not_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
585        self.in_(col, true, vals)
586    }
587
588    fn null(mut self, col: &str, neg: bool) -> Self {
589        self.wheres.push(Predicate::Null {
590            col: col.to_owned(),
591            neg,
592        });
593        self
594    }
595
596    /// `col IS NULL`.
597    pub fn where_null(self, col: &str) -> Self {
598        self.null(col, false)
599    }
600
601    /// `col IS NOT NULL`.
602    pub fn where_not_null(self, col: &str) -> Self {
603        self.null(col, true)
604    }
605
606    /// `col BETWEEN lo AND hi`.
607    pub fn where_between(mut self, col: &str, lo: impl IntoBind, hi: impl IntoBind) -> Self {
608        self.wheres.push(Predicate::Between {
609            col: col.to_owned(),
610            lo: lo.into_bind(),
611            hi: hi.into_bind(),
612        });
613        self
614    }
615
616    /// Raw SQL predicate with its own binds — the verbatim escape hatch.
617    ///
618    /// # Warning: positional placeholder contract
619    ///
620    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
621    /// `binds` are appended to the running bind list in order. For
622    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
623    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
624    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
625    /// produces a malformed query.
626    pub fn where_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
627        self.wheres.push(Predicate::Raw {
628            sql: sql.to_owned(),
629            binds,
630        });
631        self
632    }
633
634    /// `lhs op rhs` — compare two column identifiers (both escaped at compile
635    /// time), no bind. e.g. `where_column("orders.user_id", "=", "users.id")`.
636    pub fn where_column(mut self, lhs: &str, op: &'static str, rhs: &str) -> Self {
637        self.wheres.push(Predicate::Column {
638            lhs: lhs.to_owned(),
639            op,
640            rhs: rhs.to_owned(),
641        });
642        self
643    }
644
645    /// `EXISTS (subquery)` — takes an already-built sub-builder by value
646    /// (mirrors [`Self::union`] / [`Self::with`]). The sub-query is compiled
647    /// with placeholder continuity.
648    pub fn where_exists(mut self, sub: QueryBuilder<D>) -> Self {
649        self.wheres.push(Predicate::Exists {
650            neg: false,
651            sub: Box::new(sub),
652        });
653        self
654    }
655
656    /// `NOT EXISTS (subquery)`. See [`Self::where_exists`].
657    pub fn where_not_exists(mut self, sub: QueryBuilder<D>) -> Self {
658        self.wheres.push(Predicate::Exists {
659            neg: true,
660            sub: Box::new(sub),
661        });
662        self
663    }
664
665    /// `col IN (subquery)` — takes an already-built sub-builder by value. The
666    /// sub-query is compiled with placeholder continuity.
667    pub fn where_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
668        self.wheres.push(Predicate::InSubquery {
669            col: col.to_owned(),
670            neg: false,
671            sub: Box::new(sub),
672        });
673        self
674    }
675
676    /// `col NOT IN (subquery)`. See [`Self::where_in_subquery`].
677    pub fn where_not_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
678        self.wheres.push(Predicate::InSubquery {
679            col: col.to_owned(),
680            neg: true,
681            sub: Box::new(sub),
682        });
683        self
684    }
685
686    fn group(
687        mut self,
688        outer_conj: Conj,
689        f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>,
690    ) -> Self {
691        let preds = f(WhereBuilder::new()).into_preds();
692        self.wheres.push(Predicate::Group { outer_conj, preds });
693        self
694    }
695
696    /// Add a parenthesized `AND (...)` group built by the closure.
697    pub fn and_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
698        self.group(Conj::And, f)
699    }
700
701    /// Add a parenthesized `OR (...)` group built by the closure.
702    pub fn or_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
703        self.group(Conj::Or, f)
704    }
705
706    /// Build an `INSERT` from a single row of `(column, value)` pairs.
707    pub fn insert<K, V, I>(mut self, row: I) -> Self
708    where
709        K: AsRef<str>,
710        V: IntoBind,
711        I: IntoIterator<Item = (K, V)>,
712    {
713        self.method = Method::Insert;
714        self.set = row
715            .into_iter()
716            .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
717            .collect();
718        self
719    }
720
721    /// Build a multi-row `INSERT` from an iterator of rows, each a sequence of
722    /// `(column, value)` pairs.
723    ///
724    /// The inserted column set is taken from the **first** row's keys (sorted, as
725    /// with [`Self::insert`]). For each subsequent row, a value is bound for every
726    /// column in that set; a key **missing** in a later row binds `Value::Null`
727    /// rather than panicking (DoS-safe, matching the 1.x hardening). Composes with
728    /// `on_conflict_*` and `returning`.
729    pub fn insert_many<K, V, R, Rows>(mut self, rows: Rows) -> Self
730    where
731        K: AsRef<str>,
732        V: IntoBind,
733        R: IntoIterator<Item = (K, V)>,
734        Rows: IntoIterator<Item = R>,
735    {
736        self.method = Method::Insert;
737        self.insert_rows = rows
738            .into_iter()
739            .map(|row| {
740                row.into_iter()
741                    .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
742                    .collect()
743            })
744            .collect();
745        self
746    }
747
748    /// Build an `UPDATE` from `(column, value)` pairs. WHERE still applies.
749    pub fn update<K, V, I>(mut self, set: I) -> Self
750    where
751        K: AsRef<str>,
752        V: IntoBind,
753        I: IntoIterator<Item = (K, V)>,
754    {
755        self.method = Method::Update;
756        self.set = set
757            .into_iter()
758            .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
759            .collect();
760        self
761    }
762
763    /// Build a `DELETE`. WHERE still applies.
764    pub fn delete(mut self) -> Self {
765        self.method = Method::Delete;
766        self
767    }
768
769    /// On conflict, skip the row (`INSERT`-only; ignored on UPDATE/DELETE).
770    ///
771    /// `targets` are the conflict-target columns (may be empty).
772    ///
773    /// - **Postgres / SQLite:** emits `ON CONFLICT ({targets}) DO NOTHING`, or
774    ///   bare `ON CONFLICT DO NOTHING` when `targets` is empty.
775    /// - **MySQL:** emits `INSERT IGNORE INTO …` (no trailing clause). Note that
776    ///   `IGNORE` suppresses *more* than duplicate-key errors (also truncation
777    ///   and bad-value coercion) — broader than pg/sqlite `DO NOTHING`.
778    pub fn on_conflict_do_nothing<I, S>(mut self, targets: I) -> Self
779    where
780        I: IntoIterator<Item = S>,
781        S: AsRef<str>,
782    {
783        self.on_conflict = Some(OnConflict {
784            targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
785            action: ConflictAction::DoNothing,
786        });
787        self
788    }
789
790    /// On conflict, update the non-target inserted columns from the proposed row
791    /// (`INSERT`-only; ignored on UPDATE/DELETE).
792    ///
793    /// - **Postgres / SQLite:** emits
794    ///   `ON CONFLICT ({targets}) DO UPDATE SET {c} = EXCLUDED.{c}, …` for every
795    ///   inserted column *except* the conflict targets. If `targets` is empty or
796    ///   covers all inserted columns (empty SET list), falls back to the
797    ///   `DO NOTHING` rendering (pg/sqlite require a target for `DO UPDATE`).
798    /// - **MySQL:** the explicit `targets` are **ignored** (MySQL uses its own
799    ///   unique/primary keys); emits
800    ///   `ON DUPLICATE KEY UPDATE {c} = VALUES({c}), …` for *all* inserted
801    ///   columns. `VALUES()` is used for MySQL 5.7/8.x compatibility. Including a
802    ///   PK column in the insert set yields a redundant-but-harmless
803    ///   `pk = VALUES(pk)`.
804    pub fn on_conflict_merge<I, S>(mut self, targets: I) -> Self
805    where
806        I: IntoIterator<Item = S>,
807        S: AsRef<str>,
808    {
809        self.on_conflict = Some(OnConflict {
810            targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
811            action: ConflictAction::Merge,
812        });
813        self
814    }
815
816    /// Add a `RETURNING` column list. Works on INSERT / UPDATE / DELETE for
817    /// Postgres and SQLite; a `"*"` column is emitted unescaped (`RETURNING *`).
818    ///
819    /// On **MySQL** this is a silent no-op (MySQL has no `RETURNING`). On
820    /// **SQLite** `RETURNING` requires SQLite ≥ 3.35.0 (2021); `supports_returning()`
821    /// is a compile-time dialect flag, not a runtime version check.
822    pub fn returning<I, S>(mut self, cols: I) -> Self
823    where
824        I: IntoIterator<Item = S>,
825        S: AsRef<str>,
826    {
827        self.returning = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
828        self
829    }
830
831    /// Add `GROUP BY` columns (raw owned identifiers, escaped at compile time).
832    ///
833    /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
834    pub fn group_by<I, S>(mut self, cols: I) -> Self
835    where
836        I: IntoIterator<Item = S>,
837        S: AsRef<str>,
838    {
839        self.groups
840            .extend(cols.into_iter().map(|c| c.as_ref().to_owned()));
841        self
842    }
843
844    /// Add a raw `GROUP BY` fragment with its own binds — the verbatim escape
845    /// hatch. SELECT-only.
846    ///
847    /// The fragment is appended after any structured [`Self::group_by`] columns
848    /// within the same `GROUP BY` clause (e.g. `GROUP BY "a", <raw>`); if no
849    /// structured columns are present it becomes the whole `GROUP BY <raw>`.
850    ///
851    /// # Warning: positional placeholder contract
852    ///
853    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
854    /// `binds` are appended to the running bind list in order. For
855    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
856    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
857    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
858    /// produces a malformed query.
859    pub fn group_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
860        self.group_by_raw = Some((sql.to_owned(), binds));
861        self
862    }
863
864    /// Add a raw `ORDER BY` fragment with its own binds — the verbatim escape
865    /// hatch. SELECT-only.
866    ///
867    /// The fragment is appended after any structured [`Self::order_by`] terms
868    /// within the same `ORDER BY` clause (e.g. `ORDER BY "a" ASC, <raw>`); if no
869    /// structured terms are present it becomes the whole `ORDER BY <raw>`.
870    ///
871    /// # Warning: positional placeholder contract
872    ///
873    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
874    /// `binds` are appended to the running bind list in order. For
875    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
876    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
877    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
878    /// produces a malformed query.
879    pub fn order_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
880        self.order_by_raw = Some((sql.to_owned(), binds));
881        self
882    }
883
884    /// Add an `ORDER BY col <ord>` term. SELECT-only.
885    pub fn order_by(mut self, col: &str, ord: Order) -> Self {
886        self.orders.push((col.to_owned(), ord));
887        self
888    }
889
890    /// Add an `ORDER BY col ASC` term. SELECT-only.
891    pub fn order_by_asc(self, col: &str) -> Self {
892        self.order_by(col, Order::Asc)
893    }
894
895    /// Add an `ORDER BY col DESC` term. SELECT-only.
896    pub fn order_by_desc(self, col: &str) -> Self {
897        self.order_by(col, Order::Desc)
898    }
899
900    /// Set `LIMIT n` (bound as a placeholder). SELECT-only.
901    pub fn limit(mut self, n: i64) -> Self {
902        self.limit = Some(n);
903        self
904    }
905
906    /// Set `OFFSET n` (bound as a placeholder). SELECT-only.
907    ///
908    /// `offset` requires `limit`: compiling an offset without a limit panics
909    /// (`offset(...) requires limit(...)`), uniform across dialects since MySQL
910    /// rejects a bare `OFFSET`.
911    pub fn offset(mut self, n: i64) -> Self {
912        self.offset = Some(n);
913        self
914    }
915
916    /// Lock selected rows with `FOR UPDATE`. SELECT-only.
917    ///
918    /// Honored by Postgres / MySQL; a **silent no-op on SQLite**. Preserves any
919    /// `SKIP LOCKED` / `NOWAIT` modifier already set.
920    pub fn for_update(mut self) -> Self {
921        let wait = self.lock.and_then(|l| l.wait);
922        self.lock = Some(Lock {
923            strength: LockStrength::Update,
924            wait,
925        });
926        self
927    }
928
929    /// Lock selected rows with `FOR SHARE`. SELECT-only.
930    ///
931    /// Honored by Postgres / MySQL; a **silent no-op on SQLite**. Preserves any
932    /// `SKIP LOCKED` / `NOWAIT` modifier already set.
933    pub fn for_share(mut self) -> Self {
934        let wait = self.lock.and_then(|l| l.wait);
935        self.lock = Some(Lock {
936            strength: LockStrength::Share,
937            wait,
938        });
939        self
940    }
941
942    /// Add `SKIP LOCKED` to the row-locking clause (skip already-locked rows).
943    ///
944    /// If no lock strength was set yet, defaults to `FOR UPDATE`. SELECT-only;
945    /// no-op on SQLite.
946    pub fn skip_locked(mut self) -> Self {
947        let strength = self
948            .lock
949            .map(|l| l.strength)
950            .unwrap_or(LockStrength::Update);
951        self.lock = Some(Lock {
952            strength,
953            wait: Some(LockWait::SkipLocked),
954        });
955        self
956    }
957
958    /// Add `NOWAIT` to the row-locking clause (error if a row is already locked).
959    ///
960    /// If no lock strength was set yet, defaults to `FOR UPDATE`. SELECT-only;
961    /// no-op on SQLite.
962    pub fn no_wait(mut self) -> Self {
963        let strength = self
964            .lock
965            .map(|l| l.strength)
966            .unwrap_or(LockStrength::Update);
967        self.lock = Some(Lock {
968            strength,
969            wait: Some(LockWait::NoWait),
970        });
971        self
972    }
973
974    fn push_join(
975        mut self,
976        kind: JoinKind,
977        table: &str,
978        f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
979    ) -> Self {
980        let on = f(JoinClause::new()).into_conds();
981        self.joins.push(Join {
982            kind,
983            table: table.to_owned(),
984            on,
985        });
986        self
987    }
988
989    /// `INNER JOIN table ON …` — conditions built by the closure.
990    ///
991    /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
992    pub fn join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
993        self.push_join(JoinKind::Inner, table, f)
994    }
995
996    /// `LEFT JOIN table ON …`. SELECT-only.
997    pub fn left_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
998        self.push_join(JoinKind::Left, table, f)
999    }
1000
1001    /// `RIGHT JOIN table ON …`. SELECT-only.
1002    pub fn right_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
1003        self.push_join(JoinKind::Right, table, f)
1004    }
1005
1006    /// `FULL OUTER JOIN table ON …`. SELECT-only.
1007    pub fn full_outer_join(
1008        self,
1009        table: &str,
1010        f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
1011    ) -> Self {
1012        self.push_join(JoinKind::FullOuter, table, f)
1013    }
1014
1015    /// `CROSS JOIN table` — takes **no** `ON` closure (a cross join has no
1016    /// condition). SELECT-only.
1017    pub fn cross_join(mut self, table: &str) -> Self {
1018        self.joins.push(Join {
1019            kind: JoinKind::Cross,
1020            table: table.to_owned(),
1021            on: Vec::new(),
1022        });
1023        self
1024    }
1025
1026    /// `HAVING col op ?` — `col` is a real column/alias (escaped); value bound.
1027    ///
1028    /// For aggregate expressions like `COUNT(*) > ?`, use [`Self::having_raw`].
1029    /// SELECT-only: ignored for INSERT/UPDATE/DELETE. Multiple HAVING terms are
1030    /// joined by `AND`.
1031    pub fn having(mut self, col: &str, op: &str, val: impl IntoBind) -> Self {
1032        self.havings.push(Having::Col {
1033            col: col.to_owned(),
1034            op: op.to_owned(),
1035            val: val.into_bind(),
1036        });
1037        self
1038    }
1039
1040    /// Raw `HAVING` expression with its own binds — the verbatim escape hatch
1041    /// for aggregates (e.g. `having_raw("COUNT(*) > ?", …)`).
1042    ///
1043    /// # Warning: positional placeholder contract
1044    ///
1045    /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
1046    /// `binds` are appended to the running bind list in order. For
1047    /// **Postgres**, the caller MUST write `$N` numbers matching the actual
1048    /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
1049    /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
1050    /// produces a malformed query.
1051    pub fn having_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
1052        self.havings.push(Having::Raw {
1053            sql: sql.to_owned(),
1054            binds,
1055        });
1056        self
1057    }
1058
1059    /// Add a `WITH name AS (query)` common table expression. SELECT-only.
1060    ///
1061    /// CTE bodies are compiled before the main query, so their binds (and pg
1062    /// `$N` numbers) appear first.
1063    pub fn with(mut self, name: &str, query: QueryBuilder<D>) -> Self {
1064        self.ctes.push(Cte {
1065            name: name.to_owned(),
1066            recursive: false,
1067            query,
1068        });
1069        self
1070    }
1071
1072    /// Add a recursive CTE. If any CTE is recursive, the single `WITH` carries
1073    /// `RECURSIVE`. SELECT-only.
1074    pub fn with_recursive(mut self, name: &str, query: QueryBuilder<D>) -> Self {
1075        self.ctes.push(Cte {
1076            name: name.to_owned(),
1077            recursive: true,
1078            query,
1079        });
1080        self
1081    }
1082
1083    /// Append a `UNION query` arm. SELECT-only.
1084    pub fn union(mut self, query: QueryBuilder<D>) -> Self {
1085        self.unions.push((false, query));
1086        self
1087    }
1088
1089    /// Append a `UNION ALL query` arm. SELECT-only.
1090    pub fn union_all(mut self, query: QueryBuilder<D>) -> Self {
1091        self.unions.push((true, query));
1092        self
1093    }
1094
1095    /// Conditionally apply `f` to the builder, keeping the chain intact.
1096    ///
1097    /// Returns `f(self)` when `cond` is true, otherwise `self` unchanged. This
1098    /// lets you add clauses based on a runtime flag without breaking the
1099    /// by-value chain.
1100    ///
1101    /// ```
1102    /// # #[cfg(feature = "v2")] {
1103    /// use chain_builder::v2::{Postgres, QueryBuilder};
1104    /// let only_active = true;
1105    /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
1106    ///     .select(["id"])
1107    ///     .when(only_active, |q| q.where_eq("status", "active"))
1108    ///     .to_sql();
1109    /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
1110    /// # }
1111    /// ```
1112    pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
1113        if cond {
1114            f(self)
1115        } else {
1116            self
1117        }
1118    }
1119
1120    /// Apply `if_true` when `cond` holds, otherwise `if_false`, keeping the
1121    /// chain intact.
1122    ///
1123    /// ```
1124    /// # #[cfg(feature = "v2")] {
1125    /// use chain_builder::v2::{Postgres, QueryBuilder};
1126    /// let active = false;
1127    /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
1128    ///     .select(["id"])
1129    ///     .when_else(
1130    ///         active,
1131    ///         |q| q.where_eq("status", "active"),
1132    ///         |q| q.where_eq("status", "inactive"),
1133    ///     )
1134    ///     .to_sql();
1135    /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
1136    /// # }
1137    /// ```
1138    pub fn when_else(
1139        self,
1140        cond: bool,
1141        if_true: impl FnOnce(Self) -> Self,
1142        if_false: impl FnOnce(Self) -> Self,
1143    ) -> Self {
1144        if cond {
1145            if_true(self)
1146        } else {
1147            if_false(self)
1148        }
1149    }
1150
1151    /// Apply `LIMIT`/`OFFSET` for a **1-based** page: row window
1152    /// `[(page-1) * per_page, page * per_page)`.
1153    ///
1154    /// Equivalent to `self.limit(per_page).offset((page - 1).max(0) * per_page)`.
1155    /// A `page < 1` is treated as page 1 (offset 0), so callers never get a
1156    /// negative offset. SELECT-only, like [`Self::limit`] / [`Self::offset`].
1157    ///
1158    /// ```
1159    /// # #[cfg(feature = "v2")] {
1160    /// use chain_builder::v2::{Postgres, QueryBuilder, Value};
1161    /// let (sql, binds) = QueryBuilder::<Postgres>::table("users")
1162    ///     .select(["id"])
1163    ///     .paginate(2, 10)
1164    ///     .to_sql();
1165    /// assert_eq!(sql, r#"SELECT "id" FROM "users" LIMIT $1 OFFSET $2"#);
1166    /// assert_eq!(binds, vec![Value::I64(10), Value::I64(10)]);
1167    /// # }
1168    /// ```
1169    pub fn paginate(self, page: i64, per_page: i64) -> Self {
1170        self.limit(per_page).offset((page - 1).max(0) * per_page)
1171    }
1172
1173    /// Compile to `(sql, binds)`.
1174    pub fn to_sql(&self) -> (String, Vec<Value>) {
1175        compile(self)
1176    }
1177}