Skip to main content

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}