velesdb_core/velesql/ast/select.rs
1//! SELECT statement types for VelesQL.
2//!
3//! This module defines the SELECT statement and related types.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8use super::aggregation::{AggregateFunction, GroupByClause, HavingClause};
9use super::condition::Condition;
10use super::fusion::FusionClause;
11use super::join::JoinClause;
12use super::values::VectorExpr;
13use super::with_clause::WithClause;
14
15/// DISTINCT mode for SELECT queries (EPIC-052 US-001).
16#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
17#[non_exhaustive]
18pub enum DistinctMode {
19 /// No deduplication.
20 #[default]
21 None,
22 /// DISTINCT - deduplicate by all selected columns.
23 All,
24}
25
26/// A SELECT statement.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct SelectStatement {
29 /// DISTINCT mode (EPIC-052 US-001).
30 #[serde(default)]
31 pub distinct: DistinctMode,
32 /// Columns to select.
33 pub columns: SelectColumns,
34 /// Collection name (FROM clause).
35 pub from: String,
36 /// Aliases visible in scope: FROM alias + JOIN aliases (BUG-8 fix).
37 #[serde(default)]
38 pub from_alias: Vec<String>,
39 /// JOIN clauses (EPIC-031 US-004).
40 #[serde(default)]
41 pub joins: Vec<JoinClause>,
42 /// WHERE conditions.
43 pub where_clause: Option<Condition>,
44 /// ORDER BY clause.
45 pub order_by: Option<Vec<SelectOrderBy>>,
46 /// LIMIT value.
47 pub limit: Option<u64>,
48 /// OFFSET value.
49 pub offset: Option<u64>,
50 /// WITH clause.
51 pub with_clause: Option<WithClause>,
52 /// GROUP BY clause.
53 #[serde(default)]
54 pub group_by: Option<GroupByClause>,
55 /// HAVING clause.
56 #[serde(default)]
57 pub having: Option<HavingClause>,
58 /// USING FUSION clause (EPIC-040 US-005).
59 #[serde(default)]
60 pub fusion_clause: Option<FusionClause>,
61}
62
63/// Columns in a SELECT statement.
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65#[non_exhaustive]
66pub enum SelectColumns {
67 /// Select all columns (*).
68 All,
69 /// Select specific columns.
70 Columns(Vec<Column>),
71 /// Select aggregate functions.
72 Aggregations(Vec<AggregateFunction>),
73 /// Mixed: columns + aggregations + similarity scores + qualified wildcards + window functions.
74 Mixed {
75 /// Regular columns.
76 columns: Vec<Column>,
77 /// Aggregate functions.
78 aggregations: Vec<AggregateFunction>,
79 /// similarity() score expressions.
80 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 similarity_scores: Vec<SimilarityScoreExpr>,
82 /// Qualified wildcards (e.g., `ctx.*`).
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
84 qualified_wildcards: Vec<String>,
85 /// Window function expressions (Issue #386).
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
87 window_functions: Vec<super::window::WindowFunction>,
88 },
89 /// Select similarity() score only (zero-arg form).
90 SimilarityScore(SimilarityScoreExpr),
91 /// Select alias.* (qualified wildcard).
92 QualifiedWildcard(String),
93}
94
95impl SelectColumns {
96 /// Returns human-readable column names for display, one per SELECT-list
97 /// item in grammar order.
98 ///
99 /// Used by Python/WASM bindings to expose the column-metadata contract.
100 ///
101 /// # Completeness
102 ///
103 /// Every SELECT-list variant must contribute exactly one entry per item
104 /// it contains. Historically the `Mixed` arm dropped `similarity_scores`
105 /// and `qualified_wildcards` via a `..` pattern, which silently shortened
106 /// the column list for queries that combined them with regular columns —
107 /// a correctness bug that was observable through Python/WASM callers
108 /// reading the column count or iterating the list. That bug is now
109 /// fixed; the returned list reflects the *complete* SELECT projection.
110 ///
111 /// **Compatibility note**: callers that previously relied on the
112 /// incomplete list (e.g. hard-coded `len() == columns.len()`) will now
113 /// see additional entries. The new contract is pinned by
114 /// `ast_tests::test_display_names_mixed_includes_all_variants`.
115 #[must_use]
116 pub fn to_display_names(&self) -> Vec<String> {
117 match self {
118 Self::All => vec!["*".to_string()],
119 Self::Columns(cols) => cols.iter().map(|c| c.name.clone()).collect(),
120 Self::Aggregations(aggs) => aggs
121 .iter()
122 .map(|a| format!("{:?}", a.function_type))
123 .collect(),
124 Self::Mixed {
125 columns,
126 aggregations,
127 similarity_scores,
128 qualified_wildcards,
129 window_functions,
130 } => {
131 // Order mirrors the SELECT-list grammar: columns, aggregates,
132 // similarity(), qualified wildcards (`alias.*`), window
133 // functions. Python/WASM bindings consume this list to expose
134 // the column metadata contract, so every SELECT-list variant
135 // must contribute a display name.
136 let mut result: Vec<String> = columns.iter().map(|c| c.name.clone()).collect();
137 result.extend(
138 aggregations
139 .iter()
140 .map(|a| format!("{:?}", a.function_type)),
141 );
142 result.extend(similarity_scores.iter().map(|expr| {
143 expr.alias
144 .clone()
145 .unwrap_or_else(|| "similarity".to_string())
146 }));
147 result.extend(qualified_wildcards.iter().map(|alias| format!("{alias}.*")));
148 result.extend(window_functions.iter().map(|wf| {
149 wf.alias
150 .clone()
151 .unwrap_or_else(|| wf.function_type.default_alias().to_string())
152 }));
153 result
154 }
155 Self::SimilarityScore(expr) => {
156 vec![expr
157 .alias
158 .clone()
159 .unwrap_or_else(|| "similarity".to_string())]
160 }
161 Self::QualifiedWildcard(alias) => vec![format!("{alias}.*")],
162 }
163 }
164}
165
166/// A `similarity()` zero-arg expression in SELECT, with optional alias.
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168pub struct SimilarityScoreExpr {
169 /// Optional alias (e.g., `similarity() AS relevance`).
170 pub alias: Option<String>,
171}
172
173/// A column reference.
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct Column {
176 /// Column name.
177 pub name: String,
178 /// Optional alias.
179 pub alias: Option<String>,
180}
181
182impl Column {
183 /// Creates a new column reference.
184 #[must_use]
185 pub fn new(name: impl Into<String>) -> Self {
186 Self {
187 name: name.into(),
188 alias: None,
189 }
190 }
191
192 /// Creates a column with an alias.
193 #[must_use]
194 pub fn with_alias(name: impl Into<String>, alias: impl Into<String>) -> Self {
195 Self {
196 name: name.into(),
197 alias: Some(alias.into()),
198 }
199 }
200}
201
202/// ORDER BY item for sorting SELECT results.
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub struct SelectOrderBy {
205 /// Expression to order by.
206 pub expr: OrderByExpr,
207 /// Sort direction (true = DESC).
208 pub descending: bool,
209}
210
211impl SelectOrderBy {
212 /// Returns a `(column_name, direction)` pair for display.
213 #[must_use]
214 pub fn to_display_pair(&self) -> (String, String) {
215 let dir = if self.descending { "DESC" } else { "ASC" };
216 let col = match &self.expr {
217 OrderByExpr::Field(f) => f.clone(),
218 OrderByExpr::Similarity(_) | OrderByExpr::SimilarityBare => "similarity()".to_string(),
219 OrderByExpr::Aggregate(agg) => format!("{:?}", agg.function_type),
220 OrderByExpr::Arithmetic(expr) => format!("{expr}"),
221 };
222 (col, dir.to_string())
223 }
224}
225
226/// Expression types supported in ORDER BY clause.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228#[non_exhaustive]
229pub enum OrderByExpr {
230 /// Simple field reference.
231 Field(String),
232 /// Similarity function with field and vector args.
233 Similarity(SimilarityOrderBy),
234 /// Similarity zero-arg: uses pre-computed search score.
235 SimilarityBare,
236 /// Aggregate function.
237 Aggregate(AggregateFunction),
238 /// Arithmetic expression combining scores (EPIC-042).
239 ///
240 /// Example: `0.7 * vector_score + 0.3 * graph_score`
241 Arithmetic(ArithmeticExpr),
242}
243
244/// A named score binding defined by a `LET` clause (VelesQL v1.10 Phase 3).
245///
246/// Each binding assigns an arithmetic expression to a name. Bindings are
247/// evaluated in declaration order; later bindings may reference earlier ones.
248///
249/// # Example
250///
251/// ```sql
252/// LET hybrid = 0.7 * vector_score + 0.3 * bm25_score
253/// ```
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
255pub struct LetBinding {
256 /// Binding name (identifier).
257 pub name: String,
258 /// Expression to evaluate.
259 pub expr: ArithmeticExpr,
260}
261
262/// Arithmetic expression for ORDER BY custom scoring (EPIC-042).
263///
264/// Supports binary operations (+, -, *, /) with numeric literals,
265/// variables (field references), and similarity() function calls.
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267#[non_exhaustive]
268pub enum ArithmeticExpr {
269 /// Numeric literal (e.g., `0.7`, `2`).
270 Literal(f64),
271 /// Score variable or field reference (e.g., `vector_score`, `price`).
272 Variable(String),
273 /// Similarity function call (zero-arg or with field+vector).
274 Similarity(Box<OrderByExpr>),
275 /// Binary operation with operator precedence.
276 BinaryOp {
277 /// Left operand.
278 left: Box<ArithmeticExpr>,
279 /// Arithmetic operator.
280 op: ArithmeticOp,
281 /// Right operand.
282 right: Box<ArithmeticExpr>,
283 },
284}
285
286/// Arithmetic operators for ORDER BY expressions (EPIC-042).
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[non_exhaustive]
289pub enum ArithmeticOp {
290 /// Addition (`+`).
291 Add,
292 /// Subtraction (`-`).
293 Sub,
294 /// Multiplication (`*`).
295 Mul,
296 /// Division (`/`).
297 Div,
298}
299
300impl fmt::Display for ArithmeticOp {
301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 match self {
303 Self::Add => write!(f, "+"),
304 Self::Sub => write!(f, "-"),
305 Self::Mul => write!(f, "*"),
306 Self::Div => write!(f, "/"),
307 }
308 }
309}
310
311impl fmt::Display for ArithmeticExpr {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 match self {
314 Self::Literal(v) => write!(f, "{v}"),
315 Self::Variable(name) => write!(f, "{name}"),
316 Self::Similarity(inner) => match inner.as_ref() {
317 OrderByExpr::Similarity(sim) => {
318 let vec_str = match &sim.vector {
319 VectorExpr::Parameter(name) => format!("${name}"),
320 VectorExpr::Literal(vals) => format!("{vals:?}"),
321 };
322 write!(f, "similarity({}, {vec_str})", sim.field)
323 }
324 _ => write!(f, "similarity()"),
325 },
326 Self::BinaryOp { left, op, right } => write!(f, "({left} {op} {right})"),
327 }
328 }
329}
330
331/// Similarity expression for ORDER BY.
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
333pub struct SimilarityOrderBy {
334 /// Field containing the embedding vector.
335 pub field: String,
336 /// Vector to compare against.
337 pub vector: VectorExpr,
338}
339
340impl SelectStatement {
341 /// Returns an empty `SelectStatement` with all fields at their defaults.
342 ///
343 /// Used by [`crate::velesql::Query::new_dml`],
344 /// [`crate::velesql::Query::new_train`], and
345 /// [`crate::velesql::Query::new_match`] to avoid repeating the 14-field
346 /// struct literal.
347 #[must_use]
348 pub fn empty() -> Self {
349 Self {
350 distinct: DistinctMode::None,
351 columns: SelectColumns::All,
352 from: String::new(),
353 from_alias: Vec::new(),
354 joins: Vec::new(),
355 where_clause: None,
356 order_by: None,
357 limit: None,
358 offset: None,
359 with_clause: None,
360 group_by: None,
361 having: None,
362 fusion_clause: None,
363 }
364 }
365}