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