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,
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 select = SelectStatement {
100            distinct: DistinctMode::None,
101            columns: SelectColumns::All,
102            from: String::new(),
103            from_alias: Vec::new(),
104            joins: Vec::new(),
105            where_clause: match_clause.where_clause.clone(),
106            order_by: None,
107            limit: match_clause.return_clause.limit,
108            offset: None,
109            with_clause: None,
110            group_by: None,
111            having: None,
112            fusion_clause: None,
113        };
114        Self {
115            select,
116            compound: None,
117            match_clause: Some(match_clause),
118            dml: None,
119            train: None,
120        }
121    }
122
123    /// Creates a new DML query.
124    #[must_use]
125    pub fn new_dml(dml: DmlStatement) -> Self {
126        let select = SelectStatement {
127            distinct: DistinctMode::None,
128            columns: SelectColumns::All,
129            from: String::new(),
130            from_alias: Vec::new(),
131            joins: Vec::new(),
132            where_clause: None,
133            order_by: None,
134            limit: None,
135            offset: None,
136            with_clause: None,
137            group_by: None,
138            having: None,
139            fusion_clause: None,
140        };
141        Self {
142            select,
143            compound: None,
144            match_clause: None,
145            dml: Some(dml),
146            train: None,
147        }
148    }
149
150    /// Creates a new TRAIN query.
151    #[must_use]
152    pub fn new_train(train: TrainStatement) -> Self {
153        let select = SelectStatement {
154            distinct: DistinctMode::None,
155            columns: SelectColumns::All,
156            from: String::new(),
157            from_alias: Vec::new(),
158            joins: Vec::new(),
159            where_clause: None,
160            order_by: None,
161            limit: None,
162            offset: None,
163            with_clause: None,
164            group_by: None,
165            having: None,
166            fusion_clause: None,
167        };
168        Self {
169            select,
170            compound: None,
171            match_clause: None,
172            dml: None,
173            train: Some(train),
174        }
175    }
176}
177
178/// SQL set operator for compound queries (EPIC-040 US-006).
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180pub enum SetOperator {
181    /// UNION - merge results, remove duplicates.
182    Union,
183    /// UNION ALL - merge results, keep duplicates.
184    UnionAll,
185    /// INTERSECT - keep only common results.
186    Intersect,
187    /// EXCEPT - subtract second query from first.
188    Except,
189}
190
191/// Compound query combining two queries with a set operator.
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193pub struct CompoundQuery {
194    /// The set operator.
195    pub operator: SetOperator,
196    /// The second query.
197    pub right: Box<SelectStatement>,
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_with_clause_new() {
206        let clause = WithClause::new();
207        assert!(clause.options.is_empty());
208    }
209
210    #[test]
211    fn test_with_clause_with_option() {
212        let clause = WithClause::new()
213            .with_option("mode", WithValue::String("accurate".to_string()))
214            .with_option("ef_search", WithValue::Integer(512));
215        assert_eq!(clause.options.len(), 2);
216    }
217
218    #[test]
219    fn test_with_clause_get() {
220        let clause = WithClause::new().with_option("mode", WithValue::String("fast".to_string()));
221        assert!(clause.get("mode").is_some());
222        assert!(clause.get("MODE").is_some());
223        assert!(clause.get("unknown").is_none());
224    }
225
226    #[test]
227    fn test_with_clause_get_mode() {
228        let clause =
229            WithClause::new().with_option("mode", WithValue::String("accurate".to_string()));
230        assert_eq!(clause.get_mode(), Some("accurate"));
231    }
232
233    #[test]
234    fn test_with_value_as_str() {
235        let v = WithValue::String("test".to_string());
236        assert_eq!(v.as_str(), Some("test"));
237    }
238
239    #[test]
240    fn test_with_value_as_integer() {
241        let v = WithValue::Integer(100);
242        assert_eq!(v.as_integer(), Some(100));
243    }
244
245    #[test]
246    fn test_with_value_as_float() {
247        let v = WithValue::Float(1.234);
248        assert!((v.as_float().unwrap() - 1.234).abs() < 1e-5);
249    }
250
251    #[test]
252    fn test_interval_to_seconds() {
253        assert_eq!(
254            IntervalValue {
255                magnitude: 30,
256                unit: IntervalUnit::Seconds
257            }
258            .to_seconds(),
259            30
260        );
261        assert_eq!(
262            IntervalValue {
263                magnitude: 1,
264                unit: IntervalUnit::Days
265            }
266            .to_seconds(),
267            86400
268        );
269    }
270
271    #[test]
272    fn test_temporal_now() {
273        let expr = TemporalExpr::Now;
274        let epoch = expr.to_epoch_seconds();
275        assert!(epoch > 1_577_836_800);
276    }
277
278    #[test]
279    fn test_value_from_i64() {
280        let v: Value = 42i64.into();
281        assert_eq!(v, Value::Integer(42));
282    }
283
284    #[test]
285    fn test_fusion_config_default() {
286        let config = FusionConfig::default();
287        assert_eq!(config.strategy, "rrf");
288    }
289
290    #[test]
291    fn test_fusion_config_rrf() {
292        let config = FusionConfig::rrf();
293        assert_eq!(config.strategy, "rrf");
294        assert!((config.params.get("k").unwrap() - 60.0).abs() < 1e-5);
295    }
296
297    #[test]
298    fn test_fusion_clause_default() {
299        let clause = FusionClause::default();
300        assert_eq!(clause.strategy, FusionStrategyType::Rrf);
301        assert_eq!(clause.k, Some(60));
302    }
303
304    #[test]
305    fn test_group_by_clause_default() {
306        let clause = GroupByClause::default();
307        assert!(clause.columns.is_empty());
308    }
309
310    #[test]
311    fn test_having_clause_default() {
312        let clause = HavingClause::default();
313        assert!(clause.conditions.is_empty());
314    }
315}