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