Skip to main content

chain_builder/
compile.rs

1//! SQL compilation: turn a [`QueryBuilder`] into `(sql, binds)`.
2//!
3//! Values are never inlined — each value pushes onto a running `binds` vector
4//! and emits a placeholder via [`Dialect::write_placeholder`] with a 1-based
5//! counter, so Postgres yields `$1..$n` in first-appearance order (including
6//! across nested groups) while MySQL/SQLite yield `?`.
7
8use crate::builder::{
9    ConflictAction, Cte, Having, Join, JoinCond, JoinKind, Lock, LockStrength, LockWait, Method,
10    Order, QueryBuilder, SelectExpr, SetExpr,
11};
12use crate::dialect::{Dialect, UpsertStyle};
13use crate::error::BuildError;
14use crate::ident::escape_identifier;
15use crate::value::Value;
16use crate::where_::{Conj, Predicate};
17
18/// Accumulates the generated SQL and the ordered bind values.
19///
20/// A single `Ctx` is threaded through the whole compilation (see
21/// [`compile_into`]): SQL, binds, and the placeholder counter all continue
22/// across every clause and across nested builders, which is what guarantees
23/// Postgres placeholder continuity (e.g. WHERE `$1` → LIMIT `$2` → OFFSET `$3`).
24struct Ctx {
25    sql: String,
26    binds: Vec<Value>,
27    quote: char,
28}
29
30impl Ctx {
31    /// Push a value and emit its placeholder (1-based = len after push).
32    fn placeholder<D: Dialect>(&mut self, val: Value) {
33        self.binds.push(val);
34        D::write_placeholder(&mut self.sql, self.binds.len());
35    }
36
37    /// Escape a single identifier for SQL output.
38    ///
39    /// This is the ONLY place identifiers are turned into SQL in v2. Every
40    /// identifier→SQL site in this module routes through `ctx.esc`, so
41    /// `grep 'esc(' src/v2/compile.rs` is the complete inventory of identifier
42    /// writes. (The sole exception is [`Predicate::Raw`], which is the
43    /// documented verbatim escape hatch and is emitted unescaped.)
44    fn esc(&self, ident: &str) -> String {
45        escape_identifier(ident, self.quote)
46    }
47
48    /// Escape a table, optionally prefixed by a database qualifier:
49    /// `Some("db")`, `"t"` → `"db"."t"`; `None` → `"t"`.
50    fn qualify(&self, db: Option<&str>, table: &str) -> String {
51        match db {
52            Some(d) => format!("{}.{}", self.esc(d), self.esc(table)),
53            None => self.esc(table),
54        }
55    }
56}
57
58/// Compile a [`QueryBuilder`] into `(sql, binds)`, panicking on an invalid
59/// builder.
60///
61/// Panicking wrapper over [`try_compile`]; the panic message is the
62/// [`BuildError`]'s `Display` text. Prefer [`try_compile`] /
63/// [`QueryBuilder::try_to_sql`](crate::QueryBuilder::try_to_sql) when the
64/// builder may be fed from runtime input (e.g. an HTTP request).
65pub fn compile<D: Dialect>(qb: &QueryBuilder<D>) -> (String, Vec<Value>) {
66    try_compile(qb).unwrap_or_else(|e| panic!("{e}"))
67}
68
69/// Compile a [`QueryBuilder`] into `(sql, binds)`, or return the
70/// [`BuildError`] describing why the query cannot be rendered.
71pub fn try_compile<D: Dialect>(qb: &QueryBuilder<D>) -> Result<(String, Vec<Value>), BuildError> {
72    let mut ctx = Ctx {
73        sql: String::new(),
74        binds: Vec::new(),
75        quote: D::quote_char(),
76    };
77    compile_into::<D>(&mut ctx, qb)?;
78    Ok((ctx.sql, ctx.binds))
79}
80
81/// Write `qb`'s SQL into the existing `ctx`, continuing its binds and
82/// placeholder counter. This is the single-pass core used by [`compile`] (and,
83/// in later M2 tasks, by nested builders such as CTEs/UNION arms).
84fn compile_into<D: Dialect>(ctx: &mut Ctx, qb: &QueryBuilder<D>) -> Result<(), BuildError> {
85    // A builder method that detected misuse (e.g. `having()` with a disallowed
86    // operator) records the first error instead of panicking mid-chain; it
87    // surfaces here. Checked per nested builder too (CTE/UNION/subquery).
88    if let Some(e) = &qb.error {
89        return Err(e.clone());
90    }
91
92    let table = ctx.qualify(qb.db.as_deref(), &qb.table);
93
94    // A row lock is only meaningful on SELECT. Attaching one to INSERT/UPDATE/
95    // DELETE would otherwise be silently dropped — a dangerous no-op for a lock
96    // (the caller believes rows are locked when they are not). Fail loud,
97    // dialect-independent, like the `offset`/`distinct_on` guards.
98    if qb.lock.is_some() && qb.method != Method::Select {
99        return Err(BuildError::LockRequiresSelect);
100    }
101
102    match qb.method {
103        Method::Select => {
104            // CTEs are emitted first so their binds (and pg `$N`) come first.
105            write_ctes::<D>(ctx, &qb.ctes)?;
106            if !qb.distinct_on.is_empty() {
107                if !D::supports_distinct_on() {
108                    return Err(BuildError::DistinctOnRequiresPostgres);
109                }
110                ctx.sql.push_str("SELECT DISTINCT ON (");
111                let cols: Vec<String> = qb.distinct_on.iter().map(|c| ctx.esc(c)).collect();
112                ctx.sql.push_str(&cols.join(", "));
113                ctx.sql.push_str(") ");
114            } else if qb.distinct {
115                ctx.sql.push_str("SELECT DISTINCT ");
116            } else {
117                ctx.sql.push_str("SELECT ");
118            }
119            write_select_list::<D>(ctx, qb)?;
120            ctx.sql.push_str(" FROM ");
121            ctx.sql.push_str(&table);
122            write_joins::<D>(ctx, &qb.joins, qb.db.as_deref());
123            write_wheres::<D>(ctx, &qb.wheres)?;
124            write_group_by(ctx, &qb.groups, qb.group_by_raw.as_ref());
125            write_having::<D>(ctx, &qb.havings);
126            write_order_by(ctx, &qb.orders, qb.order_by_raw.as_ref());
127            write_limit_offset::<D>(ctx, qb.limit, qb.offset)?;
128            write_unions::<D>(ctx, &qb.unions)?;
129            write_lock::<D>(ctx, qb.lock.as_ref(), !qb.unions.is_empty())?;
130        }
131        Method::Insert => {
132            if qb.set.is_empty() && qb.insert_rows.is_empty() {
133                return Err(BuildError::EmptyInsert);
134            }
135
136            // Single-row sorted pairs (preserves the M-prev path byte-for-byte,
137            // including duplicate keys). For multi-row, columns come from the
138            // FIRST row's sorted keys.
139            let mut single_rows: Vec<&(String, Value)> = qb.set.iter().collect();
140            single_rows.sort_by(|a, b| a.0.cmp(&b.0));
141            let sorted_cols: Vec<&str> = if !qb.insert_rows.is_empty() {
142                let mut cols: Vec<&str> =
143                    qb.insert_rows[0].iter().map(|(k, _)| k.as_str()).collect();
144                cols.sort_unstable();
145                cols
146            } else {
147                single_rows.iter().map(|(k, _)| k.as_str()).collect()
148            };
149
150            // Decide the INSERT keyword up front: MySQL `DO NOTHING` becomes
151            // `INSERT IGNORE INTO …` with no trailing conflict clause.
152            let mysql_ignore = D::upsert_style() == UpsertStyle::OnDuplicateKey
153                && matches!(
154                    qb.on_conflict.as_ref().map(|c| c.action),
155                    Some(ConflictAction::DoNothing)
156                );
157            if mysql_ignore {
158                ctx.sql.push_str("INSERT IGNORE INTO ");
159            } else {
160                ctx.sql.push_str("INSERT INTO ");
161            }
162            ctx.sql.push_str(&table);
163            ctx.sql.push_str(" (");
164            let cols: Vec<String> = sorted_cols.iter().map(|k| ctx.esc(k)).collect();
165            ctx.sql.push_str(&cols.join(", "));
166            ctx.sql.push_str(") VALUES ");
167
168            if !qb.insert_rows.is_empty() {
169                // Multi-row: one `(…)` tuple per row. A key missing in a row binds
170                // `Value::Null` (ragged rows are NULL-padded, never a panic).
171                for (ri, row) in qb.insert_rows.iter().enumerate() {
172                    if ri > 0 {
173                        ctx.sql.push_str(", ");
174                    }
175                    ctx.sql.push('(');
176                    for (ci, col) in sorted_cols.iter().enumerate() {
177                        if ci > 0 {
178                            ctx.sql.push_str(", ");
179                        }
180                        let v = row
181                            .iter()
182                            .find(|(k, _)| k == col)
183                            .map(|(_, v)| v.clone())
184                            .unwrap_or(Value::Null);
185                        ctx.placeholder::<D>(v);
186                    }
187                    ctx.sql.push(')');
188                }
189            } else {
190                // Single-row: byte-identical to the M-prev path (iterate the
191                // sorted (key, value) pairs directly, duplicates and all).
192                ctx.sql.push('(');
193                for (i, (_, v)) in single_rows.iter().enumerate() {
194                    if i > 0 {
195                        ctx.sql.push_str(", ");
196                    }
197                    ctx.placeholder::<D>(v.clone());
198                }
199                ctx.sql.push(')');
200            }
201
202            if !mysql_ignore {
203                if let Some(oc) = &qb.on_conflict {
204                    write_on_conflict::<D>(ctx, oc, &sorted_cols);
205                }
206            }
207            write_returning::<D>(ctx, &qb.returning);
208        }
209        Method::Update => {
210            if qb.set.is_empty() && qb.set_exprs.is_empty() {
211                return Err(BuildError::EmptyUpdate);
212            }
213            let mut rows: Vec<&(String, Value)> = qb.set.iter().collect();
214            rows.sort_by(|a, b| a.0.cmp(&b.0));
215            ctx.sql.push_str("UPDATE ");
216            ctx.sql.push_str(&table);
217            ctx.sql.push_str(" SET ");
218            for (i, (k, v)) in rows.iter().enumerate() {
219                if i > 0 {
220                    ctx.sql.push_str(", ");
221                }
222                let col = ctx.esc(k);
223                ctx.sql.push_str(&col);
224                ctx.sql.push_str(" = ");
225                ctx.placeholder::<D>(v.clone());
226            }
227            for (i, ex) in qb.set_exprs.iter().enumerate() {
228                if i > 0 || !rows.is_empty() {
229                    ctx.sql.push_str(", ");
230                }
231                match ex {
232                    SetExpr::Raw { col, expr, binds } => {
233                        let col = ctx.esc(col);
234                        ctx.sql.push_str(&col);
235                        ctx.sql.push_str(" = ");
236                        // Verbatim escape hatch (see `set_raw` docs).
237                        ctx.sql.push_str(expr);
238                        ctx.binds.extend(binds.iter().cloned());
239                    }
240                    SetExpr::Step { col, by, neg } => {
241                        let col = ctx.esc(col);
242                        ctx.sql.push_str(&col);
243                        ctx.sql.push_str(" = ");
244                        ctx.sql.push_str(&col);
245                        ctx.sql.push_str(if *neg { " - " } else { " + " });
246                        ctx.placeholder::<D>(by.clone());
247                    }
248                }
249            }
250            write_wheres::<D>(ctx, &qb.wheres)?;
251            write_returning::<D>(ctx, &qb.returning);
252        }
253        Method::Delete => {
254            ctx.sql.push_str("DELETE FROM ");
255            ctx.sql.push_str(&table);
256            write_wheres::<D>(ctx, &qb.wheres)?;
257            write_returning::<D>(ctx, &qb.returning);
258        }
259    }
260    Ok(())
261}
262
263/// Render the upsert conflict clause for an `INSERT` (never called for the
264/// MySQL `INSERT IGNORE` path, which is handled at the keyword). `inserted` is
265/// the sorted-key list of inserted columns.
266fn write_on_conflict<D: Dialect>(
267    ctx: &mut Ctx,
268    oc: &crate::builder::OnConflict,
269    inserted: &[&str],
270) {
271    match D::upsert_style() {
272        UpsertStyle::OnDuplicateKey => {
273            // MySQL merge: `ON DUPLICATE KEY UPDATE c = VALUES(c), …` for ALL
274            // inserted columns (explicit targets are ignored).
275            ctx.sql.push_str(" ON DUPLICATE KEY UPDATE ");
276            let sets: Vec<String> = inserted
277                .iter()
278                .map(|c| {
279                    let e = ctx.esc(c);
280                    format!("{e} = VALUES({e})")
281                })
282                .collect();
283            ctx.sql.push_str(&sets.join(", "));
284        }
285        UpsertStyle::OnConflict => {
286            let targets = &oc.targets;
287            // SET list = inserted columns minus the conflict targets.
288            let set_cols: Vec<&&str> = inserted
289                .iter()
290                .filter(|c| !targets.iter().any(|t| t == **c))
291                .collect();
292            let do_update = matches!(oc.action, ConflictAction::Merge)
293                && !targets.is_empty()
294                && !set_cols.is_empty();
295
296            ctx.sql.push_str(" ON CONFLICT");
297            if !targets.is_empty() {
298                ctx.sql.push_str(" (");
299                let cols: Vec<String> = targets.iter().map(|t| ctx.esc(t)).collect();
300                ctx.sql.push_str(&cols.join(", "));
301                ctx.sql.push(')');
302            }
303            if do_update {
304                ctx.sql.push_str(" DO UPDATE SET ");
305                let sets: Vec<String> = set_cols
306                    .iter()
307                    .map(|c| {
308                        let e = ctx.esc(c);
309                        // `EXCLUDED` is an unquoted, case-insensitive identifier
310                        // accepted by both pg and sqlite — emitted literally.
311                        format!("{e} = EXCLUDED.{e}")
312                    })
313                    .collect();
314                ctx.sql.push_str(&sets.join(", "));
315            } else {
316                ctx.sql.push_str(" DO NOTHING");
317            }
318        }
319    }
320}
321
322/// Render ` RETURNING col, …` when the dialect supports it and the list is
323/// non-empty. A `"*"` column is emitted unescaped. No-op otherwise (e.g. MySQL).
324fn write_returning<D: Dialect>(ctx: &mut Ctx, cols: &[String]) {
325    if !D::supports_returning() || cols.is_empty() {
326        return;
327    }
328    ctx.sql.push_str(" RETURNING ");
329    let parts: Vec<String> = cols
330        .iter()
331        .map(|c| if c == "*" { "*".to_owned() } else { ctx.esc(c) })
332        .collect();
333    ctx.sql.push_str(&parts.join(", "));
334}
335
336/// Render `GROUP BY a, b, …` (SELECT only). No-op when there are no columns and
337/// no raw fragment. A raw fragment is appended (comma-joined) after the
338/// structured columns; if only raw is present it becomes the whole clause.
339fn write_group_by(ctx: &mut Ctx, groups: &[String], raw: Option<&(String, Vec<Value>)>) {
340    if groups.is_empty() && raw.is_none() {
341        return;
342    }
343    ctx.sql.push_str(" GROUP BY ");
344    let cols: Vec<String> = groups.iter().map(|c| ctx.esc(c)).collect();
345    ctx.sql.push_str(&cols.join(", "));
346    if let Some((sql, binds)) = raw {
347        if !groups.is_empty() {
348            ctx.sql.push_str(", ");
349        }
350        // Verbatim escape hatch (see `group_by_raw` docs).
351        ctx.sql.push_str(sql);
352        ctx.binds.extend(binds.iter().cloned());
353    }
354}
355
356/// Render the SELECT column list: escaped `select_cols`, then verbatim
357/// `select_raw` expressions, then `(<subquery>) AS {alias}` columns — in that
358/// order. An empty list (no cols, no raw, no subqueries) yields `*`.
359///
360/// Written directly into `ctx` (not pre-joined) so subquery binds continue the
361/// placeholder counter in emission order.
362fn write_select_list<D: Dialect>(ctx: &mut Ctx, qb: &QueryBuilder<D>) -> Result<(), BuildError> {
363    if qb.select_cols.is_empty()
364        && qb.select_exprs.is_empty()
365        && qb.select_raw.is_empty()
366        && qb.select_subqueries.is_empty()
367    {
368        ctx.sql.push('*');
369        return Ok(());
370    }
371    let mut wrote_any = false;
372    for c in &qb.select_cols {
373        if wrote_any {
374            ctx.sql.push_str(", ");
375        }
376        let e = ctx.esc(c);
377        ctx.sql.push_str(&e);
378        wrote_any = true;
379    }
380    for expr in &qb.select_exprs {
381        if wrote_any {
382            ctx.sql.push_str(", ");
383        }
384        match expr {
385            SelectExpr::Agg { func, col, alias } => {
386                ctx.sql.push_str(func.as_str());
387                ctx.sql.push('(');
388                // A `*` column is emitted unescaped (`COUNT(*)`).
389                if col == "*" {
390                    ctx.sql.push('*');
391                } else {
392                    let c = ctx.esc(col);
393                    ctx.sql.push_str(&c);
394                }
395                ctx.sql.push(')');
396                if let Some(a) = alias {
397                    let a = ctx.esc(a);
398                    ctx.sql.push_str(" AS ");
399                    ctx.sql.push_str(&a);
400                }
401            }
402            SelectExpr::ColAs { col, alias } => {
403                let c = ctx.esc(col);
404                let a = ctx.esc(alias);
405                ctx.sql.push_str(&c);
406                ctx.sql.push_str(" AS ");
407                ctx.sql.push_str(&a);
408            }
409        }
410        wrote_any = true;
411    }
412    for (sql, binds) in &qb.select_raw {
413        if wrote_any {
414            ctx.sql.push_str(", ");
415        }
416        // Verbatim escape hatch (see `select_raw` docs).
417        ctx.sql.push_str(sql);
418        ctx.binds.extend(binds.iter().cloned());
419        wrote_any = true;
420    }
421    for (alias, sub) in &qb.select_subqueries {
422        if wrote_any {
423            ctx.sql.push_str(", ");
424        }
425        ctx.sql.push('(');
426        compile_into::<D>(ctx, sub)?;
427        ctx.sql.push_str(") AS ");
428        let a = ctx.esc(alias);
429        ctx.sql.push_str(&a);
430        wrote_any = true;
431    }
432    Ok(())
433}
434
435/// Render each `JOIN` (SELECT only): ` {KIND} {esc table}[ ON cond AND …]`.
436/// `CROSS JOIN` emits no `ON`. Placeholders from `OnVal`/`OnRaw` continue the
437/// running counter.
438fn write_joins<D: Dialect>(ctx: &mut Ctx, joins: &[Join], db: Option<&str>) {
439    for j in joins {
440        let kw = match j.kind {
441            JoinKind::Inner => " INNER JOIN ",
442            JoinKind::Left => " LEFT JOIN ",
443            JoinKind::Right => " RIGHT JOIN ",
444            JoinKind::FullOuter => " FULL OUTER JOIN ",
445            JoinKind::Cross => " CROSS JOIN ",
446        };
447        ctx.sql.push_str(kw);
448        let table = ctx.qualify(db, &j.table);
449        ctx.sql.push_str(&table);
450        if j.on.is_empty() {
451            continue;
452        }
453        ctx.sql.push_str(" ON ");
454        for (i, cond) in j.on.iter().enumerate() {
455            if i > 0 {
456                ctx.sql.push_str(" AND ");
457            }
458            match cond {
459                JoinCond::On(c, op, c2) => {
460                    let l = ctx.esc(c);
461                    let r = ctx.esc(c2);
462                    ctx.sql.push_str(&l);
463                    ctx.sql.push(' ');
464                    ctx.sql.push_str(op);
465                    ctx.sql.push(' ');
466                    ctx.sql.push_str(&r);
467                }
468                JoinCond::OnVal(c, op, v) => {
469                    let l = ctx.esc(c);
470                    ctx.sql.push_str(&l);
471                    ctx.sql.push(' ');
472                    ctx.sql.push_str(op);
473                    ctx.sql.push(' ');
474                    ctx.placeholder::<D>(v.clone());
475                }
476                JoinCond::OnRaw(sql, binds) => {
477                    // Verbatim escape hatch (see `JoinClause::on_raw` docs).
478                    ctx.sql.push_str(sql);
479                    ctx.binds.extend(binds.iter().cloned());
480                }
481            }
482        }
483    }
484}
485
486/// Render ` HAVING cond AND …` (SELECT only) after GROUP BY. No-op when empty.
487fn write_having<D: Dialect>(ctx: &mut Ctx, havings: &[Having]) {
488    if havings.is_empty() {
489        return;
490    }
491    ctx.sql.push_str(" HAVING ");
492    for (i, h) in havings.iter().enumerate() {
493        if i > 0 {
494            ctx.sql.push_str(" AND ");
495        }
496        match h {
497            Having::Col { col, op, val } => {
498                let c = ctx.esc(col);
499                ctx.sql.push_str(&c);
500                ctx.sql.push(' ');
501                ctx.sql.push_str(op);
502                ctx.sql.push(' ');
503                ctx.placeholder::<D>(val.clone());
504            }
505            Having::Raw { sql, binds } => {
506                // Verbatim escape hatch (see `having_raw` docs).
507                ctx.sql.push_str(sql);
508                ctx.binds.extend(binds.iter().cloned());
509            }
510        }
511    }
512}
513
514/// Render `WITH [RECURSIVE] name AS (body), … ` BEFORE the main SELECT.
515///
516/// Single-pass per CTE: the name header is written and the body compiled in one
517/// go, so SQL text order equals bind-push order (placeholder/bind never desync).
518fn write_ctes<D: Dialect>(ctx: &mut Ctx, ctes: &[Cte<D>]) -> Result<(), BuildError> {
519    if ctes.is_empty() {
520        return Ok(());
521    }
522    ctx.sql.push_str("WITH ");
523    if ctes.iter().any(|c| c.recursive) {
524        ctx.sql.push_str("RECURSIVE ");
525    }
526    for (i, cte) in ctes.iter().enumerate() {
527        if i > 0 {
528            ctx.sql.push_str(", ");
529        }
530        let name = ctx.esc(&cte.name);
531        ctx.sql.push_str(&name);
532        ctx.sql.push_str(" AS (");
533        compile_into::<D>(ctx, &cte.query)?;
534        ctx.sql.push(')');
535    }
536    ctx.sql.push(' ');
537    Ok(())
538}
539
540/// Render ` UNION [ALL] body` per arm, AFTER the main query (SELECT only).
541fn write_unions<D: Dialect>(
542    ctx: &mut Ctx,
543    unions: &[(bool, QueryBuilder<D>)],
544) -> Result<(), BuildError> {
545    for (all, arm) in unions {
546        ctx.sql
547            .push_str(if *all { " UNION ALL " } else { " UNION " });
548        compile_into::<D>(ctx, arm)?;
549    }
550    Ok(())
551}
552
553/// Render `ORDER BY a ASC, b DESC, …` (SELECT only). No-op when empty and no raw
554/// fragment. A raw fragment is appended (comma-joined) after the structured
555/// terms; if only raw is present it becomes the whole clause.
556fn write_order_by(ctx: &mut Ctx, orders: &[(String, Order)], raw: Option<&(String, Vec<Value>)>) {
557    if orders.is_empty() && raw.is_none() {
558        return;
559    }
560    ctx.sql.push_str(" ORDER BY ");
561    let cols: Vec<String> = orders
562        .iter()
563        .map(|(c, o)| {
564            let dir = match o {
565                Order::Asc => "ASC",
566                Order::Desc => "DESC",
567            };
568            format!("{} {}", ctx.esc(c), dir)
569        })
570        .collect();
571    ctx.sql.push_str(&cols.join(", "));
572    if let Some((sql, binds)) = raw {
573        if !orders.is_empty() {
574            ctx.sql.push_str(", ");
575        }
576        // Verbatim escape hatch (see `order_by_raw` docs).
577        ctx.sql.push_str(sql);
578        ctx.binds.extend(binds.iter().cloned());
579    }
580}
581
582/// Render `LIMIT $n [OFFSET $m]` (SELECT only), binding both values.
583///
584/// Errors if `offset` is set without `limit` (uniform across dialects; MySQL
585/// rejects bare `OFFSET`).
586fn write_limit_offset<D: Dialect>(
587    ctx: &mut Ctx,
588    limit: Option<i64>,
589    offset: Option<i64>,
590) -> Result<(), BuildError> {
591    if offset.is_some() && limit.is_none() {
592        return Err(BuildError::OffsetWithoutLimit);
593    }
594    if let Some(n) = limit {
595        ctx.sql.push_str(" LIMIT ");
596        ctx.placeholder::<D>(Value::I64(n));
597    }
598    if let Some(n) = offset {
599        ctx.sql.push_str(" OFFSET ");
600        ctx.placeholder::<D>(Value::I64(n));
601    }
602    Ok(())
603}
604
605/// Render a row-locking clause (` FOR UPDATE`/` FOR SHARE` [+ ` SKIP LOCKED`/
606/// ` NOWAIT`]) at the end of a `SELECT`. A **no-op on dialects without row
607/// locking** (SQLite), so the lock is silently dropped there rather than
608/// producing invalid SQL. Errors if a lock is combined with `UNION` on a
609/// locking dialect (Postgres/MySQL reject that combination).
610fn write_lock<D: Dialect>(
611    ctx: &mut Ctx,
612    lock: Option<&Lock>,
613    has_unions: bool,
614) -> Result<(), BuildError> {
615    let Some(lock) = lock else {
616        return Ok(());
617    };
618    if !D::supports_row_locking() {
619        return Ok(());
620    }
621    // Postgres/MySQL reject `FOR UPDATE`/`FOR SHARE` on a `UNION` result; emitting
622    // it would produce invalid SQL, so fail loud here. (No-op dialects returned
623    // above never reach this, so a SQLite lock+UNION stays a harmless no-op.)
624    if has_unions {
625        return Err(BuildError::LockWithUnion);
626    }
627    ctx.sql.push_str(match lock.strength {
628        LockStrength::Update => " FOR UPDATE",
629        LockStrength::Share => " FOR SHARE",
630    });
631    if let Some(wait) = lock.wait {
632        ctx.sql.push_str(match wait {
633            LockWait::SkipLocked => " SKIP LOCKED",
634            LockWait::NoWait => " NOWAIT",
635        });
636    }
637    Ok(())
638}
639
640/// A predicate produces no SQL if it is an empty group (F4): an empty group
641/// would emit invalid `()`, so it is skipped entirely (and must not leave a
642/// dangling `AND`/`OR` separator behind it).
643fn is_omitted<D: Dialect>(p: &Predicate<D>) -> bool {
644    matches!(p, Predicate::Group { preds, .. } if preds.is_empty())
645}
646
647fn write_wheres<D: Dialect>(ctx: &mut Ctx, wheres: &[Predicate<D>]) -> Result<(), BuildError> {
648    // Skip empty groups so they neither emit `()` nor force a `WHERE`.
649    if wheres.iter().all(is_omitted) {
650        return Ok(());
651    }
652    ctx.sql.push_str(" WHERE ");
653    write_clause_list::<D>(ctx, wheres)
654}
655
656/// Render a top-level clause list. Predicates are joined by `AND` by default,
657/// but a [`Predicate::Group`] attaches to the preceding clause using its own
658/// outer conjunction (so `or_where` emits `... OR (...)`). Empty groups are
659/// omitted and never contribute a separator.
660fn write_clause_list<D: Dialect>(ctx: &mut Ctx, preds: &[Predicate<D>]) -> Result<(), BuildError> {
661    let mut wrote_any = false;
662    for p in preds.iter() {
663        if is_omitted(p) {
664            continue;
665        }
666        if wrote_any {
667            let sep = match p {
668                Predicate::Group {
669                    outer_conj: Conj::Or,
670                    ..
671                } => " OR ",
672                _ => " AND ",
673            };
674            ctx.sql.push_str(sep);
675        }
676        write_pred::<D>(ctx, p)?;
677        wrote_any = true;
678    }
679    Ok(())
680}
681
682fn write_pred<D: Dialect>(ctx: &mut Ctx, pred: &Predicate<D>) -> Result<(), BuildError> {
683    match pred {
684        Predicate::Binary { col, op, val } => {
685            let col = ctx.esc(col);
686            ctx.sql.push_str(&col);
687            ctx.sql.push(' ');
688            ctx.sql.push_str(op);
689            ctx.sql.push(' ');
690            ctx.placeholder::<D>(val.clone());
691        }
692        Predicate::In { col, neg, vals } => {
693            if vals.is_empty() {
694                // Empty IN is always false; empty NOT IN is always true.
695                ctx.sql.push_str(if *neg { "1 = 1" } else { "1 = 0" });
696                return Ok(());
697            }
698            let col = ctx.esc(col);
699            ctx.sql.push_str(&col);
700            ctx.sql.push_str(if *neg { " NOT IN (" } else { " IN (" });
701            for (i, v) in vals.iter().enumerate() {
702                if i > 0 {
703                    ctx.sql.push_str(", ");
704                }
705                ctx.placeholder::<D>(v.clone());
706            }
707            ctx.sql.push(')');
708        }
709        Predicate::Null { col, neg } => {
710            let col = ctx.esc(col);
711            ctx.sql.push_str(&col);
712            ctx.sql
713                .push_str(if *neg { " IS NOT NULL" } else { " IS NULL" });
714        }
715        Predicate::Between { col, lo, hi } => {
716            let col = ctx.esc(col);
717            ctx.sql.push_str(&col);
718            ctx.sql.push_str(" BETWEEN ");
719            ctx.placeholder::<D>(lo.clone());
720            ctx.sql.push_str(" AND ");
721            ctx.placeholder::<D>(hi.clone());
722        }
723        Predicate::ILike { col, val } => {
724            let col = ctx.esc(col);
725            if D::ilike_is_native() {
726                // Postgres: native `col ILIKE $n`.
727                ctx.sql.push_str(&col);
728                ctx.sql.push_str(" ILIKE ");
729                ctx.placeholder::<D>(val.clone());
730            } else {
731                // MySQL/SQLite: `LOWER(col) LIKE LOWER(?)`.
732                ctx.sql.push_str("LOWER(");
733                ctx.sql.push_str(&col);
734                ctx.sql.push_str(") LIKE LOWER(");
735                ctx.placeholder::<D>(val.clone());
736                ctx.sql.push(')');
737            }
738        }
739        Predicate::JsonContains { col, val } => {
740            // Postgres-oriented `@>` (jsonb contains); emitted verbatim.
741            let col = ctx.esc(col);
742            ctx.sql.push_str(&col);
743            ctx.sql.push_str(" @> ");
744            ctx.placeholder::<D>(val.clone());
745        }
746        Predicate::Raw { sql, binds } => {
747            // Verbatim escape hatch: SQL is NOT escaped (see `where_raw` docs).
748            ctx.sql.push_str(sql);
749            ctx.binds.extend(binds.iter().cloned());
750        }
751        Predicate::Group {
752            outer_conj: _,
753            preds,
754        } => {
755            // `outer_conj` controls how this group attaches to the *preceding*
756            // clause (handled in `write_clause_list`). The inner predicates are
757            // rendered with the SAME attach-conj logic as the top level
758            // (`write_clause_list`): each inner pred is joined with ` AND `
759            // unless it is itself a `Group` with `outer_conj == Conj::Or`, in
760            // which case it is joined with ` OR `. This enables M11 nested
761            // groups and inner-OR while staying byte-identical for the pre-M11
762            // case (a group whose preds are all non-`Group` predicates joins
763            // them all with ` AND `, exactly as the old hardcoded `Conj::And`).
764            //
765            // Empty groups never reach here: `write_clause_list` /
766            // `write_wheres` filter them via `is_omitted` (F4), so we never
767            // emit invalid `()`.
768            ctx.sql.push('(');
769            write_clause_list::<D>(ctx, preds)?;
770            ctx.sql.push(')');
771        }
772        Predicate::Column { lhs, op, rhs } => {
773            let l = ctx.esc(lhs);
774            let r = ctx.esc(rhs);
775            ctx.sql.push_str(&l);
776            ctx.sql.push(' ');
777            ctx.sql.push_str(op);
778            ctx.sql.push(' ');
779            ctx.sql.push_str(&r);
780        }
781        Predicate::Exists { neg, sub } => {
782            ctx.sql
783                .push_str(if *neg { "NOT EXISTS (" } else { "EXISTS (" });
784            compile_into::<D>(ctx, sub)?;
785            ctx.sql.push(')');
786        }
787        Predicate::InSubquery { col, neg, sub } => {
788            let col = ctx.esc(col);
789            ctx.sql.push_str(&col);
790            ctx.sql.push_str(if *neg { " NOT IN (" } else { " IN (" });
791            compile_into::<D>(ctx, sub)?;
792            ctx.sql.push(')');
793        }
794    }
795    Ok(())
796}