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 let mut sql = format!("{} FROM {}", select, self.from);
150
151 // JOINs
152 for join in &self.joins {
153 sql.push_str(&format!(" {}", join.to_sql()));
154 }
155
156 // Parameter index is shared across WHERE and HAVING so that
157 // PostgreSQL's 1-indexed `$N` placeholders remain contiguous and
158 // correctly ordered across both clauses. CTE parameters (if any)
159 // occupy the leading slots, so the main query starts after them.
160 let mut param_idx = 1usize + cte_param_count;
161
162 // WHERE
163 if let Some(ref expr) = self.where_expr {
164 sql.push_str(&format!(
165 " WHERE {}",
166 compile_bool_expr(expr, gen, &mut param_idx)
167 ));
168 } else if !self.filters.is_empty() {
169 sql.push_str(&format!(
170 " WHERE {}",
171 build_where_clauses(&self.filters, gen)
172 ));
173 // Advance `param_idx` past the legacy `filters` path so that
174 // HAVING placeholders (PostgreSQL `$N`) continue from the
175 // correct index. `build_where_clauses` always starts at index 1.
176 param_idx += self.filters.iter().map(|f| f.param_count()).sum::<usize>();
177 }
178
179 // GROUP BY
180 if !self.group_bys.is_empty() {
181 sql.push_str(&format!(" GROUP BY {}", self.group_bys.join(", ")));
182 }
183
184 // HAVING — compile each `HavingExpr` AST node with the provider's
185 // placeholder syntax, continuing the shared `param_idx` so PostgreSQL
186 // `$N` indices stay contiguous with the WHERE clause.
187 if !self.havings.is_empty() {
188 let compiled: Vec<String> = self
189 .havings
190 .iter()
191 .map(|h| h.to_sql(gen, &mut param_idx))
192 .collect();
193 sql.push_str(&format!(" HAVING {}", compiled.join(" AND ")));
194 }
195
196 // ORDER BY
197 if !self.orderings.is_empty() {
198 let ords: Vec<String> = self.orderings.iter().map(|o| o.to_sql()).collect();
199 sql.push_str(&format!(" ORDER BY {}", ords.join(", ")));
200 }
201
202 // LIMIT / OFFSET — delegated to the dialect-specific generator so
203 // that PostgreSQL emits `OFFSET x LIMIT y`, MySQL handles the
204 // offset-only case via `LIMIT 18446744073709551615 OFFSET y`, and
205 // SQLite/MySQL use `LIMIT x OFFSET y`.
206 let pagination = gen.pagination(self.offset, self.limit);
207 if !pagination.is_empty() {
208 sql.push(' ');
209 sql.push_str(&pagination);
210 }
211
212 // CTE prefix — emitted as `WITH name AS (body), ...` before the SELECT.
213 //
214 // Two modes:
215 // - **Raw mode** (`sql` non-empty): body is the pre-compiled SQL,
216 // emitted verbatim with `?` placeholders.
217 // - **Typed mode** (`table` non-empty): body is compiled from
218 // `where_expr` at this point using the provider's placeholder
219 // syntax. Parameter values were already extracted into `params` at
220 // construction time (see `with_cte_typed`), so `param_idx` for the
221 // main query starts at `1 + cte_param_count` (computed above).
222 //
223 // `running_idx` accumulates across all CTEs so that PostgreSQL's
224 // 1-indexed `$N` placeholders stay contiguous across multiple typed
225 // CTEs and align with each CTE's slot in `all_params()`. Raw-mode
226 // CTEs advance `running_idx` by their param count for consistency,
227 // even though their `?` placeholders don't use the index.
228 if !self.ctes.is_empty() {
229 let mut running_idx = 1usize;
230 let mut cte_parts: Vec<String> = Vec::with_capacity(self.ctes.len());
231 let has_recursive = self.ctes.iter().any(|c| c.is_recursive);
232 for c in &self.ctes {
233 let body = if !c.table.is_empty() {
234 // Typed mode: compile WHERE expression starting at the
235 // running index. `cte_idx` advances as placeholders are
236 // emitted; we then propagate it back to `running_idx`.
237 let mut cte_idx = running_idx;
238 let table = gen.quote_identifier(&c.table);
239 let anchor = match &c.where_expr {
240 Some(expr) => {
241 let where_sql = compile_bool_expr(expr, gen, &mut cte_idx);
242 format!("SELECT * FROM {} WHERE {}", table, where_sql)
243 }
244 None => format!("SELECT * FROM {}", table),
245 };
246 running_idx = cte_idx;
247 if c.is_recursive {
248 // Append recursive member:
249 // SELECT t.* FROM <table> t JOIN <name> ON t.<fk> = <name>.<pk>
250 let (fk, pk) = c
251 .recursive_link
252 .clone()
253 .expect("recursive CTE must have recursive_link");
254 let name = gen.quote_identifier(&c.name);
255 let fk = gen.quote_identifier(&fk);
256 let pk = gen.quote_identifier(&pk);
257 format!(
258 "{} UNION ALL SELECT t.* FROM {} t JOIN {} ON t.{} = {}.{}",
259 anchor, table, name, fk, name, pk
260 )
261 } else {
262 anchor
263 }
264 } else {
265 // Raw mode: pre-compiled SQL with `?` placeholders. The
266 // index isn't consumed but advance for consistency with
267 // `all_params()` ordering.
268 running_idx = running_idx.saturating_add(c.params.len());
269 c.sql.clone()
270 };
271
272 let part = if c.columns.is_empty() {
273 format!("{} AS ({})", c.name, body)
274 } else {
275 let cols = c
276 .columns
277 .iter()
278 .map(|col| gen.quote_identifier(col))
279 .collect::<Vec<_>>()
280 .join(", ");
281 format!("{} ({}) AS ({})", c.name, cols, body)
282 };
283 cte_parts.push(part);
284 }
285 let with_kw = if has_recursive {
286 "WITH RECURSIVE"
287 } else {
288 "WITH"
289 };
290 sql = format!("{} {} {}", with_kw, cte_parts.join(", "), sql);
291 }
292
293 // Set operations (UNION / INTERSECT / EXCEPT) — appended after the
294 // full SELECT (including CTE prefix). Operands are pre-compiled SQL
295 // strings; their params are appended in all_params() after the main
296 // query params. Per D5, operands should not contain ORDER BY / LIMIT.
297 for op in &self.set_operations {
298 let kw = match op.operator {
299 SetOperator::Union => " UNION ",
300 SetOperator::UnionAll => " UNION ALL ",
301 SetOperator::Intersect => " INTERSECT ",
302 SetOperator::Except => " EXCEPT ",
303 };
304 sql.push_str(kw);
305 sql.push_str(&op.operand_sql);
306 }
307
308 sql
309 }
310
311 /// Returns all parameter values for execution: CTE parameters first
312 /// (in declaration order), followed by WHERE/HAVING parameters, then
313 /// set operation operand params.
314 ///
315 /// This ordering matches the placeholder order in the generated SQL,
316 /// where CTE bodies appear before the main SELECT and set operations
317 /// appear after.
318 pub fn all_params(&self) -> Vec<DbValue> {
319 let mut params = Vec::new();
320 for cte in &self.ctes {
321 params.extend(cte.params.clone());
322 }
323 params.extend(self.parameters.clone());
324 for op in &self.set_operations {
325 params.extend(op.operand_params.clone());
326 }
327 params
328 }
329
330 /// Compile SQL with `?` placeholders (SQLite/MySQL style).
331 pub fn to_sql(&self) -> String {
332 self.to_sql_with(&PortablePlaceholderGenerator)
333 }
334
335 /// Returns the accumulated parameters.
336 pub fn params(&self) -> &[DbValue] {
337 &self.parameters
338 }
339}