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