Skip to main content

rust_ef/query/
state.rs

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