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