Skip to main content

siderite_backends/sql/compiler/
mod.rs

1//! `QueryPlan` / `WritePlan` → SQL compiler.
2//!
3//! [`compile`] and [`compile_write`] are the entry points. The compiler is a
4//! single-pass writer: [`Compiler`] appends SQL text and collects bind
5//! parameters; sibling modules add the expression, lookup and function
6//! grammar as further `impl` blocks.
7
8mod expr;
9mod function;
10mod lookup;
11#[cfg(test)]
12mod tests;
13#[cfg(test)]
14mod tests_mysql;
15#[cfg(all(test, feature = "mysql"))]
16mod tests_write_bare;
17
18use super::dialect::Dialect;
19use siderite_orm::expr::Ident;
20use siderite_orm::{
21    BackendKind, DistinctMode, Expr, JoinKind, LockMode, OrderDirection, OrmError, QueryError,
22    QueryPlan, QuerySource, SetOp, Value, WritePlan,
23};
24use std::fmt::Write;
25
26/// SQL text plus its bind parameters, in placeholder order.
27#[derive(Debug, Clone, PartialEq)]
28pub struct CompiledQuery {
29    /// Parameterised SQL.
30    pub sql: String,
31    /// Parameters to bind, in order.
32    pub params: Vec<Value>,
33}
34
35/// Compile `plan` for `dialect`.
36///
37/// # Errors
38/// [`OrmError::Capability`] if the plan needs a feature the dialect does not
39/// declare; [`QueryError::InvalidPlan`] for structural problems (unresolved
40/// relations, wrong function arity, `OuterRef` outside a subquery, ...).
41pub fn compile(plan: &QueryPlan, dialect: &dyn Dialect) -> Result<CompiledQuery, OrmError> {
42    plan.check(&dialect.capabilities())?;
43    let mut c = Compiler::new(dialect);
44    c.plan(plan);
45    c.finish()
46}
47
48/// Compile a write `plan` for `dialect`.
49///
50/// # Errors
51/// [`OrmError::Capability`] for unsupported features (e.g. `RETURNING`), and
52/// [`QueryError::InvalidPlan`] for an insert without rows or with rows whose
53/// length differs from the column list.
54pub fn compile_write(plan: &WritePlan, dialect: &dyn Dialect) -> Result<CompiledQuery, OrmError> {
55    compile_write_inner(plan, dialect, true)
56}
57
58/// Compile the statement body for an adapter emulating RETURNING itself.
59/// Other capability and structural checks remain mandatory.
60///
61/// Args:
62///     plan: Borrowed write plan, including emulated return columns.
63///     dialect: SQL grammar and supported capabilities.
64///
65/// Returns:
66///     Statement SQL and bind values, or a capability/validation error.
67#[cfg(feature = "mysql")]
68pub(crate) fn compile_write_bare(
69    plan: &WritePlan,
70    dialect: &dyn Dialect,
71) -> Result<CompiledQuery, OrmError> {
72    compile_write_inner(plan, dialect, false)
73}
74
75fn compile_write_inner(
76    plan: &WritePlan,
77    dialect: &dyn Dialect,
78    returning: bool,
79) -> Result<CompiledQuery, OrmError> {
80    let caps = dialect.capabilities();
81    for feature in plan.required_features() {
82        if returning || feature != siderite_orm::Feature::Returning {
83            caps.require(feature)?;
84        }
85    }
86    let mut c = Compiler::new(dialect);
87    match plan {
88        WritePlan::Insert(p) => {
89            if p.rows.is_empty() || p.rows.iter().any(|r| r.len() != p.columns.len()) {
90                return Err(QueryError::InvalidPlan(
91                    "insert rows must be non-empty and match the column list".into(),
92                )
93                .into());
94            }
95            c.push("INSERT INTO ");
96            c.ident(&p.table);
97            if p.columns.is_empty() {
98                // Only generated columns: one `DEFAULT VALUES` row per statement.
99                if p.rows.len() > 1 {
100                    return Err(QueryError::InvalidPlan(
101                        "multi-row insert needs at least one explicit column".into(),
102                    )
103                    .into());
104                }
105                c.push(dialect.default_values_sql());
106            } else {
107                c.push(" (");
108                c.list(&p.columns, ", ", |c, col| c.ident(col));
109                c.push(") VALUES ");
110                c.list(&p.rows, ", ", |c, row| {
111                    c.push("(");
112                    c.list(row, ", ", |c, v| c.value(v));
113                    c.push(")");
114                });
115            }
116        }
117        WritePlan::Update(p) => {
118            if p.assignments.is_empty() {
119                return Err(QueryError::InvalidPlan("update without assignments".into()).into());
120            }
121            let assignments = if dialect.evaluates_assignments_in_order() {
122                order_assignments(&p.assignments)?
123            } else {
124                p.assignments.clone()
125            };
126            c.write_scope(&p.table);
127            c.push("UPDATE ");
128            c.ident(&p.table);
129            c.push(" SET ");
130            c.list(&assignments, ", ", |c, (col, e)| {
131                c.ident(col);
132                c.push(" = ");
133                c.expr(e);
134            });
135            c.where_clause(p.filter.as_ref());
136        }
137        WritePlan::Delete(p) => {
138            c.write_scope(&p.table);
139            c.push("DELETE FROM ");
140            c.ident(&p.table);
141            c.where_clause(p.filter.as_ref());
142        }
143    }
144    // Dialects without `RETURNING` fail the capability check above, so this
145    // only ever writes a clause the dialect supports.
146    if returning {
147        c.returning(plan.returning());
148    }
149    c.finish()
150}
151
152/// Reorder `SET` assignments for a dialect that evaluates them left to right
153/// (MySQL), so that each right-hand side still reads the *old* value of every
154/// column, as the SQL standard (and PostgreSQL / SQLite) specify: an
155/// assignment that reads column `c` is placed before the one that sets `c`.
156///
157/// # Errors
158/// [`QueryError::InvalidPlan`] if the assignments read each other's columns in
159/// a cycle (`SET a = b, b = a`), which no order can express.
160fn order_assignments(assignments: &[(Ident, Expr)]) -> Result<Vec<(Ident, Expr)>, OrmError> {
161    let reads: Vec<Vec<String>> = assignments
162        .iter()
163        .map(|(_, e)| {
164            let mut columns = Vec::new();
165            expr_reads(e, 0, &mut columns);
166            columns
167        })
168        .collect();
169    let n = assignments.len();
170    // `before[i]`: assignments that must come before `i` (they read its column).
171    let before: Vec<Vec<usize>> = (0..n)
172        .map(|i| {
173            (0..n)
174                .filter(|&j| j != i && reads[j].iter().any(|c| *c == *assignments[i].0))
175                .collect()
176        })
177        .collect();
178    let mut order: Vec<usize> = Vec::with_capacity(n);
179    while order.len() < n {
180        let next =
181            (0..n).find(|i| !order.contains(i) && before[*i].iter().all(|j| order.contains(j)));
182        match next {
183            Some(i) => order.push(i),
184            None => {
185                return Err(QueryError::InvalidPlan(
186                    "MySQL evaluates SET assignments left to right; assignments that read \
187                     each other's columns in a cycle cannot be expressed"
188                        .into(),
189                )
190                .into());
191            }
192        }
193    }
194    Ok(order.into_iter().map(|i| assignments[i].clone()).collect())
195}
196
197/// Columns of the statement's own row that `expr` reads: plain columns at
198/// `depth` 0, and `OuterRef`s of first-level subqueries.
199fn expr_reads(expr: &Expr, depth: usize, out: &mut Vec<String>) {
200    match expr {
201        Expr::Column(c) if depth == 0 => out.push(c.name.to_string()),
202        Expr::OuterRef(c) if depth == 1 => out.push(c.name.to_string()),
203        _ => {}
204    }
205    for child in expr.children() {
206        expr_reads(child, depth, out);
207    }
208    if let Some(plan) = expr.subplan() {
209        plan_reads(plan, depth + 1, out);
210    }
211}
212
213fn plan_reads(plan: &QueryPlan, depth: usize, out: &mut Vec<String>) {
214    let distinct_on = match &plan.distinct {
215        DistinctMode::On(exprs) => exprs.as_slice(),
216        _ => &[],
217    };
218    let exprs = plan
219        .projection
220        .iter()
221        .map(|s| &s.expr)
222        .chain(plan.joins.iter().map(|j| &j.on))
223        .chain(plan.filter.iter())
224        .chain(plan.having.iter())
225        .chain(plan.grouping.iter())
226        .chain(plan.ordering.iter().map(|o| &o.expr))
227        .chain(distinct_on);
228    for e in exprs {
229        expr_reads(e, depth, out);
230    }
231    if let Some(inner) = plan.source.subquery.as_deref() {
232        plan_reads(inner, depth + 1, out);
233    }
234    for member in &plan.compound {
235        plan_reads(&member.plan, depth, out);
236    }
237}
238
239/// Whether `plan` (or a derived source, join or set-operation member of it)
240/// reads `table`.
241fn reads_table(plan: &QueryPlan, table: &str) -> bool {
242    plan.source.name == table
243        || plan.joins.iter().any(|j| j.source.name == table)
244        || plan
245            .source
246            .subquery
247            .as_deref()
248            .is_some_and(|inner| reads_table(inner, table))
249        || plan.compound.iter().any(|c| reads_table(&c.plan, table))
250}
251
252/// A query being compiled: the name its columns are qualified with, and
253/// whether qualification is needed (the query has joins).
254struct Scope {
255    reference: String,
256    qualify: bool,
257}
258
259struct Compiler<'d> {
260    dialect: &'d dyn Dialect,
261    native_ilike: bool,
262    kind: BackendKind,
263    sql: String,
264    params: Vec<Value>,
265    /// Enclosing queries, innermost last; `OuterRef` looks one level up.
266    scopes: Vec<Scope>,
267    /// Counter for generated derived-table aliases (`u1`, `u2`, ...).
268    derived: usize,
269    /// Target table of the `UPDATE` / `DELETE` being compiled, if any.
270    write_table: Option<String>,
271    /// First structural problem found; reported by [`finish`](Self::finish).
272    error: Option<QueryError>,
273}
274
275impl<'d> Compiler<'d> {
276    fn new(dialect: &'d dyn Dialect) -> Self {
277        let caps = dialect.capabilities();
278        Self {
279            dialect,
280            native_ilike: caps.ilike,
281            kind: caps.kind,
282            sql: String::new(),
283            params: Vec::new(),
284            scopes: Vec::new(),
285            derived: 0,
286            write_table: None,
287            error: None,
288        }
289    }
290
291    fn finish(self) -> Result<CompiledQuery, OrmError> {
292        match self.error {
293            Some(err) => Err(err.into()),
294            None => Ok(CompiledQuery {
295                sql: self.sql,
296                params: self.params,
297            }),
298        }
299    }
300
301    /// Record the first structural problem; later ones are ignored.
302    fn fail(&mut self, message: impl Into<String>) {
303        self.error
304            .get_or_insert_with(|| QueryError::InvalidPlan(message.into()));
305    }
306
307    fn returning(&mut self, columns: &[Ident]) {
308        if !columns.is_empty() {
309            self.push(" RETURNING ");
310            self.list(columns, ", ", |c, col| c.ident(col));
311        }
312    }
313
314    fn where_clause(&mut self, filter: Option<&siderite_orm::Expr>) {
315        if let Some(f) = filter {
316            self.push(" WHERE ");
317            self.expr(f);
318        }
319    }
320
321    fn push(&mut self, s: &str) {
322        self.sql.push_str(s);
323    }
324
325    fn ident(&mut self, ident: &str) {
326        self.dialect.write_ident(&mut self.sql, ident);
327    }
328
329    /// The scope of an `UPDATE` / `DELETE` target, so correlated subqueries in
330    /// its assignments and filter can refer to the row with `OuterRef`.
331    fn write_scope(&mut self, table: &str) {
332        self.write_table = Some(table.to_owned());
333        self.scopes.push(Scope {
334            reference: table.to_owned(),
335            qualify: false,
336        });
337    }
338
339    /// A subquery used as an operand (`IN (..)`, scalar).
340    ///
341    /// MySQL rejects `LIMIT` inside `IN (subquery)` (error 1235) and any
342    /// subquery over the table an `UPDATE` / `DELETE` modifies (error 1093).
343    /// Both are avoided by selecting from the subquery as a derived table.
344    fn operand_subquery(&mut self, plan: &QueryPlan, scalar: bool) {
345        let wrap = self.kind == BackendKind::MySql
346            && match &self.write_table {
347                Some(table) => !scalar || reads_table(plan, table),
348                None => !scalar && (plan.limit.is_some() || plan.offset.is_some()),
349            };
350        if !wrap {
351            self.plan(plan);
352            return;
353        }
354        self.derived += 1;
355        let alias = format!("u{}", self.derived);
356        self.push("SELECT * FROM (");
357        self.plan(plan);
358        self.push(") AS ");
359        self.ident(&alias);
360    }
361
362    /// A value operand: `NULL` is written as the keyword, everything else is bound.
363    ///
364    /// Spelling `NULL` out (instead of binding an untyped parameter) keeps the
365    /// statement text distinct from the same statement with a value, which
366    /// matters for PostgreSQL: SQLx caches prepared statements by text, and a
367    /// parameter whose type the server inferred from `NULL` would reject a
368    /// later binary value of another width.
369    fn value(&mut self, v: &Value) {
370        if v.is_null() {
371            self.push("NULL");
372        } else {
373            self.bind(v.clone());
374        }
375    }
376
377    fn bind(&mut self, v: Value) {
378        self.params.push(v);
379        self.dialect
380            .write_placeholder(&mut self.sql, self.params.len());
381    }
382
383    fn list<T>(&mut self, items: &[T], sep: &str, mut f: impl FnMut(&mut Self, &T)) {
384        for (i, item) in items.iter().enumerate() {
385            if i > 0 {
386                self.push(sep);
387            }
388            f(self, item);
389        }
390    }
391
392    /// A complete query: the core `SELECT`, its set-operation members, then
393    /// the `ORDER BY` / `LIMIT` / `OFFSET` / lock clauses of the whole result.
394    fn plan(&mut self, p: &QueryPlan) {
395        self.scopes.push(Scope {
396            reference: p.source.reference().to_string(),
397            qualify: !p.joins.is_empty(),
398        });
399        self.select(p);
400        for member in &p.compound {
401            self.push(match member.op {
402                SetOp::Union => " UNION ",
403                SetOp::UnionAll => " UNION ALL ",
404                SetOp::Intersect => " INTERSECT ",
405                SetOp::Except => " EXCEPT ",
406            });
407            self.set_member(&member.plan);
408        }
409        self.tail(p);
410        self.scopes.pop();
411    }
412
413    /// A member of a set operation. Members with their own `ORDER BY` /
414    /// `LIMIT` are wrapped in a derived table, which every dialect accepts
415    /// (bare parenthesised members are rejected by SQLite).
416    fn set_member(&mut self, member: &QueryPlan) {
417        let needs_wrap = !member.ordering.is_empty()
418            || member.limit.is_some()
419            || member.offset.is_some()
420            || member.lock.is_some()
421            || !member.compound.is_empty();
422        if !needs_wrap {
423            self.plan(member);
424            return;
425        }
426        self.derived += 1;
427        let alias = format!("u{}", self.derived);
428        self.push("SELECT * FROM (");
429        self.plan(member);
430        self.push(") AS ");
431        self.ident(&alias);
432    }
433
434    /// `SELECT .. FROM .. WHERE .. GROUP BY .. HAVING ..`.
435    fn select(&mut self, p: &QueryPlan) {
436        self.push("SELECT ");
437        match &p.distinct {
438            DistinctMode::None => {}
439            DistinctMode::All => self.push("DISTINCT "),
440            DistinctMode::On(exprs) => {
441                self.push("DISTINCT ON (");
442                self.list(exprs, ", ", Self::expr);
443                self.push(") ");
444            }
445        }
446        if !p.projection.is_empty() {
447            self.list(&p.projection, ", ", |c, s| {
448                c.expr(&s.expr);
449                if let Some(alias) = &s.alias {
450                    c.push(" AS ");
451                    c.ident(alias);
452                }
453            });
454        } else if p.joins.is_empty() {
455            self.push("*");
456        } else {
457            // `*` would also select the joined tables' (clashing) columns.
458            let root = p.source.reference().clone();
459            self.ident(&root);
460            self.push(".*");
461        }
462        self.push(" FROM ");
463        self.source(&p.source);
464        for j in &p.joins {
465            self.push(match j.kind {
466                JoinKind::Inner => " INNER JOIN ",
467                JoinKind::Left => " LEFT JOIN ",
468            });
469            self.source(&j.source);
470            self.push(" ON ");
471            self.expr(&j.on);
472        }
473        self.where_clause(p.filter.as_ref());
474        if !p.grouping.is_empty() {
475            self.push(" GROUP BY ");
476            self.list(&p.grouping, ", ", Self::expr);
477        }
478        if let Some(h) = &p.having {
479            self.push(" HAVING ");
480            self.expr(h);
481        }
482    }
483
484    /// `ORDER BY .. LIMIT .. OFFSET .. FOR UPDATE ..`.
485    fn tail(&mut self, p: &QueryPlan) {
486        if !p.ordering.is_empty() {
487            self.push(" ORDER BY ");
488            self.list(&p.ordering, ", ", |c, o| {
489                c.expr(&o.expr);
490                c.push(match o.direction {
491                    OrderDirection::Asc => " ASC",
492                    OrderDirection::Desc => " DESC",
493                });
494            });
495        }
496        match (p.limit, p.offset) {
497            (Some(l), _) => {
498                let _ = write!(self.sql, " LIMIT {l}");
499            }
500            (None, Some(_)) if self.dialect.offset_requires_limit() => {
501                let _ = write!(self.sql, " LIMIT {}", self.dialect.unbounded_limit());
502            }
503            _ => {}
504        }
505        if let Some(o) = p.offset {
506            let _ = write!(self.sql, " OFFSET {o}");
507        }
508        if let Some(lock) = p.lock {
509            self.push(match lock {
510                LockMode::ForUpdate => " FOR UPDATE",
511                LockMode::ForUpdateNoWait => " FOR UPDATE NOWAIT",
512                LockMode::ForUpdateSkipLocked => " FOR UPDATE SKIP LOCKED",
513            });
514        }
515    }
516
517    fn source(&mut self, s: &QuerySource) {
518        if let Some(inner) = &s.subquery {
519            self.push("(");
520            self.plan(inner);
521            self.push(")");
522        } else {
523            self.ident(&s.name);
524            if let Some(alias) = &s.alias {
525                self.push(" AS ");
526                self.ident(alias);
527            }
528            return;
529        }
530        self.push(" AS ");
531        self.ident(s.reference());
532    }
533}