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