velesdb-core 3.5.0

High-performance vector database engine written in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
//! SELECT statement types for VelesQL.
//!
//! This module defines the SELECT statement and related types.

use serde::{Deserialize, Serialize};
use std::fmt;

use super::aggregation::{AggregateFunction, GroupByClause, HavingClause};
use super::condition::Condition;
use super::fusion::FusionClause;
use super::join::JoinClause;
use super::values::VectorExpr;
use super::with_clause::WithClause;

/// Default `LIMIT` applied to every SELECT statement without an explicit
/// `LIMIT` clause.
///
/// VelesQL is ANN-first: a SELECT is a top-k retrieval, so every execution
/// path (vector NEAR, sparse, scalar filter, hybrid) truncates to this value
/// when no `LIMIT` is given. This differs from standard SQL, where a SELECT
/// without LIMIT returns all rows.
///
/// Exceptions (no implicit limit is applied):
/// - `MATCH ... RETURN` graph queries return all matching rows;
/// - compound queries (`UNION` / `INTERSECT` / `EXCEPT`) evaluate their
///   operands exhaustively before the set operation, and only an explicit
///   outer `LIMIT` caps the merged result.
pub const DEFAULT_SELECT_LIMIT: u64 = 10;

/// DISTINCT mode for SELECT queries (EPIC-052 US-001).
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DistinctMode {
    /// No deduplication.
    #[default]
    None,
    /// DISTINCT - deduplicate by all selected columns.
    All,
}

/// A SELECT statement.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SelectStatement {
    /// DISTINCT mode (EPIC-052 US-001).
    #[serde(default)]
    pub distinct: DistinctMode,
    /// Columns to select.
    pub columns: SelectColumns,
    /// Collection name (FROM clause).
    pub from: String,
    /// Aliases visible in scope: FROM alias + JOIN aliases (BUG-8 fix).
    #[serde(default)]
    pub from_alias: Vec<String>,
    /// JOIN clauses (EPIC-031 US-004).
    #[serde(default)]
    pub joins: Vec<JoinClause>,
    /// WHERE conditions.
    pub where_clause: Option<Condition>,
    /// ORDER BY clause.
    pub order_by: Option<Vec<SelectOrderBy>>,
    /// LIMIT value.
    pub limit: Option<u64>,
    /// OFFSET value.
    pub offset: Option<u64>,
    /// WITH clause.
    pub with_clause: Option<WithClause>,
    /// GROUP BY clause.
    #[serde(default)]
    pub group_by: Option<GroupByClause>,
    /// HAVING clause.
    #[serde(default)]
    pub having: Option<HavingClause>,
    /// USING FUSION clause (EPIC-040 US-005).
    #[serde(default)]
    pub fusion_clause: Option<FusionClause>,
}

/// Columns in a SELECT statement.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SelectColumns {
    /// Select all columns (*).
    All,
    /// Select specific columns.
    Columns(Vec<Column>),
    /// Select aggregate functions.
    Aggregations(Vec<AggregateFunction>),
    /// Mixed: columns + aggregations + similarity scores + qualified wildcards + window functions.
    Mixed {
        /// Regular columns.
        columns: Vec<Column>,
        /// Aggregate functions.
        aggregations: Vec<AggregateFunction>,
        /// similarity() score expressions.
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        similarity_scores: Vec<SimilarityScoreExpr>,
        /// Qualified wildcards (e.g., `ctx.*`).
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        qualified_wildcards: Vec<String>,
        /// Window function expressions (Issue #386).
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        window_functions: Vec<super::window::WindowFunction>,
    },
    /// Select similarity() score only (zero-arg form).
    SimilarityScore(SimilarityScoreExpr),
    /// Select alias.* (qualified wildcard).
    QualifiedWildcard(String),
}

impl SelectColumns {
    /// Returns human-readable column names for display, one per SELECT-list
    /// item in grammar order.
    ///
    /// Used by Python/WASM bindings to expose the column-metadata contract.
    ///
    /// # Completeness
    ///
    /// Every SELECT-list variant must contribute exactly one entry per item
    /// it contains. Historically the `Mixed` arm dropped `similarity_scores`
    /// and `qualified_wildcards` via a `..` pattern, which silently shortened
    /// the column list for queries that combined them with regular columns —
    /// a correctness bug that was observable through Python/WASM callers
    /// reading the column count or iterating the list. That bug is now
    /// fixed; the returned list reflects the *complete* SELECT projection.
    ///
    /// **Compatibility note**: callers that previously relied on the
    /// incomplete list (e.g. hard-coded `len() == columns.len()`) will now
    /// see additional entries. The new contract is pinned by
    /// `ast_tests::test_display_names_mixed_includes_all_variants`.
    #[must_use]
    pub fn to_display_names(&self) -> Vec<String> {
        match self {
            Self::All => vec!["*".to_string()],
            Self::Columns(cols) => cols.iter().map(|c| c.name.clone()).collect(),
            Self::Aggregations(aggs) => aggs
                .iter()
                .map(|a| format!("{:?}", a.function_type))
                .collect(),
            Self::Mixed {
                columns,
                aggregations,
                similarity_scores,
                qualified_wildcards,
                window_functions,
            } => {
                // Order mirrors the SELECT-list grammar: columns, aggregates,
                // similarity(), qualified wildcards (`alias.*`), window
                // functions. Python/WASM bindings consume this list to expose
                // the column metadata contract, so every SELECT-list variant
                // must contribute a display name.
                let mut result: Vec<String> = columns.iter().map(|c| c.name.clone()).collect();
                result.extend(
                    aggregations
                        .iter()
                        .map(|a| format!("{:?}", a.function_type)),
                );
                result.extend(similarity_scores.iter().map(|expr| {
                    expr.alias
                        .clone()
                        .unwrap_or_else(|| "similarity".to_string())
                }));
                result.extend(qualified_wildcards.iter().map(|alias| format!("{alias}.*")));
                result.extend(window_functions.iter().map(|wf| {
                    wf.alias
                        .clone()
                        .unwrap_or_else(|| wf.function_type.default_alias().to_string())
                }));
                result
            }
            Self::SimilarityScore(expr) => {
                vec![expr
                    .alias
                    .clone()
                    .unwrap_or_else(|| "similarity".to_string())]
            }
            Self::QualifiedWildcard(alias) => vec![format!("{alias}.*")],
        }
    }
}

/// A `similarity()` zero-arg expression in SELECT, with optional alias.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimilarityScoreExpr {
    /// Optional alias (e.g., `similarity() AS relevance`).
    pub alias: Option<String>,
}

/// A column reference.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Column {
    /// Column name.
    pub name: String,
    /// Optional alias.
    pub alias: Option<String>,
}

impl Column {
    /// Creates a new column reference.
    #[must_use]
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            alias: None,
        }
    }

    /// Creates a column with an alias.
    #[must_use]
    pub fn with_alias(name: impl Into<String>, alias: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            alias: Some(alias.into()),
        }
    }
}

/// ORDER BY item for sorting SELECT results.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SelectOrderBy {
    /// Expression to order by.
    pub expr: OrderByExpr,
    /// Sort direction (true = DESC).
    pub descending: bool,
}

impl SelectOrderBy {
    /// Returns a `(column_name, direction)` pair for display.
    #[must_use]
    pub fn to_display_pair(&self) -> (String, String) {
        let dir = if self.descending { "DESC" } else { "ASC" };
        let col = match &self.expr {
            OrderByExpr::Field(f) => f.clone(),
            OrderByExpr::Similarity(_) | OrderByExpr::SimilarityBare => "similarity()".to_string(),
            OrderByExpr::Aggregate(agg) => format!("{:?}", agg.function_type),
            OrderByExpr::Arithmetic(expr) => format!("{expr}"),
        };
        (col, dir.to_string())
    }
}

/// Expression types supported in ORDER BY clause.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum OrderByExpr {
    /// Simple field reference.
    Field(String),
    /// Similarity function with field and vector args.
    Similarity(SimilarityOrderBy),
    /// Similarity zero-arg: uses pre-computed search score.
    SimilarityBare,
    /// Aggregate function.
    Aggregate(AggregateFunction),
    /// Arithmetic expression combining scores (EPIC-042).
    ///
    /// Example: `0.7 * vector_score + 0.3 * graph_score`
    Arithmetic(ArithmeticExpr),
}

/// A named score binding defined by a `LET` clause (VelesQL v1.10 Phase 3).
///
/// Each binding assigns an arithmetic expression to a name. Bindings are
/// evaluated in declaration order; later bindings may reference earlier ones.
///
/// # Example
///
/// ```sql
/// LET hybrid = 0.7 * vector_score + 0.3 * bm25_score
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LetBinding {
    /// Binding name (identifier).
    pub name: String,
    /// Expression to evaluate.
    pub expr: ArithmeticExpr,
}

/// Arithmetic expression for ORDER BY custom scoring (EPIC-042).
///
/// Supports binary operations (+, -, *, /) with numeric literals,
/// variables (field references), and similarity() function calls.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ArithmeticExpr {
    /// Numeric literal (e.g., `0.7`, `2`).
    Literal(f64),
    /// Score variable or field reference (e.g., `vector_score`, `price`).
    Variable(String),
    /// Similarity function call (zero-arg or with field+vector).
    Similarity(Box<OrderByExpr>),
    /// Binary operation with operator precedence.
    BinaryOp {
        /// Left operand.
        left: Box<ArithmeticExpr>,
        /// Arithmetic operator.
        op: ArithmeticOp,
        /// Right operand.
        right: Box<ArithmeticExpr>,
    },
}

/// Arithmetic operators for ORDER BY expressions (EPIC-042).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ArithmeticOp {
    /// Addition (`+`).
    Add,
    /// Subtraction (`-`).
    Sub,
    /// Multiplication (`*`).
    Mul,
    /// Division (`/`).
    Div,
}

impl fmt::Display for ArithmeticOp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Add => write!(f, "+"),
            Self::Sub => write!(f, "-"),
            Self::Mul => write!(f, "*"),
            Self::Div => write!(f, "/"),
        }
    }
}

impl fmt::Display for ArithmeticExpr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Literal(v) => write!(f, "{v}"),
            Self::Variable(name) => write!(f, "{name}"),
            Self::Similarity(inner) => match inner.as_ref() {
                OrderByExpr::Similarity(sim) => {
                    let vec_str = match &sim.vector {
                        VectorExpr::Parameter(name) => format!("${name}"),
                        VectorExpr::Literal(vals) => format!("{vals:?}"),
                    };
                    write!(f, "similarity({}, {vec_str})", sim.field)
                }
                _ => write!(f, "similarity()"),
            },
            Self::BinaryOp { left, op, right } => write!(f, "({left} {op} {right})"),
        }
    }
}

/// Similarity expression for ORDER BY.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimilarityOrderBy {
    /// Field containing the embedding vector.
    pub field: String,
    /// Vector to compare against.
    pub vector: VectorExpr,
}

impl SelectStatement {
    /// Returns the table names and aliases visible as *outer* scope for a nested
    /// subquery: the `FROM` collection plus any `FROM`/JOIN aliases.
    ///
    /// A subquery's inner WHERE is only **correlated** when it references one of
    /// these names; a dotted payload path whose prefix is not in this set is a
    /// plain payload filter, not a correlation (EPIC-039).
    #[must_use]
    pub fn outer_table_scope(&self) -> Vec<&str> {
        std::iter::once(self.from.as_str())
            .chain(self.from_alias.iter().map(String::as_str))
            .collect()
    }

    /// Returns an empty `SelectStatement` with all fields at their defaults.
    ///
    /// Used by [`crate::velesql::Query::new_dml`],
    /// [`crate::velesql::Query::new_train`], and
    /// [`crate::velesql::Query::new_match`] to avoid repeating the 14-field
    /// struct literal.
    #[must_use]
    pub fn empty() -> Self {
        Self {
            distinct: DistinctMode::None,
            columns: SelectColumns::All,
            from: String::new(),
            from_alias: Vec::new(),
            joins: Vec::new(),
            where_clause: None,
            order_by: None,
            limit: None,
            offset: None,
            with_clause: None,
            group_by: None,
            having: None,
            fusion_clause: None,
        }
    }

    /// Returns `true` when this SELECT must run through the aggregation engine
    /// (scalar aggregates or `GROUP BY`) rather than the row-projection path.
    ///
    /// `NEAR ... GROUP BY` (vector-search grouping) is post-processed inside the
    /// standard execute path, not the aggregate engine, so it returns `false`.
    /// Single source of truth shared by the server `/query` handler and the CLI
    /// REPL so every surface routes aggregation identically.
    #[must_use]
    pub fn is_aggregation_query(&self) -> bool {
        let has_aggs = match &self.columns {
            SelectColumns::Aggregations(_) => true,
            SelectColumns::Mixed { aggregations, .. } => !aggregations.is_empty(),
            _ => false,
        };
        let is_agg_query = has_aggs || self.group_by.is_some();
        if is_agg_query && self.group_by.is_some() {
            let has_vector_near = self
                .where_clause
                .as_ref()
                .is_some_and(Condition::has_vector_search);
            if has_vector_near {
                return false;
            }
        }
        is_agg_query
    }
}