Skip to main content

chain_builder/
builder.rs

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