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 the table names and aliases visible as *outer* scope for a nested
357 /// subquery: the `FROM` collection plus any `FROM`/JOIN aliases.
358 ///
359 /// A subquery's inner WHERE is only **correlated** when it references one of
360 /// these names; a dotted payload path whose prefix is not in this set is a
361 /// plain payload filter, not a correlation (EPIC-039).
362 #[must_use]
363 pub fn outer_table_scope(&self) -> Vec<&str> {
364 std::iter::once(self.from.as_str())
365 .chain(self.from_alias.iter().map(String::as_str))
366 .collect()
367 }
368
369 /// Returns an empty `SelectStatement` with all fields at their defaults.
370 ///
371 /// Used by [`crate::velesql::Query::new_dml`],
372 /// [`crate::velesql::Query::new_train`], and
373 /// [`crate::velesql::Query::new_match`] to avoid repeating the 14-field
374 /// struct literal.
375 #[must_use]
376 pub fn empty() -> Self {
377 Self {
378 distinct: DistinctMode::None,
379 columns: SelectColumns::All,
380 from: String::new(),
381 from_alias: Vec::new(),
382 joins: Vec::new(),
383 where_clause: None,
384 order_by: None,
385 limit: None,
386 offset: None,
387 with_clause: None,
388 group_by: None,
389 having: None,
390 fusion_clause: None,
391 }
392 }
393
394 /// Returns `true` when this SELECT must run through the aggregation engine
395 /// (scalar aggregates or `GROUP BY`) rather than the row-projection path.
396 ///
397 /// `NEAR ... GROUP BY` (vector-search grouping) is post-processed inside the
398 /// standard execute path, not the aggregate engine, so it returns `false`.
399 /// Single source of truth shared by the server `/query` handler and the CLI
400 /// REPL so every surface routes aggregation identically.
401 #[must_use]
402 pub fn is_aggregation_query(&self) -> bool {
403 let has_aggs = match &self.columns {
404 SelectColumns::Aggregations(_) => true,
405 SelectColumns::Mixed { aggregations, .. } => !aggregations.is_empty(),
406 _ => false,
407 };
408 let is_agg_query = has_aggs || self.group_by.is_some();
409 if is_agg_query && self.group_by.is_some() {
410 let has_vector_near = self
411 .where_clause
412 .as_ref()
413 .is_some_and(Condition::has_vector_search);
414 if has_vector_near {
415 return false;
416 }
417 }
418 is_agg_query
419 }
420}