Skip to main content

velesdb_core/velesql/ast/
mod.rs

1//! Abstract Syntax Tree (AST) for VelesQL queries.
2//!
3//! This module defines the data structures representing parsed VelesQL queries.
4
5mod aggregation;
6pub(crate) mod condition;
7mod dml;
8mod fusion;
9mod join;
10mod select;
11mod train;
12mod values;
13mod with_clause;
14
15use serde::{Deserialize, Serialize};
16
17// Re-export all types for backward compatibility
18pub use aggregation::{
19    AggregateArg, AggregateFunction, AggregateType, GroupByClause, HavingClause, HavingCondition,
20    LogicalOp,
21};
22pub use condition::{
23    BetweenCondition, CompareOp, Comparison, Condition, GraphMatchPredicate, InCondition,
24    IsNullCondition, LikeCondition, MatchCondition, SimilarityCondition, SparseVectorExpr,
25    SparseVectorSearch, VectorFusedSearch, VectorSearch,
26};
27pub use dml::{DmlStatement, InsertStatement, UpdateAssignment, UpdateStatement};
28pub use fusion::{FusionClause, FusionConfig, FusionStrategyType};
29pub use join::{ColumnRef, JoinClause, JoinCondition, JoinType};
30pub use select::{
31    Column, DistinctMode, OrderByExpr, SelectColumns, SelectOrderBy, SelectStatement,
32    SimilarityOrderBy, SimilarityScoreExpr,
33};
34pub use train::TrainStatement;
35pub use values::{
36    CorrelatedColumn, IntervalUnit, IntervalValue, Subquery, TemporalExpr, Value, VectorExpr,
37};
38pub use with_clause::{QuantizationMode, WithClause, WithOption, WithValue};
39
40/// A complete VelesQL query.
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub struct Query {
43    /// The SELECT statement.
44    pub select: SelectStatement,
45    /// Compound query (UNION/INTERSECT/EXCEPT) - EPIC-040 US-006.
46    #[serde(default)]
47    pub compound: Option<CompoundQuery>,
48    /// MATCH clause for graph pattern matching (EPIC-045 US-001).
49    #[serde(default)]
50    pub match_clause: Option<crate::velesql::MatchClause>,
51    /// Optional DML statement (INSERT/UPDATE).
52    #[serde(default)]
53    pub dml: Option<DmlStatement>,
54    /// Optional TRAIN statement (TRAIN QUANTIZER).
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub train: Option<TrainStatement>,
57}
58
59impl Query {
60    /// Returns true if this is a MATCH query.
61    #[must_use]
62    pub fn is_match_query(&self) -> bool {
63        self.match_clause.is_some()
64    }
65
66    /// Returns true if this is a SELECT query.
67    #[must_use]
68    pub fn is_select_query(&self) -> bool {
69        self.match_clause.is_none() && self.dml.is_none() && self.train.is_none()
70    }
71
72    /// Returns true if this is a DML query.
73    #[must_use]
74    pub fn is_dml_query(&self) -> bool {
75        self.dml.is_some()
76    }
77
78    /// Returns true if this is a TRAIN statement.
79    #[must_use]
80    pub fn is_train(&self) -> bool {
81        self.train.is_some()
82    }
83
84    /// Creates a new SELECT query.
85    #[must_use]
86    pub fn new_select(select: SelectStatement) -> Self {
87        Self {
88            select,
89            compound: None,
90            match_clause: None,
91            dml: None,
92            train: None,
93        }
94    }
95
96    /// Creates a new MATCH query (EPIC-045).
97    #[must_use]
98    pub fn new_match(match_clause: crate::velesql::MatchClause) -> Self {
99        let mut select = SelectStatement::empty();
100        select.where_clause.clone_from(&match_clause.where_clause);
101        select.limit = match_clause.return_clause.limit;
102        Self {
103            select,
104            compound: None,
105            match_clause: Some(match_clause),
106            dml: None,
107            train: None,
108        }
109    }
110
111    /// Creates a new DML query.
112    #[must_use]
113    pub fn new_dml(dml: DmlStatement) -> Self {
114        Self {
115            select: SelectStatement::empty(),
116            compound: None,
117            match_clause: None,
118            dml: Some(dml),
119            train: None,
120        }
121    }
122
123    /// Creates a new TRAIN query.
124    #[must_use]
125    pub fn new_train(train: TrainStatement) -> Self {
126        Self {
127            select: SelectStatement::empty(),
128            compound: None,
129            match_clause: None,
130            dml: None,
131            train: Some(train),
132        }
133    }
134}
135
136/// SQL set operator for compound queries (EPIC-040 US-006).
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138pub enum SetOperator {
139    /// UNION - merge results, remove duplicates.
140    Union,
141    /// UNION ALL - merge results, keep duplicates.
142    UnionAll,
143    /// INTERSECT - keep only common results.
144    Intersect,
145    /// EXCEPT - subtract second query from first.
146    Except,
147}
148
149/// Compound query combining two queries with a set operator.
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct CompoundQuery {
152    /// The set operator.
153    pub operator: SetOperator,
154    /// The second query.
155    pub right: Box<SelectStatement>,
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_with_clause_new() {
164        let clause = WithClause::new();
165        assert!(clause.options.is_empty());
166    }
167
168    #[test]
169    fn test_with_clause_with_option() {
170        let clause = WithClause::new()
171            .with_option("mode", WithValue::String("accurate".to_string()))
172            .with_option("ef_search", WithValue::Integer(512));
173        assert_eq!(clause.options.len(), 2);
174    }
175
176    #[test]
177    fn test_with_clause_get() {
178        let clause = WithClause::new().with_option("mode", WithValue::String("fast".to_string()));
179        assert!(clause.get("mode").is_some());
180        assert!(clause.get("MODE").is_some());
181        assert!(clause.get("unknown").is_none());
182    }
183
184    #[test]
185    fn test_with_clause_get_mode() {
186        let clause =
187            WithClause::new().with_option("mode", WithValue::String("accurate".to_string()));
188        assert_eq!(clause.get_mode(), Some("accurate"));
189    }
190
191    #[test]
192    fn test_with_value_as_str() {
193        let v = WithValue::String("test".to_string());
194        assert_eq!(v.as_str(), Some("test"));
195    }
196
197    #[test]
198    fn test_with_value_as_integer() {
199        let v = WithValue::Integer(100);
200        assert_eq!(v.as_integer(), Some(100));
201    }
202
203    #[test]
204    fn test_with_value_as_float() {
205        let v = WithValue::Float(1.234);
206        assert!((v.as_float().unwrap() - 1.234).abs() < 1e-5);
207    }
208
209    #[test]
210    fn test_interval_to_seconds() {
211        assert_eq!(
212            IntervalValue {
213                magnitude: 30,
214                unit: IntervalUnit::Seconds
215            }
216            .to_seconds(),
217            30
218        );
219        assert_eq!(
220            IntervalValue {
221                magnitude: 1,
222                unit: IntervalUnit::Days
223            }
224            .to_seconds(),
225            86400
226        );
227    }
228
229    #[test]
230    fn test_temporal_now() {
231        let expr = TemporalExpr::Now;
232        let epoch = expr.to_epoch_seconds();
233        assert!(epoch > 1_577_836_800);
234    }
235
236    #[test]
237    fn test_value_from_i64() {
238        let v: Value = 42i64.into();
239        assert_eq!(v, Value::Integer(42));
240    }
241
242    #[test]
243    fn test_fusion_config_default() {
244        let config = FusionConfig::default();
245        assert_eq!(config.strategy, "rrf");
246    }
247
248    #[test]
249    fn test_fusion_config_rrf() {
250        let config = FusionConfig::rrf();
251        assert_eq!(config.strategy, "rrf");
252        assert!((config.params.get("k").unwrap() - 60.0).abs() < 1e-5);
253    }
254
255    #[test]
256    fn test_fusion_clause_default() {
257        let clause = FusionClause::default();
258        assert_eq!(clause.strategy, FusionStrategyType::Rrf);
259        assert_eq!(clause.k, Some(60));
260    }
261
262    #[test]
263    fn test_group_by_clause_default() {
264        let clause = GroupByClause::default();
265        assert!(clause.columns.is_empty());
266    }
267
268    #[test]
269    fn test_having_clause_default() {
270        let clause = HavingClause::default();
271        assert!(clause.conditions.is_empty());
272    }
273}