Skip to main content

rust_ef/query/
state.rs

1//! `QueryState` — accumulator for a single query's intent.
2//!
3//! Holds the WHERE expression tree, JOINs, GROUP BY, HAVING, ORDER BY,
4//! pagination, includes, projections, window functions, CTEs, and set
5//! operations.
6//! `to_sql_with` compiles the state into a SQL string using the provider's
7//! placeholder syntax; `all_params` returns the bound parameter vector in
8//! the order expected by the generated SQL (CTE params first, then WHERE/
9//! HAVING, then set operation operand params).
10
11use crate::provider::DbValue;
12
13use super::ast::{BoolExpr, IncludePath, JoinSpec, OrderBy};
14use super::compile::{compile_bool_expr, PortablePlaceholderGenerator};
15use super::cte::{CteSpec, SetOpSpec, SetOperator};
16use super::having_expr::HavingExpr;
17use super::window::WindowSpec;
18
19/// Accumulated intent for a single query.
20#[derive(Debug, Clone)]
21pub struct QueryState {
22    /// FROM table / subquery segment.
23    pub from: String,
24    /// JOIN specifications.
25    pub joins: Vec<JoinSpec>,
26    /// GROUP BY columns.
27    pub group_bys: Vec<String>,
28    /// HAVING conditions stored as AST nodes so they can be compiled with the
29    /// provider-specific placeholder syntax (`?` vs `$N`) at SQL generation
30    /// time, rather than being pre-compiled to a fixed placeholder.
31    pub havings: Vec<HavingExpr>,
32    /// ORDER BY clauses.
33    pub orderings: Vec<OrderBy>,
34    /// OFFSET (Skip).
35    pub offset: Option<usize>,
36    /// LIMIT (Take).
37    pub limit: Option<usize>,
38    /// Include navigation paths.
39    pub includes: Vec<IncludePath>,
40    /// Whether this is a projection (SELECT col1, col2 instead of SELECT *).
41    pub projected_columns: Option<Vec<String>>,
42    /// Whether this is a COUNT query.
43    pub is_count: bool,
44    /// Whether this is an EXISTS sub-query.
45    pub is_exists: bool,
46    /// Aggregate function to apply: "SUM", "AVG", "MIN", "MAX", "COUNT"
47    pub aggregate: Option<String>,
48    /// The column to aggregate.
49    pub aggregate_column: Option<String>,
50    /// Collected parameter values in order of appearance.
51    pub parameters: Vec<DbValue>,
52    /// Boolean WHERE expression (the only filter representation).
53    pub where_expr: Option<BoolExpr>,
54    /// Whether to emit `SELECT DISTINCT`.
55    pub distinct: bool,
56    /// Window function projections (v1.1). Emitted in the SELECT list as
57    /// `func(col) OVER (PARTITION BY ... ORDER BY ...) AS alias`.
58    pub windows: Vec<WindowSpec>,
59    /// CTE definitions (v1.1). Emitted as `WITH name AS (...)` prefix
60    /// before the SELECT. CTE parameters are prepended to the query's
61    /// parameter list at execution time.
62    pub ctes: Vec<CteSpec>,
63    /// Set operations (UNION / INTERSECT / EXCEPT) appended after the main
64    /// SELECT. Multiple operations chain left-to-right.
65    pub set_operations: Vec<SetOpSpec>,
66}
67
68impl QueryState {
69    pub fn new(from: impl Into<String>) -> Self {
70        Self {
71            from: from.into(),
72            joins: Vec::new(),
73            group_bys: Vec::new(),
74            havings: Vec::new(),
75            orderings: Vec::new(),
76            offset: None,
77            limit: None,
78            includes: Vec::new(),
79            projected_columns: None,
80            is_count: false,
81            is_exists: false,
82            aggregate: None,
83            aggregate_column: None,
84            parameters: Vec::new(),
85            where_expr: None,
86            distinct: false,
87            windows: Vec::new(),
88            ctes: Vec::new(),
89            set_operations: Vec::new(),
90        }
91    }
92
93    pub(crate) fn append_bool_expr(&mut self, expr: BoolExpr) {
94        self.where_expr = Some(match self.where_expr.take() {
95            None => expr,
96            Some(existing) => BoolExpr::And(Box::new(existing), Box::new(expr)),
97        });
98    }
99
100    /// Compile the state into a SQL string using the provider's placeholder style.
101    pub fn to_sql_with(&self, gen: &dyn crate::provider::ISqlGenerator) -> String {
102        // CTE parameter count — the main query's PostgreSQL `$N` placeholders
103        // must continue from this offset to stay contiguous with CTE params.
104        let cte_param_count: usize = self.ctes.iter().map(|c| c.params.len()).sum();
105
106        let distinct_kw = if self.distinct { "DISTINCT " } else { "" };
107        let select = if self.is_count {
108            if self.distinct {
109                "SELECT COUNT(DISTINCT *)".to_string()
110            } else {
111                "SELECT COUNT(*)".to_string()
112            }
113        } else if self.is_exists {
114            "SELECT 1".to_string()
115        } else if let Some(ref agg) = self.aggregate {
116            let col = self.aggregate_column.as_deref().unwrap_or("*");
117            if self.distinct {
118                format!("SELECT {}(DISTINCT {})", agg, col)
119            } else {
120                format!("SELECT {}({})", agg, col)
121            }
122        } else if let Some(ref cols) = self.projected_columns {
123            let mut parts: Vec<String> = cols.to_vec();
124            // Window function projections are appended to explicit column lists.
125            for w in &self.windows {
126                parts.push(w.to_sql(gen));
127            }
128            format!("SELECT {}{}", distinct_kw, parts.join(", "))
129        } else {
130            // Default SELECT * — append window projections if present.
131            if self.windows.is_empty() {
132                format!("SELECT {}*", distinct_kw)
133            } else {
134                let mut parts: Vec<String> = vec![format!("{}*", distinct_kw)];
135                for w in &self.windows {
136                    parts.push(w.to_sql(gen));
137                }
138                format!("SELECT {}", parts.join(", "))
139            }
140        };
141
142        // Pre-allocate a single buffer instead of chaining format!() calls
143        // that each allocate a temporary String and copy it via push_str.
144        let mut sql = String::with_capacity(256);
145        sql.push_str(&select);
146        sql.push_str(" FROM ");
147        sql.push_str(&self.from);
148
149        // JOINs
150        for join in &self.joins {
151            sql.push(' ');
152            sql.push_str(&join.to_sql());
153        }
154
155        // Parameter index is shared across WHERE and HAVING so that
156        // PostgreSQL's 1-indexed `$N` placeholders remain contiguous and
157        // correctly ordered across both clauses. CTE parameters (if any)
158        // occupy the leading slots, so the main query starts after them.
159        let mut param_idx = 1usize + cte_param_count;
160
161        // WHERE
162        if let Some(ref expr) = self.where_expr {
163            sql.push_str(" WHERE ");
164            sql.push_str(&compile_bool_expr(expr, gen, &mut param_idx));
165        }
166
167        // GROUP BY
168        if !self.group_bys.is_empty() {
169            sql.push_str(" GROUP BY ");
170            sql.push_str(&self.group_bys.join(", "));
171        }
172
173        // HAVING — compile each `HavingExpr` AST node with the provider's
174        // placeholder syntax, continuing the shared `param_idx` so PostgreSQL
175        // `$N` indices stay contiguous with the WHERE clause.
176        if !self.havings.is_empty() {
177            let compiled: Vec<String> = self
178                .havings
179                .iter()
180                .map(|h| h.to_sql(gen, &mut param_idx))
181                .collect();
182            sql.push_str(" HAVING ");
183            sql.push_str(&compiled.join(" AND "));
184        }
185
186        // ORDER BY
187        if !self.orderings.is_empty() {
188            let ords: Vec<String> = self.orderings.iter().map(|o| o.to_sql()).collect();
189            sql.push_str(" ORDER BY ");
190            sql.push_str(&ords.join(", "));
191        }
192
193        // LIMIT / OFFSET — delegated to the dialect-specific generator so
194        // that PostgreSQL emits `OFFSET x LIMIT y`, MySQL handles the
195        // offset-only case via `LIMIT 18446744073709551615 OFFSET y`, and
196        // SQLite/MySQL use `LIMIT x OFFSET y`.
197        let pagination = gen.pagination(self.offset, self.limit);
198        if !pagination.is_empty() {
199            sql.push(' ');
200            sql.push_str(&pagination);
201        }
202
203        // CTE prefix — emitted as `WITH name AS (body), ...` before the SELECT.
204        //
205        // Two modes:
206        // - **Raw mode** (`sql` non-empty): body is the pre-compiled SQL,
207        //   emitted verbatim with `?` placeholders.
208        // - **Typed mode** (`table` non-empty): body is compiled from
209        //   `where_expr` at this point using the provider's placeholder
210        //   syntax. Parameter values were already extracted into `params` at
211        //   construction time (see `with_cte_typed`), so `param_idx` for the
212        //   main query starts at `1 + cte_param_count` (computed above).
213        //
214        // `running_idx` accumulates across all CTEs so that PostgreSQL's
215        // 1-indexed `$N` placeholders stay contiguous across multiple typed
216        // CTEs and align with each CTE's slot in `all_params()`. Raw-mode
217        // CTEs advance `running_idx` by their param count for consistency,
218        // even though their `?` placeholders don't use the index.
219        if !self.ctes.is_empty() {
220            let mut running_idx = 1usize;
221            let mut cte_parts: Vec<String> = Vec::with_capacity(self.ctes.len());
222            let has_recursive = self.ctes.iter().any(|c| c.is_recursive);
223            for c in &self.ctes {
224                let body = if !c.table.is_empty() {
225                    // Typed mode: compile WHERE expression starting at the
226                    // running index. `cte_idx` advances as placeholders are
227                    // emitted; we then propagate it back to `running_idx`.
228                    let mut cte_idx = running_idx;
229                    let table = gen.quote_identifier(&c.table);
230                    let anchor = match &c.where_expr {
231                        Some(expr) => {
232                            let where_sql = compile_bool_expr(expr, gen, &mut cte_idx);
233                            format!("SELECT * FROM {} WHERE {}", table, where_sql)
234                        }
235                        None => format!("SELECT * FROM {}", table),
236                    };
237                    running_idx = cte_idx;
238                    if c.is_recursive {
239                        // Append recursive member:
240                        // SELECT t.* FROM <table> t JOIN <name> ON t.<fk> = <name>.<pk>
241                        let (fk, pk) = c
242                            .recursive_link
243                            .clone()
244                            .expect("recursive CTE must have recursive_link");
245                        let name = gen.quote_identifier(&c.name);
246                        let fk = gen.quote_identifier(&fk);
247                        let pk = gen.quote_identifier(&pk);
248                        format!(
249                            "{} UNION ALL SELECT t.* FROM {} t JOIN {} ON t.{} = {}.{}",
250                            anchor, table, name, fk, name, pk
251                        )
252                    } else {
253                        anchor
254                    }
255                } else {
256                    // Raw mode: pre-compiled SQL with `?` placeholders. The
257                    // index isn't consumed but advance for consistency with
258                    // `all_params()` ordering.
259                    running_idx = running_idx.saturating_add(c.params.len());
260                    c.sql.clone()
261                };
262
263                let part = if c.columns.is_empty() {
264                    format!("{} AS ({})", c.name, body)
265                } else {
266                    let cols = c
267                        .columns
268                        .iter()
269                        .map(|col| gen.quote_identifier(col))
270                        .collect::<Vec<_>>()
271                        .join(", ");
272                    format!("{} ({}) AS ({})", c.name, cols, body)
273                };
274                cte_parts.push(part);
275            }
276            let with_kw = if has_recursive {
277                "WITH RECURSIVE"
278            } else {
279                "WITH"
280            };
281            // Prepend the CTE prefix to `sql` without allocating a new String
282            // via format!("{} {} {}", ...).
283            let joined = cte_parts.join(", ");
284            let mut prefix = String::with_capacity(with_kw.len() + 1 + joined.len() + 1);
285            prefix.push_str(with_kw);
286            prefix.push(' ');
287            prefix.push_str(&joined);
288            prefix.push(' ');
289            sql.insert_str(0, &prefix);
290        }
291
292        // Set operations (UNION / INTERSECT / EXCEPT) — appended after the
293        // full SELECT (including CTE prefix). Operands are pre-compiled SQL
294        // strings; their params are appended in all_params() after the main
295        // query params. Per D5, operands should not contain ORDER BY / LIMIT.
296        for op in &self.set_operations {
297            let kw = match op.operator {
298                SetOperator::Union => " UNION ",
299                SetOperator::UnionAll => " UNION ALL ",
300                SetOperator::Intersect => " INTERSECT ",
301                SetOperator::Except => " EXCEPT ",
302            };
303            sql.push_str(kw);
304            sql.push_str(&op.operand_sql);
305        }
306
307        sql
308    }
309
310    /// Returns all parameter values for execution: CTE parameters first
311    /// (in declaration order), followed by WHERE/HAVING parameters, then
312    /// set operation operand params.
313    ///
314    /// This ordering matches the placeholder order in the generated SQL,
315    /// where CTE bodies appear before the main SELECT and set operations
316    /// appear after.
317    pub fn all_params(&self) -> Vec<DbValue> {
318        let mut params = Vec::new();
319        for cte in &self.ctes {
320            params.extend(cte.params.clone());
321        }
322        params.extend(self.parameters.clone());
323        for op in &self.set_operations {
324            params.extend(op.operand_params.clone());
325        }
326        params
327    }
328
329    /// Compile SQL with `?` placeholders (SQLite/MySQL style).
330    pub fn to_sql(&self) -> String {
331        self.to_sql_with(&PortablePlaceholderGenerator)
332    }
333
334    /// Returns the accumulated parameters.
335    pub fn params(&self) -> &[DbValue] {
336        &self.parameters
337    }
338}