chain_builder/builder.rs
1//! Typed, dialect-aware query builder.
2//!
3//! [`QueryBuilder`] is parameterized over a [`Dialect`] marker and uses
4//! by-value (`self`) chaining: every mutator takes and returns `Self`. The
5//! terminal [`QueryBuilder::to_sql`] compiles to `(sql, binds)`.
6
7use core::marker::PhantomData;
8
9use crate::compile::compile;
10use crate::dialect::Dialect;
11use crate::value::{IntoBind, Value};
12use crate::where_::{Conj, Predicate, WhereBuilder};
13
14/// Sort direction for an `ORDER BY` column.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Order {
17 /// Ascending (`ASC`).
18 Asc,
19 /// Descending (`DESC`).
20 Desc,
21}
22
23/// The kind of SQL `JOIN`.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum JoinKind {
26 /// `INNER JOIN`.
27 Inner,
28 /// `LEFT JOIN`.
29 Left,
30 /// `RIGHT JOIN`.
31 Right,
32 /// `FULL OUTER JOIN`.
33 FullOuter,
34 /// `CROSS JOIN` (no `ON`).
35 Cross,
36}
37
38/// A single `ON` condition of a [`Join`].
39///
40/// Columns in `On`/`OnVal` are stored raw and escaped at compile time. `OnRaw`
41/// is the verbatim escape hatch (see [`JoinClause::on_raw`]).
42#[derive(Debug, Clone, PartialEq)]
43pub enum JoinCond {
44 /// `lhs op rhs` — both sides are columns (escaped at compile time).
45 On(String, &'static str, String),
46 /// `col op ?` — `col` escaped, the value is bound.
47 OnVal(String, &'static str, Value),
48 /// Verbatim SQL with its own binds.
49 OnRaw(String, Vec<Value>),
50}
51
52/// A `JOIN` clause: a kind, a target table, and zero or more `ON` conditions.
53#[derive(Debug, Clone, PartialEq)]
54pub struct Join {
55 /// The join kind (`INNER`, `LEFT`, …).
56 pub kind: JoinKind,
57 /// Raw target table identifier (escaped at compile time).
58 pub table: String,
59 /// `ON` conditions, joined by `AND`. Empty for `CROSS JOIN`.
60 pub on: Vec<JoinCond>,
61}
62
63/// A `HAVING` condition (SELECT-only, rendered after `GROUP BY`).
64#[derive(Debug, Clone, PartialEq)]
65pub enum Having {
66 /// `col op ?` — `col` is a real column/alias (escaped); value bound.
67 Col {
68 /// Raw column identifier (escaped at compile time).
69 col: String,
70 /// SQL operator token (`>`, `=`, …).
71 op: String,
72 /// Bound value.
73 val: Value,
74 },
75 /// Verbatim aggregate expression with its own binds (e.g. `COUNT(*) > ?`).
76 Raw {
77 /// Verbatim SQL.
78 sql: String,
79 /// Bound values appended in order.
80 binds: Vec<Value>,
81 },
82}
83
84/// A common table expression (`WITH` / `WITH RECURSIVE`).
85#[derive(Debug, Clone, PartialEq)]
86pub struct Cte<D: Dialect> {
87 /// Raw CTE name (escaped at compile time).
88 pub name: String,
89 /// Whether this CTE forces the single `WITH` to carry `RECURSIVE`.
90 pub recursive: bool,
91 /// The sub-query compiled into the CTE body.
92 pub query: QueryBuilder<D>,
93}
94
95/// Accumulator passed to `join`/`left_join`/… closures to build `ON` conditions.
96///
97/// The closure receives an empty `JoinClause`, chains `on`/`on_val`/`on_raw`
98/// calls, and returns it; the builder stores the collected conditions.
99pub struct JoinClause<D: Dialect> {
100 conds: Vec<JoinCond>,
101 _marker: PhantomData<D>,
102}
103
104impl<D: Dialect> Default for JoinClause<D> {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl<D: Dialect> JoinClause<D> {
111 /// Create an empty accumulator.
112 pub fn new() -> Self {
113 Self {
114 conds: Vec::new(),
115 _marker: PhantomData,
116 }
117 }
118
119 fn into_conds(self) -> Vec<JoinCond> {
120 self.conds
121 }
122
123 /// `lhs op rhs` — both sides are columns (each escaped at compile time).
124 pub fn on(mut self, col: &str, op: &'static str, col2: &str) -> Self {
125 self.conds
126 .push(JoinCond::On(col.to_owned(), op, col2.to_owned()));
127 self
128 }
129
130 /// `col op ?` — `col` escaped, the value bound as a placeholder.
131 pub fn on_val(mut self, col: &str, op: &'static str, val: impl IntoBind) -> Self {
132 self.conds
133 .push(JoinCond::OnVal(col.to_owned(), op, val.into_bind()));
134 self
135 }
136
137 /// Raw `ON` SQL fragment with its own binds — the verbatim escape hatch.
138 ///
139 /// # Warning: positional placeholder contract
140 ///
141 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
142 /// `binds` are appended to the running bind list in order. For
143 /// **Postgres**, the caller MUST write `$N` numbers matching the actual
144 /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
145 /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
146 /// produces a malformed query.
147 pub fn on_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
148 self.conds.push(JoinCond::OnRaw(sql.to_owned(), binds));
149 self
150 }
151}
152
153/// What to do when an `INSERT` hits a conflict (see [`OnConflict`]).
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum ConflictAction {
156 /// Skip the conflicting row (`DO NOTHING` / `INSERT IGNORE`).
157 DoNothing,
158 /// Update the non-target inserted columns from the proposed row
159 /// (`DO UPDATE SET … = EXCLUDED.…` / `ON DUPLICATE KEY UPDATE …`).
160 Merge,
161}
162
163/// An `ON CONFLICT` specification attached to an `INSERT`.
164///
165/// `targets` are the raw conflict-target column identifiers (escaped at compile
166/// time). They are honored by Postgres / SQLite (`OnConflict` style) and
167/// **ignored** by MySQL (`OnDuplicateKey` style), which relies on its own
168/// unique/primary keys.
169#[derive(Debug, Clone, PartialEq)]
170pub struct OnConflict {
171 /// Raw conflict-target column identifiers.
172 pub targets: Vec<String>,
173 /// What to do on conflict.
174 pub action: ConflictAction,
175}
176
177/// Which kind of statement is being built.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
179pub enum Method {
180 /// `SELECT`.
181 #[default]
182 Select,
183 /// `INSERT`.
184 Insert,
185 /// `UPDATE`.
186 Update,
187 /// `DELETE`.
188 Delete,
189}
190
191/// Typed, dialect-aware SQL query builder.
192#[derive(Debug, Clone, PartialEq)]
193pub struct QueryBuilder<D: Dialect> {
194 pub(crate) table: String,
195 /// Optional database/schema qualifier (multi-tenant: one connection, many DBs).
196 /// When set, prefixes the main table and join tables: `"db"."table"`.
197 pub(crate) db: Option<String>,
198 pub(crate) select_cols: Vec<String>,
199 /// Raw `SELECT` expressions (verbatim, NOT escaped) with their own binds,
200 /// appended after `select_cols`. Backs `select_raw`.
201 pub(crate) select_raw: Vec<(String, Vec<Value>)>,
202 /// Subquery `SELECT` columns: `(alias, sub)` → `(<sub>) AS {esc alias}`,
203 /// appended after `select_cols` / `select_raw`. Backs `select_subquery`.
204 pub(crate) select_subqueries: Vec<(String, Box<QueryBuilder<D>>)>,
205 /// `SELECT DISTINCT` flag (raw; off by default for M1 byte-identity).
206 pub(crate) distinct: bool,
207 /// `SELECT DISTINCT ON (cols)` columns (raw; Postgres-only).
208 pub(crate) distinct_on: Vec<String>,
209 pub(crate) wheres: Vec<Predicate<D>>,
210 pub(crate) method: Method,
211 pub(crate) set: Vec<(String, Value)>,
212 /// Multi-row `INSERT` rows (empty unless `insert_many` was used). Each row is
213 /// a `(column, value)` list; columns come from the first row's sorted keys.
214 pub(crate) insert_rows: Vec<Vec<(String, Value)>>,
215 pub(crate) joins: Vec<Join>,
216 pub(crate) groups: Vec<String>,
217 /// Raw `GROUP BY` fragment (verbatim) with its own binds, appended after any
218 /// structured `groups`.
219 pub(crate) group_by_raw: Option<(String, Vec<Value>)>,
220 pub(crate) havings: Vec<Having>,
221 pub(crate) orders: Vec<(String, Order)>,
222 /// Raw `ORDER BY` fragment (verbatim) with its own binds, appended after any
223 /// structured `orders`.
224 pub(crate) order_by_raw: Option<(String, Vec<Value>)>,
225 pub(crate) limit: Option<i64>,
226 pub(crate) offset: Option<i64>,
227 pub(crate) ctes: Vec<Cte<D>>,
228 pub(crate) unions: Vec<(bool, QueryBuilder<D>)>,
229 /// `ON CONFLICT` spec for `INSERT` (ignored on UPDATE/DELETE).
230 pub(crate) on_conflict: Option<OnConflict>,
231 /// `RETURNING` column list (raw; `"*"` emitted unescaped).
232 pub(crate) returning: Vec<String>,
233 _marker: PhantomData<D>,
234}
235
236impl<D: Dialect> QueryBuilder<D> {
237 /// Start a query against `name`.
238 pub fn table(name: &str) -> Self {
239 Self {
240 table: name.to_owned(),
241 db: None,
242 select_cols: Vec::new(),
243 select_raw: Vec::new(),
244 select_subqueries: Vec::new(),
245 distinct: false,
246 distinct_on: Vec::new(),
247 wheres: Vec::new(),
248 method: Method::Select,
249 set: Vec::new(),
250 insert_rows: Vec::new(),
251 joins: Vec::new(),
252 groups: Vec::new(),
253 group_by_raw: None,
254 havings: Vec::new(),
255 orders: Vec::new(),
256 order_by_raw: None,
257 limit: None,
258 offset: None,
259 ctes: Vec::new(),
260 unions: Vec::new(),
261 on_conflict: None,
262 returning: Vec::new(),
263 _marker: PhantomData,
264 }
265 }
266
267 /// Set the database/schema qualifier (multi-tenant: one connection, many DBs).
268 ///
269 /// The name prefixes the main table and every join table, escaped per dialect:
270 /// `QueryBuilder::<Postgres>::table("users").db("mydb")` →
271 /// `… FROM "mydb"."users"`. Matches 1.x `db()`.
272 pub fn db(mut self, name: &str) -> Self {
273 self.db = Some(name.to_owned());
274 self
275 }
276
277 /// Restrict the selected columns. An empty list selects `*`.
278 pub fn select<I, S>(mut self, cols: I) -> Self
279 where
280 I: IntoIterator<Item = S>,
281 S: AsRef<str>,
282 {
283 self.select_cols = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
284 self
285 }
286
287 /// Add a raw `SELECT` expression (verbatim, NOT escaped) with optional binds
288 /// — the escape hatch for aggregates/functions like `COUNT(*)`.
289 ///
290 /// Appended to the column list after any [`Self::select`] columns. Multiple
291 /// calls accumulate.
292 ///
293 /// # Warning: positional placeholder contract
294 ///
295 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and any
296 /// `binds` are appended to the running bind list in order. For **Postgres**,
297 /// the caller MUST write `$N` numbers matching the actual bind position. For
298 /// MySQL/SQLite use `?`.
299 pub fn select_raw(mut self, sql: &str, binds: Option<Vec<Value>>) -> Self {
300 self.select_raw
301 .push((sql.to_owned(), binds.unwrap_or_default()));
302 self
303 }
304
305 /// Add a subquery `SELECT` column: emits `(<sub>) AS {alias}` after the
306 /// regular columns and any [`Self::select_raw`] expressions.
307 ///
308 /// The subquery is compiled with placeholder continuity (its binds appear in
309 /// `$N` order at the point it is emitted — before the `WHERE` clause, since
310 /// the SELECT list is rendered first). SELECT-only.
311 pub fn select_subquery(mut self, alias: &str, sub: QueryBuilder<D>) -> Self {
312 self.select_subqueries
313 .push((alias.to_owned(), Box::new(sub)));
314 self
315 }
316
317 /// Emit `SELECT DISTINCT …` (all dialects).
318 pub fn distinct(mut self) -> Self {
319 self.distinct = true;
320 self
321 }
322
323 /// Emit `SELECT DISTINCT ON (cols) …` — **Postgres only**.
324 ///
325 /// `cols` are raw identifiers (escaped at compile time). Compiling against a
326 /// dialect without `DISTINCT ON` support panics
327 /// (`DISTINCT ON requires PostgreSQL`).
328 pub fn distinct_on<I, S>(mut self, cols: I) -> Self
329 where
330 I: IntoIterator<Item = S>,
331 S: AsRef<str>,
332 {
333 self.distinct_on = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
334 self.distinct = true;
335 self
336 }
337
338 /// `col ILIKE val` — dialect-aware case-insensitive match.
339 ///
340 /// On **Postgres** this compiles to the native `{col} ILIKE {ph}`. On
341 /// MySQL/SQLite (no native `ILIKE`) it compiles to
342 /// `LOWER({col}) LIKE LOWER({ph})`.
343 pub fn where_ilike(mut self, col: &str, val: impl IntoBind) -> Self {
344 self.wheres.push(Predicate::ILike {
345 col: col.to_owned(),
346 val: val.into_bind(),
347 });
348 self
349 }
350
351 /// `col @> val` — JSONB containment.
352 ///
353 /// **Postgres-specific:** the `@>` operator is emitted verbatim for all
354 /// dialects, but is only meaningful on Postgres `jsonb` columns. `val` is
355 /// typically a JSON text string (or `Value::Json` behind the `json`
356 /// feature).
357 pub fn where_jsonb_contains(mut self, col: &str, val: impl IntoBind) -> Self {
358 self.wheres.push(Predicate::JsonContains {
359 col: col.to_owned(),
360 val: val.into_bind(),
361 });
362 self
363 }
364
365 fn binary(mut self, col: &str, op: &'static str, val: impl IntoBind) -> Self {
366 self.wheres.push(Predicate::Binary {
367 col: col.to_owned(),
368 op,
369 val: val.into_bind(),
370 });
371 self
372 }
373
374 /// `col = val`.
375 pub fn where_eq(self, col: &str, val: impl IntoBind) -> Self {
376 self.binary(col, "=", val)
377 }
378
379 /// `col != val`.
380 pub fn where_ne(self, col: &str, val: impl IntoBind) -> Self {
381 self.binary(col, "!=", val)
382 }
383
384 /// `col > val`.
385 pub fn where_gt(self, col: &str, val: impl IntoBind) -> Self {
386 self.binary(col, ">", val)
387 }
388
389 /// `col >= val`.
390 pub fn where_gte(self, col: &str, val: impl IntoBind) -> Self {
391 self.binary(col, ">=", val)
392 }
393
394 /// `col < val`.
395 pub fn where_lt(self, col: &str, val: impl IntoBind) -> Self {
396 self.binary(col, "<", val)
397 }
398
399 /// `col <= val`.
400 pub fn where_lte(self, col: &str, val: impl IntoBind) -> Self {
401 self.binary(col, "<=", val)
402 }
403
404 /// `col LIKE val`.
405 pub fn where_like(self, col: &str, val: impl IntoBind) -> Self {
406 self.binary(col, "LIKE", val)
407 }
408
409 fn in_(mut self, col: &str, neg: bool, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
410 self.wheres.push(Predicate::In {
411 col: col.to_owned(),
412 neg,
413 vals: vals.into_iter().map(IntoBind::into_bind).collect(),
414 });
415 self
416 }
417
418 /// `col IN (...)`.
419 pub fn where_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
420 self.in_(col, false, vals)
421 }
422
423 /// `col NOT IN (...)`.
424 pub fn where_not_in(self, col: &str, vals: impl IntoIterator<Item = impl IntoBind>) -> Self {
425 self.in_(col, true, vals)
426 }
427
428 fn null(mut self, col: &str, neg: bool) -> Self {
429 self.wheres.push(Predicate::Null {
430 col: col.to_owned(),
431 neg,
432 });
433 self
434 }
435
436 /// `col IS NULL`.
437 pub fn where_null(self, col: &str) -> Self {
438 self.null(col, false)
439 }
440
441 /// `col IS NOT NULL`.
442 pub fn where_not_null(self, col: &str) -> Self {
443 self.null(col, true)
444 }
445
446 /// `col BETWEEN lo AND hi`.
447 pub fn where_between(mut self, col: &str, lo: impl IntoBind, hi: impl IntoBind) -> Self {
448 self.wheres.push(Predicate::Between {
449 col: col.to_owned(),
450 lo: lo.into_bind(),
451 hi: hi.into_bind(),
452 });
453 self
454 }
455
456 /// Raw SQL predicate with its own binds — the verbatim escape hatch.
457 ///
458 /// # Warning: positional placeholder contract
459 ///
460 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
461 /// `binds` are appended to the running bind list in order. For
462 /// **Postgres**, the caller MUST write `$N` numbers matching the actual
463 /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
464 /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
465 /// produces a malformed query.
466 pub fn where_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
467 self.wheres.push(Predicate::Raw {
468 sql: sql.to_owned(),
469 binds,
470 });
471 self
472 }
473
474 /// `lhs op rhs` — compare two column identifiers (both escaped at compile
475 /// time), no bind. e.g. `where_column("orders.user_id", "=", "users.id")`.
476 pub fn where_column(mut self, lhs: &str, op: &'static str, rhs: &str) -> Self {
477 self.wheres.push(Predicate::Column {
478 lhs: lhs.to_owned(),
479 op,
480 rhs: rhs.to_owned(),
481 });
482 self
483 }
484
485 /// `EXISTS (subquery)` — takes an already-built sub-builder by value
486 /// (mirrors [`Self::union`] / [`Self::with`]). The sub-query is compiled
487 /// with placeholder continuity.
488 pub fn where_exists(mut self, sub: QueryBuilder<D>) -> Self {
489 self.wheres.push(Predicate::Exists {
490 neg: false,
491 sub: Box::new(sub),
492 });
493 self
494 }
495
496 /// `NOT EXISTS (subquery)`. See [`Self::where_exists`].
497 pub fn where_not_exists(mut self, sub: QueryBuilder<D>) -> Self {
498 self.wheres.push(Predicate::Exists {
499 neg: true,
500 sub: Box::new(sub),
501 });
502 self
503 }
504
505 /// `col IN (subquery)` — takes an already-built sub-builder by value. The
506 /// sub-query is compiled with placeholder continuity.
507 pub fn where_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
508 self.wheres.push(Predicate::InSubquery {
509 col: col.to_owned(),
510 neg: false,
511 sub: Box::new(sub),
512 });
513 self
514 }
515
516 /// `col NOT IN (subquery)`. See [`Self::where_in_subquery`].
517 pub fn where_not_in_subquery(mut self, col: &str, sub: QueryBuilder<D>) -> Self {
518 self.wheres.push(Predicate::InSubquery {
519 col: col.to_owned(),
520 neg: true,
521 sub: Box::new(sub),
522 });
523 self
524 }
525
526 fn group(
527 mut self,
528 outer_conj: Conj,
529 f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>,
530 ) -> Self {
531 let preds = f(WhereBuilder::new()).into_preds();
532 self.wheres.push(Predicate::Group { outer_conj, preds });
533 self
534 }
535
536 /// Add a parenthesized `AND (...)` group built by the closure.
537 pub fn and_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
538 self.group(Conj::And, f)
539 }
540
541 /// Add a parenthesized `OR (...)` group built by the closure.
542 pub fn or_where(self, f: impl FnOnce(WhereBuilder<D>) -> WhereBuilder<D>) -> Self {
543 self.group(Conj::Or, f)
544 }
545
546 /// Build an `INSERT` from a single row of `(column, value)` pairs.
547 pub fn insert<K, V, I>(mut self, row: I) -> Self
548 where
549 K: AsRef<str>,
550 V: IntoBind,
551 I: IntoIterator<Item = (K, V)>,
552 {
553 self.method = Method::Insert;
554 self.set = row
555 .into_iter()
556 .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
557 .collect();
558 self
559 }
560
561 /// Build a multi-row `INSERT` from an iterator of rows, each a sequence of
562 /// `(column, value)` pairs.
563 ///
564 /// The inserted column set is taken from the **first** row's keys (sorted, as
565 /// with [`Self::insert`]). For each subsequent row, a value is bound for every
566 /// column in that set; a key **missing** in a later row binds `Value::Null`
567 /// rather than panicking (DoS-safe, matching the 1.x hardening). Composes with
568 /// `on_conflict_*` and `returning`.
569 pub fn insert_many<K, V, R, Rows>(mut self, rows: Rows) -> Self
570 where
571 K: AsRef<str>,
572 V: IntoBind,
573 R: IntoIterator<Item = (K, V)>,
574 Rows: IntoIterator<Item = R>,
575 {
576 self.method = Method::Insert;
577 self.insert_rows = rows
578 .into_iter()
579 .map(|row| {
580 row.into_iter()
581 .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
582 .collect()
583 })
584 .collect();
585 self
586 }
587
588 /// Build an `UPDATE` from `(column, value)` pairs. WHERE still applies.
589 pub fn update<K, V, I>(mut self, set: I) -> Self
590 where
591 K: AsRef<str>,
592 V: IntoBind,
593 I: IntoIterator<Item = (K, V)>,
594 {
595 self.method = Method::Update;
596 self.set = set
597 .into_iter()
598 .map(|(k, v)| (k.as_ref().to_owned(), v.into_bind()))
599 .collect();
600 self
601 }
602
603 /// Build a `DELETE`. WHERE still applies.
604 pub fn delete(mut self) -> Self {
605 self.method = Method::Delete;
606 self
607 }
608
609 /// On conflict, skip the row (`INSERT`-only; ignored on UPDATE/DELETE).
610 ///
611 /// `targets` are the conflict-target columns (may be empty).
612 ///
613 /// - **Postgres / SQLite:** emits `ON CONFLICT ({targets}) DO NOTHING`, or
614 /// bare `ON CONFLICT DO NOTHING` when `targets` is empty.
615 /// - **MySQL:** emits `INSERT IGNORE INTO …` (no trailing clause). Note that
616 /// `IGNORE` suppresses *more* than duplicate-key errors (also truncation
617 /// and bad-value coercion) — broader than pg/sqlite `DO NOTHING`.
618 pub fn on_conflict_do_nothing<I, S>(mut self, targets: I) -> Self
619 where
620 I: IntoIterator<Item = S>,
621 S: AsRef<str>,
622 {
623 self.on_conflict = Some(OnConflict {
624 targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
625 action: ConflictAction::DoNothing,
626 });
627 self
628 }
629
630 /// On conflict, update the non-target inserted columns from the proposed row
631 /// (`INSERT`-only; ignored on UPDATE/DELETE).
632 ///
633 /// - **Postgres / SQLite:** emits
634 /// `ON CONFLICT ({targets}) DO UPDATE SET {c} = EXCLUDED.{c}, …` for every
635 /// inserted column *except* the conflict targets. If `targets` is empty or
636 /// covers all inserted columns (empty SET list), falls back to the
637 /// `DO NOTHING` rendering (pg/sqlite require a target for `DO UPDATE`).
638 /// - **MySQL:** the explicit `targets` are **ignored** (MySQL uses its own
639 /// unique/primary keys); emits
640 /// `ON DUPLICATE KEY UPDATE {c} = VALUES({c}), …` for *all* inserted
641 /// columns. `VALUES()` is used for MySQL 5.7/8.x compatibility. Including a
642 /// PK column in the insert set yields a redundant-but-harmless
643 /// `pk = VALUES(pk)`.
644 pub fn on_conflict_merge<I, S>(mut self, targets: I) -> Self
645 where
646 I: IntoIterator<Item = S>,
647 S: AsRef<str>,
648 {
649 self.on_conflict = Some(OnConflict {
650 targets: targets.into_iter().map(|c| c.as_ref().to_owned()).collect(),
651 action: ConflictAction::Merge,
652 });
653 self
654 }
655
656 /// Add a `RETURNING` column list. Works on INSERT / UPDATE / DELETE for
657 /// Postgres and SQLite; a `"*"` column is emitted unescaped (`RETURNING *`).
658 ///
659 /// On **MySQL** this is a silent no-op (MySQL has no `RETURNING`). On
660 /// **SQLite** `RETURNING` requires SQLite ≥ 3.35.0 (2021); `supports_returning()`
661 /// is a compile-time dialect flag, not a runtime version check.
662 pub fn returning<I, S>(mut self, cols: I) -> Self
663 where
664 I: IntoIterator<Item = S>,
665 S: AsRef<str>,
666 {
667 self.returning = cols.into_iter().map(|c| c.as_ref().to_owned()).collect();
668 self
669 }
670
671 /// Add `GROUP BY` columns (raw owned identifiers, escaped at compile time).
672 ///
673 /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
674 pub fn group_by<I, S>(mut self, cols: I) -> Self
675 where
676 I: IntoIterator<Item = S>,
677 S: AsRef<str>,
678 {
679 self.groups
680 .extend(cols.into_iter().map(|c| c.as_ref().to_owned()));
681 self
682 }
683
684 /// Add a raw `GROUP BY` fragment with its own binds — the verbatim escape
685 /// hatch. SELECT-only.
686 ///
687 /// The fragment is appended after any structured [`Self::group_by`] columns
688 /// within the same `GROUP BY` clause (e.g. `GROUP BY "a", <raw>`); if no
689 /// structured columns are present it becomes the whole `GROUP BY <raw>`.
690 ///
691 /// # Warning: positional placeholder contract
692 ///
693 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
694 /// `binds` are appended to the running bind list in order. For
695 /// **Postgres**, the caller MUST write `$N` numbers matching the actual
696 /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
697 /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
698 /// produces a malformed query.
699 pub fn group_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
700 self.group_by_raw = Some((sql.to_owned(), binds));
701 self
702 }
703
704 /// Add a raw `ORDER BY` fragment with its own binds — the verbatim escape
705 /// hatch. SELECT-only.
706 ///
707 /// The fragment is appended after any structured [`Self::order_by`] terms
708 /// within the same `ORDER BY` clause (e.g. `ORDER BY "a" ASC, <raw>`); if no
709 /// structured terms are present it becomes the whole `ORDER BY <raw>`.
710 ///
711 /// # Warning: positional placeholder contract
712 ///
713 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
714 /// `binds` are appended to the running bind list in order. For
715 /// **Postgres**, the caller MUST write `$N` numbers matching the actual
716 /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
717 /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
718 /// produces a malformed query.
719 pub fn order_by_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
720 self.order_by_raw = Some((sql.to_owned(), binds));
721 self
722 }
723
724 /// Add an `ORDER BY col <ord>` term. SELECT-only.
725 pub fn order_by(mut self, col: &str, ord: Order) -> Self {
726 self.orders.push((col.to_owned(), ord));
727 self
728 }
729
730 /// Add an `ORDER BY col ASC` term. SELECT-only.
731 pub fn order_by_asc(self, col: &str) -> Self {
732 self.order_by(col, Order::Asc)
733 }
734
735 /// Add an `ORDER BY col DESC` term. SELECT-only.
736 pub fn order_by_desc(self, col: &str) -> Self {
737 self.order_by(col, Order::Desc)
738 }
739
740 /// Set `LIMIT n` (bound as a placeholder). SELECT-only.
741 pub fn limit(mut self, n: i64) -> Self {
742 self.limit = Some(n);
743 self
744 }
745
746 /// Set `OFFSET n` (bound as a placeholder). SELECT-only.
747 ///
748 /// `offset` requires `limit`: compiling an offset without a limit panics
749 /// (`offset(...) requires limit(...)`), uniform across dialects since MySQL
750 /// rejects a bare `OFFSET`.
751 pub fn offset(mut self, n: i64) -> Self {
752 self.offset = Some(n);
753 self
754 }
755
756 fn push_join(
757 mut self,
758 kind: JoinKind,
759 table: &str,
760 f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
761 ) -> Self {
762 let on = f(JoinClause::new()).into_conds();
763 self.joins.push(Join {
764 kind,
765 table: table.to_owned(),
766 on,
767 });
768 self
769 }
770
771 /// `INNER JOIN table ON …` — conditions built by the closure.
772 ///
773 /// SELECT-only: ignored for INSERT/UPDATE/DELETE.
774 pub fn join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
775 self.push_join(JoinKind::Inner, table, f)
776 }
777
778 /// `LEFT JOIN table ON …`. SELECT-only.
779 pub fn left_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
780 self.push_join(JoinKind::Left, table, f)
781 }
782
783 /// `RIGHT JOIN table ON …`. SELECT-only.
784 pub fn right_join(self, table: &str, f: impl FnOnce(JoinClause<D>) -> JoinClause<D>) -> Self {
785 self.push_join(JoinKind::Right, table, f)
786 }
787
788 /// `FULL OUTER JOIN table ON …`. SELECT-only.
789 pub fn full_outer_join(
790 self,
791 table: &str,
792 f: impl FnOnce(JoinClause<D>) -> JoinClause<D>,
793 ) -> Self {
794 self.push_join(JoinKind::FullOuter, table, f)
795 }
796
797 /// `CROSS JOIN table` — takes **no** `ON` closure (a cross join has no
798 /// condition). SELECT-only.
799 pub fn cross_join(mut self, table: &str) -> Self {
800 self.joins.push(Join {
801 kind: JoinKind::Cross,
802 table: table.to_owned(),
803 on: Vec::new(),
804 });
805 self
806 }
807
808 /// `HAVING col op ?` — `col` is a real column/alias (escaped); value bound.
809 ///
810 /// For aggregate expressions like `COUNT(*) > ?`, use [`Self::having_raw`].
811 /// SELECT-only: ignored for INSERT/UPDATE/DELETE. Multiple HAVING terms are
812 /// joined by `AND`.
813 pub fn having(mut self, col: &str, op: &str, val: impl IntoBind) -> Self {
814 self.havings.push(Having::Col {
815 col: col.to_owned(),
816 op: op.to_owned(),
817 val: val.into_bind(),
818 });
819 self
820 }
821
822 /// Raw `HAVING` expression with its own binds — the verbatim escape hatch
823 /// for aggregates (e.g. `having_raw("COUNT(*) > ?", …)`).
824 ///
825 /// # Warning: positional placeholder contract
826 ///
827 /// `sql` is emitted **verbatim** (it is NOT escaped or renumbered) and
828 /// `binds` are appended to the running bind list in order. For
829 /// **Postgres**, the caller MUST write `$N` numbers matching the actual
830 /// bind position — that is, `number of binds already accumulated + 1`, `+2`,
831 /// … For MySQL/SQLite use `?`. No renumbering is performed, so a wrong `$N`
832 /// produces a malformed query.
833 pub fn having_raw(mut self, sql: &str, binds: Vec<Value>) -> Self {
834 self.havings.push(Having::Raw {
835 sql: sql.to_owned(),
836 binds,
837 });
838 self
839 }
840
841 /// Add a `WITH name AS (query)` common table expression. SELECT-only.
842 ///
843 /// CTE bodies are compiled before the main query, so their binds (and pg
844 /// `$N` numbers) appear first.
845 pub fn with(mut self, name: &str, query: QueryBuilder<D>) -> Self {
846 self.ctes.push(Cte {
847 name: name.to_owned(),
848 recursive: false,
849 query,
850 });
851 self
852 }
853
854 /// Add a recursive CTE. If any CTE is recursive, the single `WITH` carries
855 /// `RECURSIVE`. SELECT-only.
856 pub fn with_recursive(mut self, name: &str, query: QueryBuilder<D>) -> Self {
857 self.ctes.push(Cte {
858 name: name.to_owned(),
859 recursive: true,
860 query,
861 });
862 self
863 }
864
865 /// Append a `UNION query` arm. SELECT-only.
866 pub fn union(mut self, query: QueryBuilder<D>) -> Self {
867 self.unions.push((false, query));
868 self
869 }
870
871 /// Append a `UNION ALL query` arm. SELECT-only.
872 pub fn union_all(mut self, query: QueryBuilder<D>) -> Self {
873 self.unions.push((true, query));
874 self
875 }
876
877 /// Conditionally apply `f` to the builder, keeping the chain intact.
878 ///
879 /// Returns `f(self)` when `cond` is true, otherwise `self` unchanged. This
880 /// lets you add clauses based on a runtime flag without breaking the
881 /// by-value chain.
882 ///
883 /// ```
884 /// # #[cfg(feature = "v2")] {
885 /// use chain_builder::v2::{Postgres, QueryBuilder};
886 /// let only_active = true;
887 /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
888 /// .select(["id"])
889 /// .when(only_active, |q| q.where_eq("status", "active"))
890 /// .to_sql();
891 /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
892 /// # }
893 /// ```
894 pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
895 if cond {
896 f(self)
897 } else {
898 self
899 }
900 }
901
902 /// Apply `if_true` when `cond` holds, otherwise `if_false`, keeping the
903 /// chain intact.
904 ///
905 /// ```
906 /// # #[cfg(feature = "v2")] {
907 /// use chain_builder::v2::{Postgres, QueryBuilder};
908 /// let active = false;
909 /// let (sql, _) = QueryBuilder::<Postgres>::table("users")
910 /// .select(["id"])
911 /// .when_else(
912 /// active,
913 /// |q| q.where_eq("status", "active"),
914 /// |q| q.where_eq("status", "inactive"),
915 /// )
916 /// .to_sql();
917 /// assert_eq!(sql, r#"SELECT "id" FROM "users" WHERE "status" = $1"#);
918 /// # }
919 /// ```
920 pub fn when_else(
921 self,
922 cond: bool,
923 if_true: impl FnOnce(Self) -> Self,
924 if_false: impl FnOnce(Self) -> Self,
925 ) -> Self {
926 if cond {
927 if_true(self)
928 } else {
929 if_false(self)
930 }
931 }
932
933 /// Apply `LIMIT`/`OFFSET` for a **1-based** page: row window
934 /// `[(page-1) * per_page, page * per_page)`.
935 ///
936 /// Equivalent to `self.limit(per_page).offset((page - 1).max(0) * per_page)`.
937 /// A `page < 1` is treated as page 1 (offset 0), so callers never get a
938 /// negative offset. SELECT-only, like [`Self::limit`] / [`Self::offset`].
939 ///
940 /// ```
941 /// # #[cfg(feature = "v2")] {
942 /// use chain_builder::v2::{Postgres, QueryBuilder, Value};
943 /// let (sql, binds) = QueryBuilder::<Postgres>::table("users")
944 /// .select(["id"])
945 /// .paginate(2, 10)
946 /// .to_sql();
947 /// assert_eq!(sql, r#"SELECT "id" FROM "users" LIMIT $1 OFFSET $2"#);
948 /// assert_eq!(binds, vec![Value::I64(10), Value::I64(10)]);
949 /// # }
950 /// ```
951 pub fn paginate(self, page: i64, per_page: i64) -> Self {
952 self.limit(per_page).offset((page - 1).max(0) * per_page)
953 }
954
955 /// Compile to `(sql, binds)`.
956 pub fn to_sql(&self) -> (String, Vec<Value>) {
957 compile(self)
958 }
959}