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}