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/// A structured or raw `SET` expression for UPDATE. Backs
232/// [`QueryBuilder::set_raw`] / [`QueryBuilder::increment`] /
233/// [`QueryBuilder::decrement`]. Rendered after the (sorted) structured
234/// `update()` columns, in call order.
235#[derive(Debug, Clone, PartialEq)]
236pub(crate) enum SetExpr {
237 /// `{esc col} = {expr verbatim}` with its own binds (NOT escaped or
238 /// renumbered — the `where_raw` contract).
239 Raw {
240 /// Column identifier; escaped in compile.rs.
241 col: String,
242 /// Verbatim RHS expression.
243 expr: String,
244 /// Binds appended to the running bind list in order.
245 binds: Vec<Value>,
246 },
247 /// `{esc col} = {esc col} {+|-} {placeholder}` — structured step.
248 Step {
249 /// Column identifier; escaped in compile.rs.
250 col: String,
251 /// The step amount, bound via the normal placeholder machinery.
252 by: Value,
253 /// `false` → `+` (increment); `true` → `-` (decrement).
254 neg: bool,
255 },
256}
257
258/// The strength of a row-locking clause.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum LockStrength {
261 /// `FOR UPDATE` — exclusive lock.
262 Update,
263 /// `FOR SHARE` — shared lock.
264 Share,
265}
266
267/// The optional wait behavior of a row-locking clause.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum LockWait {
270 /// `SKIP LOCKED` — skip rows already locked.
271 SkipLocked,
272 /// `NOWAIT` — error immediately if a row is already locked.
273 NoWait,
274}
275
276/// A row-locking clause appended to a `SELECT` (`FOR UPDATE` / `FOR SHARE`,
277/// optionally `SKIP LOCKED` / `NOWAIT`).
278///
279/// Honored by Postgres / MySQL; a **silent no-op on SQLite** (see
280/// [`Dialect::supports_row_locking`](crate::Dialect::supports_row_locking)).
281/// Compiling panics if a lock is attached to a non-`SELECT` statement (a
282/// dangerous silent no-op otherwise) or combined with `UNION` on a locking
283/// dialect (invalid SQL).
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub struct Lock {
286 /// `FOR UPDATE` vs `FOR SHARE`.
287 pub strength: LockStrength,
288 /// Optional `SKIP LOCKED` / `NOWAIT` modifier.
289 pub wait: Option<LockWait>,
290}
291
292/// Which kind of statement is being built.
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
294pub enum Method {
295 /// `SELECT`.
296 #[default]
297 Select,
298 /// `INSERT`.
299 Insert,
300 /// `UPDATE`.
301 Update,
302 /// `DELETE`.
303 Delete,
304}
305
306/// Typed, dialect-aware SQL query builder.
307#[derive(Debug, Clone, PartialEq)]
308pub struct QueryBuilder<D: Dialect> {
309 pub(crate) table: String,
310 /// Optional database/schema qualifier (multi-tenant: one connection, many DBs).
311 /// When set, prefixes the main table and join tables: `"db"."table"`.
312 pub(crate) db: Option<String>,
313 pub(crate) select_cols: Vec<String>,
314 /// Structured `SELECT` expressions (aggregates / aliased columns), escaped at
315 /// compile time. Rendered after `select_cols`. Backs `select_count`/… /
316 /// `select_as`.
317 pub(crate) select_exprs: Vec<SelectExpr>,
318 /// Raw `SELECT` expressions (verbatim, NOT escaped) with their own binds,
319 /// appended after `select_cols`. Backs `select_raw`.
320 pub(crate) select_raw: Vec<(String, Vec<Value>)>,
321 /// Subquery `SELECT` columns: `(alias, sub)` → `(<sub>) AS {esc alias}`,
322 /// appended after `select_cols` / `select_raw`. Backs `select_subquery`.
323 pub(crate) select_subqueries: Vec<(String, Box<QueryBuilder<D>>)>,
324 /// `SELECT DISTINCT` flag (raw; off by default for M1 byte-identity).
325 pub(crate) distinct: bool,
326 /// `SELECT DISTINCT ON (cols)` columns (raw; Postgres-only).
327 pub(crate) distinct_on: Vec<String>,
328 pub(crate) wheres: Vec<Predicate<D>>,
329 pub(crate) method: Method,
330 pub(crate) set: Vec<(String, Value)>,
331 /// UPDATE `SET` expressions, rendered after the sorted `set` pairs in
332 /// call order. Backs `set_raw`/`increment`/`decrement`.
333 pub(crate) set_exprs: Vec<SetExpr>,
334 /// Multi-row `INSERT` rows (empty unless `insert_many` was used). Each row is
335 /// a `(column, value)` list; columns come from the first row's sorted keys.
336 pub(crate) insert_rows: Vec<Vec<(String, Value)>>,
337 pub(crate) joins: Vec<Join>,
338 pub(crate) groups: Vec<String>,
339 /// Raw `GROUP BY` fragment (verbatim) with its own binds, appended after any
340 /// structured `groups`.
341 pub(crate) group_by_raw: Option<(String, Vec<Value>)>,
342 pub(crate) havings: Vec<Having>,
343 pub(crate) orders: Vec<(String, Order)>,
344 /// Raw `ORDER BY` fragment (verbatim) with its own binds, appended after any
345 /// structured `orders`.
346 pub(crate) order_by_raw: Option<(String, Vec<Value>)>,
347 pub(crate) limit: Option<i64>,
348 pub(crate) offset: Option<i64>,
349 pub(crate) ctes: Vec<Cte<D>>,
350 pub(crate) unions: Vec<(bool, QueryBuilder<D>)>,
351 /// `ON CONFLICT` spec for `INSERT` (ignored on UPDATE/DELETE).
352 pub(crate) on_conflict: Option<OnConflict>,
353 /// `RETURNING` column list (raw; `"*"` emitted unescaped).
354 pub(crate) returning: Vec<String>,
355 /// Row-locking clause (`FOR UPDATE`/`FOR SHARE`); SELECT-only, no-op on SQLite.
356 pub(crate) lock: Option<Lock>,
357 /// First misuse detected by a builder method (e.g. a disallowed `having()`
358 /// operator). Deferred instead of panicking mid-chain; surfaced by
359 /// [`Self::try_to_sql`] as `Err` (and by [`Self::to_sql`] as a panic).
360 pub(crate) error: Option<BuildError>,
361 _marker: PhantomData<D>,
362}
363
364impl<D: Dialect> QueryBuilder<D> {
365 /// Start a query against `name`.
366 pub fn table(name: &str) -> Self {
367 Self {
368 table: name.to_owned(),
369 db: None,
370 select_cols: Vec::new(),
371 select_exprs: Vec::new(),
372 select_raw: Vec::new(),
373 select_subqueries: Vec::new(),
374 distinct: false,
375 distinct_on: Vec::new(),
376 wheres: Vec::new(),
377 method: Method::Select,
378 set: Vec::new(),
379 set_exprs: Vec::new(),
380 insert_rows: Vec::new(),
381 joins: Vec::new(),
382 groups: Vec::new(),
383 group_by_raw: None,
384 havings: Vec::new(),
385 orders: Vec::new(),
386 order_by_raw: None,
387 limit: None,
388 offset: None,
389 ctes: Vec::new(),
390 unions: Vec::new(),
391 on_conflict: None,
392 returning: Vec::new(),
393 lock: None,
394 error: None,
395 _marker: PhantomData,
396 }
397 }
398
399 /// Set the database/schema qualifier (multi-tenant: one connection, many DBs).
400 ///
401 /// The name prefixes the main table and every join table, escaped per dialect:
402 /// `QueryBuilder::<Postgres>::table("users").db("mydb")` →
403 /// `… FROM "mydb"."users"`. Matches 1.x `db()`.
404 pub fn db(mut self, name: &str) -> Self {
405 self.db = Some(name.to_owned());
406 self
407 }
408
409 /// Restrict the selected columns. An empty list selects `*`.
410 pub fn select<I, S>(mut self, cols: I) -> Self
411 where
412 I: IntoIterator<Item = S>,
413 S: AsRef<str>,
414 {
415 self.select_cols = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
416 self
417 }
418
419 /// Add a raw `SELECT` expression (verbatim, NOT escaped) with optional binds
420 /// — the escape hatch for aggregates/functions like `COUNT(*)`.
421 ///
422 /// Appended to the column list after any [`Self::select`] columns. Multiple
423 /// calls accumulate.
424 ///
425 /// # Warning: positional placeholder contract
426 ///
427 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and any
428 /// `binds` are appended to the running bind list in order. For **Postgres**,
429 /// the caller MUST write `$N` numbers matching the actual bind position. For
430 /// MySQL/SQLite use `?`.
431 pub fn select_raw(mut self, sql: &str, binds: Option<Vec<Value>>) -> Self {
432 self.select_raw
433 .push((sql.to_owned(), binds.unwrap_or_default()));
434 self
435 }
436
437 /// Add a subquery `SELECT` column: emits `(<sub>) AS {alias}` after the
438 /// regular columns and any [`Self::select_raw`] expressions.
439 ///
440 /// The subquery is compiled with placeholder continuity (its binds appear in
441 /// `$N` order at the point it is emitted — before the `WHERE` clause, since
442 /// the SELECT list is rendered first). SELECT-only.
443 pub fn select_subquery(mut self, alias: &str, sub: QueryBuilder<D>) -> Self {
444 self.select_subqueries
445 .push((alias.to_owned(), Box::new(sub)));
446 self
447 }
448
449 fn push_agg(mut self, func: AggFn, col: &str, alias: Option<&str>) -> Self {
450 self.select_exprs.push(SelectExpr::Agg {
451 func,
452 col: col.to_owned(),
453 alias: alias.map(|a| a.to_owned()),
454 });
455 self
456 }
457
458 /// Add `COUNT(col)` to the SELECT list (`col == "*"` → `COUNT(*)`).
459 pub fn select_count(self, col: &str) -> Self {
460 self.push_agg(AggFn::Count, col, None)
461 }
462
463 /// Add `COUNT(col) AS alias` (both identifiers escaped; `*` passed through).
464 pub fn select_count_as(self, col: &str, alias: &str) -> Self {
465 self.push_agg(AggFn::Count, col, Some(alias))
466 }
467
468 /// Add `SUM(col)` to the SELECT list.
469 pub fn select_sum(self, col: &str) -> Self {
470 self.push_agg(AggFn::Sum, col, None)
471 }
472
473 /// Add `SUM(col) AS alias`.
474 pub fn select_sum_as(self, col: &str, alias: &str) -> Self {
475 self.push_agg(AggFn::Sum, col, Some(alias))
476 }
477
478 /// Add `AVG(col)` to the SELECT list.
479 pub fn select_avg(self, col: &str) -> Self {
480 self.push_agg(AggFn::Avg, col, None)
481 }
482
483 /// Add `AVG(col) AS alias`.
484 pub fn select_avg_as(self, col: &str, alias: &str) -> Self {
485 self.push_agg(AggFn::Avg, col, Some(alias))
486 }
487
488 /// Add `MIN(col)` to the SELECT list.
489 pub fn select_min(self, col: &str) -> Self {
490 self.push_agg(AggFn::Min, col, None)
491 }
492
493 /// Add `MIN(col) AS alias`.
494 pub fn select_min_as(self, col: &str, alias: &str) -> Self {
495 self.push_agg(AggFn::Min, col, Some(alias))
496 }
497
498 /// Add `MAX(col)` to the SELECT list.
499 pub fn select_max(self, col: &str) -> Self {
500 self.push_agg(AggFn::Max, col, None)
501 }
502
503 /// Add `MAX(col) AS alias`.
504 pub fn select_max_as(self, col: &str, alias: &str) -> Self {
505 self.push_agg(AggFn::Max, col, Some(alias))
506 }
507
508 /// Add `col AS alias` to the SELECT list (both identifiers escaped).
509 pub fn select_as(mut self, col: &str, alias: &str) -> Self {
510 self.select_exprs.push(SelectExpr::ColAs {
511 col: col.to_owned(),
512 alias: alias.to_owned(),
513 });
514 self
515 }
516
517 /// Emit `SELECT DISTINCT …` (all dialects).
518 pub fn distinct(mut self) -> Self {
519 self.distinct = true;
520 self
521 }
522
523 /// Emit `SELECT DISTINCT ON (cols) …` — **Postgres only**.
524 ///
525 /// `cols` are raw identifiers (escaped at compile time). Compiling against a
526 /// dialect without `DISTINCT ON` support panics
527 /// (`DISTINCT ON requires PostgreSQL`).
528 pub fn distinct_on<I, S>(mut self, cols: I) -> Self
529 where
530 I: IntoIterator<Item = S>,
531 S: AsRef<str>,
532 {
533 self.distinct_on = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
534 self.distinct = true;
535 self
536 }
537
538 /// `col ILIKE val` — dialect-aware case-insensitive match.
539 ///
540 /// On **Postgres** this compiles to the native `{col} ILIKE {ph}`. On
541 /// MySQL/SQLite (no native `ILIKE`) it compiles to
542 /// `LOWER({col}) LIKE LOWER({ph})`.
543 pub fn where_ilike(mut self, col: &str, val: impl IntoBind) -> Self {
544 self.wheres.push(Predicate::ILike {
545 col: col.to_owned(),
546 val: val.into_bind(),
547 });
548 self
549 }
550
551 /// `col @> val` — JSONB containment.
552 ///
553 /// **Postgres-specific:** the `@>` operator is emitted verbatim for all
554 /// dialects, but is only meaningful on Postgres `jsonb` columns. `val` is
555 /// typically a JSON text string (or `Value::Json` behind the `json`
556 /// feature).
557 pub fn where_jsonb_contains(mut self, col: &str, val: impl IntoBind) -> Self {
558 self.wheres.push(Predicate::JsonContains {
559 col: col.to_owned(),
560 val: val.into_bind(),
561 });
562 self
563 }
564
565 fn binary(mut self, col: &str, op: &'static str, val: impl IntoBind) -> Self {
566 self.wheres.push(Predicate::Binary {
567 col: col.to_owned(),
568 op,
569 val: val.into_bind(),
570 });
571 self
572 }
573
574 /// `col = val`.
575 pub fn where_eq(self, col: &str, val: impl IntoBind) -> Self {
576 self.binary(col, "=", val)
577 }
578
579 /// `col != val`.
580 pub fn where_ne(self, col: &str, val: impl IntoBind) -> Self {
581 self.binary(col, "!=", val)
582 }
583
584 /// `col > val`.
585 pub fn where_gt(self, col: &str, val: impl IntoBind) -> Self {
586 self.binary(col, ">", val)
587 }
588
589 /// `col >= val`.
590 pub fn where_gte(self, col: &str, val: impl IntoBind) -> Self {
591 self.binary(col, ">=", val)
592 }
593
594 /// `col < val`.
595 pub fn where_lt(self, col: &str, val: impl IntoBind) -> Self {
596 self.binary(col, "<", val)
597 }
598
599 /// `col <= val`.
600 pub fn where_lte(self, col: &str, val: impl IntoBind) -> Self {
601 self.binary(col, "<=", val)
602 }
603
604 /// `col LIKE val`.
605 pub fn where_like(self, col: &str, val: impl IntoBind) -> Self {
606 self.binary(col, "LIKE", val)
607 }
608
609 fn in_(mut self, col: &str, neg: bool, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
610 self.wheres.push(Predicate::In {
611 col: col.to_owned(),
612 neg,
613 vals: vals.into_iter().map(IntoBind::into_bind).collect(),
614 });
615 self
616 }
617
618 /// `col IN (...)`.
619 pub fn where_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
620 self.in_(col, false, vals)
621 }
622
623 /// `col NOT IN (...)`.
624 pub fn where_not_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
625 self.in_(col, true, vals)
626 }
627
628 fn null(mut self, col: &str, neg: bool) -> Self {
629 self.wheres.push(Predicate::Null {
630 col: col.to_owned(),
631 neg,
632 });
633 self
634 }
635
636 /// `col IS NULL`.
637 pub fn where_null(self, col: &str) -> Self {
638 self.null(col, false)
639 }
640
641 /// `col IS NOT NULL`.
642 pub fn where_not_null(self, col: &str) -> Self {
643 self.null(col, true)
644 }
645
646 /// `col BETWEEN lo AND hi`.
647 pub fn where_between(mut self, col: &str, lo: impl IntoBind, hi: impl IntoBind) -> Self {
648 self.wheres.push(Predicate::Between {
649 col: col.to_owned(),
650 lo: lo.into_bind(),
651 hi: hi.into_bind(),
652 });
653 self
654 }
655
656 /// Raw SQL predicate with its own binds — the verbatim escape hatch.
657 ///
658 /// # Warning: positional placeholder contract
659 ///
660 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
661 /// `binds` are appended to the running bind list in order. For
662 /// **Postgres**, the caller MUST write `$N` numbers matching the actual
663 /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
664 /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
665 /// produces a malformed query.
666 pub fn where_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
667 self.wheres.push(Predicate::Raw {
668 sql: sql.to_owned(),
669 binds,
670 });
671 self
672 }
673
674 /// `lhs op rhs` — compare two column identifiers (both escaped at compile
675 /// time), no bind. e.g. `where_column("orders.user_id", "=", "users.id")`.
676 pub fn where_column(mut self, lhs: &str, op: &'static str, rhs: &str) -> Self {
677 self.wheres.push(Predicate::Column {
678 lhs: lhs.to_owned(),
679 op,
680 rhs: rhs.to_owned(),
681 });
682 self
683 }
684
685 /// `EXISTS (subquery)` — takes an already-built sub-builder by value
686 /// (mirrors [`Self::union`] / [`Self::with`]). The sub-query is compiled
687 /// with placeholder continuity.
688 pub fn where_exists(mut self, sub: QueryBuilder<D>) -> Self {
689 self.wheres.push(Predicate::Exists {
690 neg: false,
691 sub: Box::new(sub),
692 });
693 self
694 }
695
696 /// `NOT EXISTS (subquery)`. See [`Self::where_exists`].
697 pub fn where_not_exists(mut self, sub: QueryBuilder<D>) -> Self {
698 self.wheres.push(Predicate::Exists {
699 neg: true,
700 sub: Box::new(sub),
701 });
702 self
703 }
704
705 /// `col IN (subquery)` — takes an already-built sub-builder by value. The
706 /// sub-query is compiled with placeholder continuity.
707 pub fn where_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
708 self.wheres.push(Predicate::InSubquery {
709 col: col.to_owned(),
710 neg: false,
711 sub: Box::new(sub),
712 });
713 self
714 }
715
716 /// `col NOT IN (subquery)`. See [`Self::where_in_subquery`].
717 pub fn where_not_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
718 self.wheres.push(Predicate::InSubquery {
719 col: col.to_owned(),
720 neg: true,
721 sub: Box::new(sub),
722 });
723 self
724 }
725
726 fn group(
727 mut self,
728 outer_conj: Conj,
729 f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>,
730 ) -> Self {
731 let preds = f(WhereBuilder::new()).into_preds();
732 self.wheres.push(Predicate::Group { outer_conj, preds });
733 self
734 }
735
736 /// Add a parenthesized `AND (...)` group built by the closure.
737 pub fn and_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
738 self.group(Conj::And, f)
739 }
740
741 /// Add a parenthesized `OR (...)` group built by the closure.
742 pub fn or_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
743 self.group(Conj::Or, f)
744 }
745
746 /// Build an `INSERT` from a single row of `(column, value)` pairs.
747 pub fn insert<K, V, I>(mut self, row: I) -> Self
748 where
749 K: AsRef<str>,
750 V: IntoBind,
751 I: IntoIterator<Item = (K, V)>,
752 {
753 self.method = Method::Insert;
754 self.set = row
755 .into_iter()
756 .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
757 .collect();
758 self
759 }
760
761 /// Build a multi-row `INSERT` from an iterator of rows, each a sequence of
762 /// `(column, value)` pairs.
763 ///
764 /// The inserted column set is taken from the **first** row's keys (sorted, as
765 /// with [`Self::insert`]). For each subsequent row, a value is bound for every
766 /// column in that set; a key **missing** in a later row binds `Value::Null`
767 /// rather than panicking (DoS-safe, matching the 1.x hardening). Composes with
768 /// `on_conflict_*` and `returning`.
769 pub fn insert_many<K, V, R, Rows>(mut self, rows: Rows) -> Self
770 where
771 K: AsRef<str>,
772 V: IntoBind,
773 R: IntoIterator<Item = (K, V)>,
774 Rows: IntoIterator<Item = R>,
775 {
776 self.method = Method::Insert;
777 self.insert_rows = rows
778 .into_iter()
779 .map(|row| {
780 row.into_iter()
781 .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
782 .collect()
783 })
784 .collect();
785 self
786 }
787
788 /// Build an `UPDATE` from `(column, value)` pairs. WHERE still applies.
789 pub fn update<K, V, I>(mut self, set: I) -> Self
790 where
791 K: AsRef<str>,
792 V: IntoBind,
793 I: IntoIterator<Item = (K, V)>,
794 {
795 self.method = Method::Update;
796 self.set = set
797 .into_iter()
798 .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
799 .collect();
800 self
801 }
802
803 /// Set a column to a **verbatim** SQL expression — the UPDATE escape
804 /// hatch. Switches the builder to UPDATE, like [`Self::update`] (and
805 /// like every method-selecting call, last one wins: switching away from
806 /// UPDATE afterwards leaves the expressions ignored).
807 ///
808 /// `col` is identifier-escaped; `expr` is emitted verbatim with `binds`
809 /// appended. Expressions render after the (sorted) structured
810 /// [`Self::update`] columns, in call order. Duplicate target columns are
811 /// not detected — the database reports them.
812 ///
813 /// # Warning: positional placeholder contract
814 ///
815 /// `expr` is NOT escaped or renumbered. For **Postgres**, hand-write
816 /// `$N` matching the actual bind position at the point this expression
817 /// is reached — `SET` precedes `WHERE`, so that is `(number of
818 /// structured SET binds) + (binds of all earlier expressions, counting
819 /// 1 per preceding `increment`/`decrement`) + 1`, `+2`, … For
820 /// MySQL/SQLite use `?`. A wrong `$N` produces a malformed query.
821 ///
822 /// ```
823 /// use chain_builder::{Postgres, QueryBuilder, Value};
824 /// let (sql, binds) = QueryBuilder::<Postgres>::table("t")
825 /// .update([("name", "x")])
826 /// .set_raw("updated_at", "NOW()", vec![])
827 /// .to_sql();
828 /// assert_eq!(sql, r#"UPDATE "t" SET "name" = $1, "updated_at" = NOW()"#);
829 /// assert_eq!(binds, vec![Value::Text("x".into())]);
830 /// ```
831 pub fn set_raw(mut self, col: &str, expr: &str, binds: Vec<Value>) -> Self {
832 self.method = Method::Update;
833 self.set_exprs.push(SetExpr::Raw {
834 col: col.to_owned(),
835 expr: expr.to_owned(),
836 binds,
837 });
838 self
839 }
840
841 /// `col = col + value` — atomic counter increment. Structured (column
842 /// escaped, value bound); switches the builder to UPDATE, so
843 /// `qb.increment("views", 1)` alone is a valid UPDATE.
844 ///
845 /// ```
846 /// use chain_builder::{Postgres, QueryBuilder, Value};
847 /// let (sql, binds) = QueryBuilder::<Postgres>::table("t")
848 /// .increment("views", 1i64)
849 /// .to_sql();
850 /// assert_eq!(sql, r#"UPDATE "t" SET "views" = "views" + $1"#);
851 /// assert_eq!(binds, vec![Value::I64(1)]);
852 /// ```
853 pub fn increment(mut self, col: &str, by: impl IntoBind) -> Self {
854 self.method = Method::Update;
855 self.set_exprs.push(SetExpr::Step {
856 col: col.to_owned(),
857 by: by.into_bind(),
858 neg: false,
859 });
860 self
861 }
862
863 /// `col = col - value` — atomic counter decrement. See
864 /// [`Self::increment`].
865 pub fn decrement(mut self, col: &str, by: impl IntoBind) -> Self {
866 self.method = Method::Update;
867 self.set_exprs.push(SetExpr::Step {
868 col: col.to_owned(),
869 by: by.into_bind(),
870 neg: true,
871 });
872 self
873 }
874
875 /// Build a `DELETE`. WHERE still applies.
876 pub fn delete(mut self) -> Self {
877 self.method = Method::Delete;
878 self
879 }
880
881 /// On conflict, skip the row (`INSERT`-only; ignored on UPDATE/DELETE).
882 ///
883 /// `targets` are the conflict-target columns (may be empty).
884 ///
885 /// - **Postgres / SQLite:** emits `ON CONFLICT ({targets}) DO NOTHING`, or
886 /// bare `ON CONFLICT DO NOTHING` when `targets` is empty.
887 /// - **MySQL:** emits `INSERT IGNORE INTO …` (no trailing clause). Note that
888 /// `IGNORE` suppresses *more* than duplicate-key errors (also truncation
889 /// and bad-value coercion) — broader than pg/sqlite `DO NOTHING`.
890 pub fn on_conflict_do_nothing<I, S>(mut self, targets: I) -> Self
891 where
892 I: IntoIterator<Item = S>,
893 S: AsRef<str>,
894 {
895 self.on_conflict = Some(OnConflict {
896 targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
897 action: ConflictAction::DoNothing,
898 });
899 self
900 }
901
902 /// On conflict, update the non-target inserted columns from the proposed row
903 /// (`INSERT`-only; ignored on UPDATE/DELETE).
904 ///
905 /// - **Postgres / SQLite:** emits
906 /// `ON CONFLICT ({targets}) DO UPDATE SET {c} = EXCLUDED.{c}, …` for every
907 /// inserted column *except* the conflict targets. If `targets` is empty or
908 /// covers all inserted columns (empty SET list), falls back to the
909 /// `DO NOTHING` rendering (pg/sqlite require a target for `DO UPDATE`).
910 /// - **MySQL:** the explicit `targets` are **ignored** (MySQL uses its own
911 /// unique/primary keys); emits
912 /// `ON DUPLICATE KEY UPDATE {c} = VALUES({c}), …` for *all* inserted
913 /// columns. `VALUES()` is used for MySQL 5.7/8.x compatibility. Including a
914 /// PK column in the insert set yields a redundant-but-harmless
915 /// `pk = VALUES(pk)`.
916 pub fn on_conflict_merge<I, S>(mut self, targets: I) -> Self
917 where
918 I: IntoIterator<Item = S>,
919 S: AsRef<str>,
920 {
921 self.on_conflict = Some(OnConflict {
922 targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
923 action: ConflictAction::Merge,
924 });
925 self
926 }
927
928 /// Add a `RETURNING` column list. Works on INSERT / UPDATE / DELETE for
929 /// Postgres and SQLite; a `"*"` column is emitted unescaped (`RETURNING *`).
930 ///
931 /// On **MySQL** this is a silent no-op (MySQL has no `RETURNING`). On
932 /// **SQLite** `RETURNING` requires SQLite ≥ 3.35.0 (2021); `supports_returning()`
933 /// is a compile-time dialect flag, not a runtime version check.
934 pub fn returning<I, S>(mut self, cols: I) -> Self
935 where
936 I: IntoIterator<Item = S>,
937 S: AsRef<str>,
938 {
939 self.returning = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
940 self
941 }
942
943 /// Add `GROUP BY` columns (raw owned identifiers, escaped at compile time).
944 ///
945 /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
946 pub fn group_by<I, S>(mut self, cols: I) -> Self
947 where
948 I: IntoIterator<Item = S>,
949 S: AsRef<str>,
950 {
951 self.groups
952 .extend(cols.into_iter().map(|c| c.as_ref().to_owned()));
953 self
954 }
955
956 /// Add a raw `GROUP BY` fragment with its own binds — the verbatim escape
957 /// hatch. SELECT-only.
958 ///
959 /// The fragment is appended after any structured [`Self::group_by`] columns
960 /// within the same `GROUP BY` clause (e.g. `GROUP BY "a", <raw>`); if no
961 /// structured columns are present it becomes the whole `GROUP BY <raw>`.
962 ///
963 /// # Warning: positional placeholder contract
964 ///
965 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
966 /// `binds` are appended to the running bind list in order. For
967 /// **Postgres**, the caller MUST write `$N` numbers matching the actual
968 /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
969 /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
970 /// produces a malformed query.
971 pub fn group_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
972 self.group_by_raw = Some((sql.to_owned(), binds));
973 self
974 }
975
976 /// Add a raw `ORDER BY` fragment with its own binds — the verbatim escape
977 /// hatch. SELECT-only.
978 ///
979 /// The fragment is appended after any structured [`Self::order_by`] terms
980 /// within the same `ORDER BY` clause (e.g. `ORDER BY "a" ASC, <raw>`); if no
981 /// structured terms are present it becomes the whole `ORDER BY <raw>`.
982 ///
983 /// # Warning: positional placeholder contract
984 ///
985 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
986 /// `binds` are appended to the running bind list in order. For
987 /// **Postgres**, the caller MUST write `$N` numbers matching the actual
988 /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
989 /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
990 /// produces a malformed query.
991 pub fn order_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
992 self.order_by_raw = Some((sql.to_owned(), binds));
993 self
994 }
995
996 /// Add an `ORDER BY col <ord>` term. SELECT-only.
997 pub fn order_by(mut self, col: &str, ord: Order) -> Self {
998 self.orders.push((col.to_owned(), ord));
999 self
1000 }
1001
1002 /// Add an `ORDER BY col ASC` term. SELECT-only.
1003 pub fn order_by_asc(self, col: &str) -> Self {
1004 self.order_by(col, Order::Asc)
1005 }
1006
1007 /// Add an `ORDER BY col DESC` term. SELECT-only.
1008 pub fn order_by_desc(self, col: &str) -> Self {
1009 self.order_by(col, Order::Desc)
1010 }
1011
1012 /// Set `LIMIT n` (bound as a placeholder). SELECT-only.
1013 pub fn limit(mut self, n: i64) -> Self {
1014 self.limit = Some(n);
1015 self
1016 }
1017
1018 /// Set `OFFSET n` (bound as a placeholder). SELECT-only.
1019 ///
1020 /// `offset` requires `limit`: compiling an offset without a limit panics
1021 /// (`offset(...) requires limit(...)`), uniform across dialects since MySQL
1022 /// rejects a bare `OFFSET`.
1023 pub fn offset(mut self, n: i64) -> Self {
1024 self.offset = Some(n);
1025 self
1026 }
1027
1028 /// Lock selected rows with `FOR UPDATE`.
1029 ///
1030 /// Honored by Postgres / MySQL; a **silent no-op on SQLite**. Preserves any
1031 /// `SKIP LOCKED` / `NOWAIT` modifier already set. **SELECT-only:** compiling
1032 /// panics if attached to INSERT/UPDATE/DELETE or combined with `UNION`.
1033 pub fn for_update(mut self) -> Self {
1034 let wait = self.lock.and_then(|l| l.wait);
1035 self.lock = Some(Lock {
1036 strength: LockStrength::Update,
1037 wait,
1038 });
1039 self
1040 }
1041
1042 /// Lock selected rows with `FOR SHARE`.
1043 ///
1044 /// Honored by Postgres / MySQL; a **silent no-op on SQLite**. Preserves any
1045 /// `SKIP LOCKED` / `NOWAIT` modifier already set. **SELECT-only:** compiling
1046 /// panics if attached to INSERT/UPDATE/DELETE or combined with `UNION`.
1047 pub fn for_share(mut self) -> Self {
1048 let wait = self.lock.and_then(|l| l.wait);
1049 self.lock = Some(Lock {
1050 strength: LockStrength::Share,
1051 wait,
1052 });
1053 self
1054 }
1055
1056 /// Add `SKIP LOCKED` to the row-locking clause (skip already-locked rows).
1057 ///
1058 /// If no lock strength was set yet, defaults to `FOR UPDATE`. SELECT-only;
1059 /// no-op on SQLite.
1060 pub fn skip_locked(mut self) -> Self {
1061 let strength = self
1062 .lock
1063 .map(|l| l.strength)
1064 .unwrap_or(LockStrength::Update);
1065 self.lock = Some(Lock {
1066 strength,
1067 wait: Some(LockWait::SkipLocked),
1068 });
1069 self
1070 }
1071
1072 /// Add `NOWAIT` to the row-locking clause (error if a row is already locked).
1073 ///
1074 /// If no lock strength was set yet, defaults to `FOR UPDATE`. SELECT-only;
1075 /// no-op on SQLite.
1076 pub fn no_wait(mut self) -> Self {
1077 let strength = self
1078 .lock
1079 .map(|l| l.strength)
1080 .unwrap_or(LockStrength::Update);
1081 self.lock = Some(Lock {
1082 strength,
1083 wait: Some(LockWait::NoWait),
1084 });
1085 self
1086 }
1087
1088 fn push_join(
1089 mut self,
1090 kind: JoinKind,
1091 table: &str,
1092 f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
1093 ) -> Self {
1094 let on = f(JoinClause::new()).into_conds();
1095 self.joins.push(Join {
1096 kind,
1097 table: table.to_owned(),
1098 on,
1099 });
1100 self
1101 }
1102
1103 /// `INNER JOIN table ON …` — conditions built by the closure.
1104 ///
1105 /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
1106 pub fn join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
1107 self.push_join(JoinKind::Inner, table, f)
1108 }
1109
1110 /// `LEFT JOIN table ON …`. SELECT-only.
1111 pub fn left_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
1112 self.push_join(JoinKind::Left, table, f)
1113 }
1114
1115 /// `RIGHT JOIN table ON …`. SELECT-only.
1116 pub fn right_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
1117 self.push_join(JoinKind::Right, table, f)
1118 }
1119
1120 /// `FULL OUTER JOIN table ON …`. SELECT-only.
1121 pub fn full_outer_join(
1122 self,
1123 table: &str,
1124 f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
1125 ) -> Self {
1126 self.push_join(JoinKind::FullOuter, table, f)
1127 }
1128
1129 /// `CROSS JOIN table` — takes **no** `ON` closure (a cross join has no
1130 /// condition). SELECT-only.
1131 pub fn cross_join(mut self, table: &str) -> Self {
1132 self.joins.push(Join {
1133 kind: JoinKind::Cross,
1134 table: table.to_owned(),
1135 on: Vec::new(),
1136 });
1137 self
1138 }
1139
1140 /// `HAVING col op ?` — `col` is a real column/alias (escaped); value bound.
1141 ///
1142 /// For aggregate expressions like `COUNT(*) > ?`, use [`Self::having_raw`].
1143 /// SELECT-only: ignored for INSERT/UPDATE/DELETE. Multiple HAVING terms are
1144 /// joined by `AND`.
1145 ///
1146 /// # Operator allowlist (injection guard)
1147 ///
1148 /// Unlike `where_eq`/`where_column`/`JoinClause::on`, which take
1149 /// `op: &'static str` (so only compile-time literals are accepted), this
1150 /// method takes `op: &str` for ergonomics. Because `op` is emitted
1151 /// **verbatim** into the SQL (it is not a bound value and cannot be escaped
1152 /// without changing its meaning), an attacker-controlled operator would be a
1153 /// SQL-injection vector. To prevent that, `op` is validated against a fixed
1154 /// set of comparison operators. A disallowed operator records a deferred
1155 /// [`BuildError::InvalidHavingOperator`] (the chain stays intact):
1156 /// [`Self::try_to_sql`] returns it as `Err`, while [`Self::to_sql`] panics
1157 /// with the same message (fail-loud, like the `offset`/`distinct_on`/lock
1158 /// guards). The operator is matched case-insensitively and stored trimmed.
1159 /// For anything outside this set — arbitrary aggregate expressions, custom
1160 /// operators — use [`Self::having_raw`], the documented verbatim escape
1161 /// hatch.
1162 ///
1163 /// Allowed: `=`, `!=`, `<>`, `>`, `>=`, `<`, `<=`, `LIKE`, `NOT LIKE`.
1164 pub fn having(mut self, col: &str, op: &str, val: impl IntoBind) -> Self {
1165 const ALLOWED_HAVING_OPS: &[&str] =
1166 &["=", "!=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"];
1167 let normalized = op.trim();
1168 if !ALLOWED_HAVING_OPS
1169 .iter()
1170 .any(|allowed| allowed.eq_ignore_ascii_case(normalized))
1171 {
1172 // Keep the FIRST error: it points at the original misuse, and a
1173 // later mistake must not mask it.
1174 if self.error.is_none() {
1175 self.error = Some(BuildError::InvalidHavingOperator(op.to_owned()));
1176 }
1177 return self;
1178 }
1179 self.havings.push(Having::Col {
1180 col: col.to_owned(),
1181 op: normalized.to_owned(),
1182 val: val.into_bind(),
1183 });
1184 self
1185 }
1186
1187 /// Raw `HAVING` expression with its own binds — the verbatim escape hatch
1188 /// for aggregates (e.g. `having_raw("COUNT(*) > ?", …)`).
1189 ///
1190 /// # Warning: positional placeholder contract
1191 ///
1192 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
1193 /// `binds` are appended to the running bind list in order. For
1194 /// **Postgres**, the caller MUST write `$N` numbers matching the actual
1195 /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
1196 /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
1197 /// produces a malformed query.
1198 pub fn having_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
1199 self.havings.push(Having::Raw {
1200 sql: sql.to_owned(),
1201 binds,
1202 });
1203 self
1204 }
1205
1206 /// Add a `WITH name AS (query)` common table expression. SELECT-only.
1207 ///
1208 /// CTE bodies are compiled before the main query, so their binds (and pg
1209 /// `$N` numbers) appear first.
1210 pub fn with(mut self, name: &str, query: QueryBuilder<D>) -> Self {
1211 self.ctes.push(Cte {
1212 name: name.to_owned(),
1213 recursive: false,
1214 query,
1215 });
1216 self
1217 }
1218
1219 /// Add a recursive CTE. If any CTE is recursive, the single `WITH` carries
1220 /// `RECURSIVE`. SELECT-only.
1221 pub fn with_recursive(mut self, name: &str, query: QueryBuilder<D>) -> Self {
1222 self.ctes.push(Cte {
1223 name: name.to_owned(),
1224 recursive: true,
1225 query,
1226 });
1227 self
1228 }
1229
1230 /// Append a `UNION query` arm. SELECT-only.
1231 pub fn union(mut self, query: QueryBuilder<D>) -> Self {
1232 self.unions.push((false, query));
1233 self
1234 }
1235
1236 /// Append a `UNION ALL query` arm. SELECT-only.
1237 pub fn union_all(mut self, query: QueryBuilder<D>) -> Self {
1238 self.unions.push((true, query));
1239 self
1240 }
1241
1242 /// Conditionally apply `f` to the builder, keeping the chain intact.
1243 ///
1244 /// Returns `f(self)` when `cond` is true, otherwise `self` unchanged. This
1245 /// lets you add clauses based on a runtime flag without breaking the
1246 /// by-value chain.
1247 ///
1248 /// ```
1249 /// use chain_builder::{Postgres, QueryBuilder};
1250 /// let only_active = true;
1251 /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
1252 /// .select(["id"])
1253 /// .when(only_active, |q| q.where_eq("status", "active"))
1254 /// .to_sql();
1255 /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
1256 /// ```
1257 pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
1258 if cond {
1259 f(self)
1260 } else {
1261 self
1262 }
1263 }
1264
1265 /// Apply `if_true` when `cond` holds, otherwise `if_false`, keeping the
1266 /// chain intact.
1267 ///
1268 /// ```
1269 /// use chain_builder::{Postgres, QueryBuilder};
1270 /// let active = false;
1271 /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
1272 /// .select(["id"])
1273 /// .when_else(
1274 /// active,
1275 /// |q| q.where_eq("status", "active"),
1276 /// |q| q.where_eq("status", "inactive"),
1277 /// )
1278 /// .to_sql();
1279 /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
1280 /// ```
1281 pub fn when_else(
1282 self,
1283 cond: bool,
1284 if_true: impl FnOnce(Self) -> Self,
1285 if_false: impl FnOnce(Self) -> Self,
1286 ) -> Self {
1287 if cond {
1288 if_true(self)
1289 } else {
1290 if_false(self)
1291 }
1292 }
1293
1294 /// Apply `LIMIT`/`OFFSET` for a **1-based** page: row window
1295 /// `[(page-1) * per_page, page * per_page)`.
1296 ///
1297 /// Equivalent to `self.limit(per_page).offset((page - 1).max(0) * per_page)`.
1298 /// A `page < 1` is treated as page 1 (offset 0), so callers never get a
1299 /// negative offset. SELECT-only, like [`Self::limit`] / [`Self::offset`].
1300 ///
1301 /// ```
1302 /// use chain_builder::{Postgres, QueryBuilder, Value};
1303 /// let (sql, binds) = QueryBuilder::<Postgres>::table("users")
1304 /// .select(["id"])
1305 /// .paginate(2, 10)
1306 /// .to_sql();
1307 /// assert_eq!(sql, r#"SELECT "id" FROM "users" LIMIT $1 OFFSET $2"#);
1308 /// assert_eq!(binds, vec![Value::I64(10), Value::I64(10)]);
1309 /// ```
1310 pub fn paginate(self, page: i64, per_page: i64) -> Self {
1311 self.limit(per_page).offset((page - 1).max(0) * per_page)
1312 }
1313
1314 /// Compile to `(sql, binds)`, panicking on an invalid builder.
1315 ///
1316 /// Panicking twin of [`Self::try_to_sql`]; the panic message is the
1317 /// [`BuildError`]'s `Display` text. Prefer [`Self::try_to_sql`] when any
1318 /// part of the query is driven by runtime input (e.g. an HTTP request), so
1319 /// misuse maps to an error response instead of a crash.
1320 pub fn to_sql(&self) -> (String, Vec<Value>) {
1321 compile(self)
1322 }
1323
1324 /// Compile to `(sql, binds)`, or return the [`BuildError`] describing why
1325 /// the query cannot be rendered (invalid construction such as `offset()`
1326 /// without `limit()`, an empty `insert()`/`update()`, a row lock outside
1327 /// SELECT, or a disallowed `having()` operator).
1328 ///
1329 /// Errors recorded by builder methods (deferred, chain-preserving) and
1330 /// errors detected during compilation — including inside nested builders
1331 /// (CTEs, UNION arms, subqueries) — all surface here.
1332 pub fn try_to_sql(&self) -> Result<(String, Vec<Value>), BuildError> {
1333 try_compile(self)
1334 }
1335
1336 /// Render the compiled query as a human-readable string for logs and
1337 /// debugging: the SQL, then one indented line per bind.
1338 ///
1339 /// ```
1340 /// use chain_builder::{Postgres, QueryBuilder};
1341 /// let qb = QueryBuilder::<Postgres>::table("users")
1342 /// .select(["id"])
1343 /// .where_eq("status", "active");
1344 /// assert_eq!(
1345 /// qb.to_sql_pretty(),
1346 /// "SELECT \"id\" FROM \"users\" WHERE \"status\" = $1\nbinds:\n $1 = Text(\"active\")"
1347 /// );
1348 /// ```
1349 ///
1350 /// Bind labels are the dialect placeholder (`$1`, `$2`, … on Postgres);
1351 /// dialects whose placeholder is a bare `?` get a 1-based ordinal
1352 /// appended for readability (`?1`, `?2`, …) — the SQL itself still uses
1353 /// `?`. With zero binds the output is the SQL line only. The output
1354 /// format is for humans and is **not** a stability contract.
1355 ///
1356 /// # Warning
1357 ///
1358 /// The output includes the **literal bind values**. Do not log it if
1359 /// any bind may contain sensitive data (passwords, tokens, PII).
1360 ///
1361 /// Panicking twin of [`Self::try_to_sql_pretty`]; the panic message is
1362 /// the [`BuildError`]'s `Display` text (same policy as
1363 /// [`Self::to_sql`]).
1364 pub fn to_sql_pretty(&self) -> String {
1365 self.try_to_sql_pretty().unwrap_or_else(|e| panic!("{e}"))
1366 }
1367
1368 /// Fallible twin of [`Self::to_sql_pretty`]; surfaces the same
1369 /// [`BuildError`] as [`Self::try_to_sql`]. The same sensitive-data
1370 /// warning applies.
1371 pub fn try_to_sql_pretty(&self) -> Result<String, BuildError> {
1372 use std::fmt::Write as _;
1373 let (sql, binds) = try_compile(self)?;
1374 let mut out = sql;
1375 if !binds.is_empty() {
1376 out.push_str("\nbinds:");
1377 let mut label = String::with_capacity(4);
1378 for (i, b) in binds.iter().enumerate() {
1379 label.clear();
1380 D::write_placeholder(&mut label, i + 1);
1381 if label == "?" {
1382 // Infallible for String.
1383 let _ = write!(label, "{}", i + 1);
1384 }
1385 out.push_str("\n ");
1386 out.push_str(&label);
1387 out.push_str(" = ");
1388 let _ = write!(out, "{b:?}");
1389 }
1390 }
1391 Ok(out)
1392 }
1393}