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 admin;
6mod aggregation;
7pub(crate) mod condition;
8mod ddl;
9mod dml;
10mod fusion;
11mod introspection;
12mod join;
13mod select;
14mod train;
15mod values;
16mod window;
17mod with_clause;
18
19use serde::{Deserialize, Serialize};
20
21// Re-export all types for backward compatibility
22pub use admin::{AdminStatement, FlushStatement};
23pub use aggregation::{
24    AggregateArg, AggregateFunction, AggregateType, GroupByClause, HavingClause, HavingCondition,
25    LogicalOp,
26};
27pub use condition::{
28    BetweenCondition, CompareOp, Comparison, Condition, ContainsCondition, ContainsMode,
29    ContainsTextCondition, GeoBboxCondition, GeoDistanceCondition, GraphMatchPredicate,
30    InCondition, IsNullCondition, LikeCondition, MatchCondition, SimilarityCondition,
31    SparseVectorExpr, SparseVectorSearch, VectorFusedSearch, VectorSearch,
32};
33pub use ddl::{
34    AlterCollectionStatement, AnalyzeStatement, CreateCollectionKind, CreateCollectionStatement,
35    CreateIndexStatement, DdlStatement, DropCollectionStatement, DropIndexStatement,
36    GraphCollectionParams, GraphSchemaMode, SchemaDefinition, TruncateStatement,
37    VectorCollectionParams,
38};
39pub use dml::{
40    DeleteEdgeStatement, DeleteStatement, DmlStatement, InsertEdgeStatement, InsertNodeStatement,
41    InsertStatement, SelectEdgesStatement, UpdateAssignment, UpdateStatement,
42};
43pub use fusion::{FusionClause, FusionConfig, FusionStrategyType};
44pub use introspection::{DescribeCollectionStatement, IntrospectionStatement};
45pub use join::{ColumnRef, JoinClause, JoinCondition, JoinType};
46pub use select::{
47    ArithmeticExpr, ArithmeticOp, Column, DistinctMode, LetBinding, OrderByExpr, SelectColumns,
48    SelectOrderBy, SelectStatement, SimilarityOrderBy, SimilarityScoreExpr,
49};
50pub use train::TrainStatement;
51pub use values::{
52    CorrelatedColumn, IntervalUnit, IntervalValue, Subquery, TemporalExpr, Value, VectorExpr,
53};
54pub use window::{OverClause, WindowFunction, WindowFunctionType, WindowOrderBy};
55pub use with_clause::{QuantizationMode, WithClause, WithOption, WithValue};
56
57/// A complete VelesQL query.
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct Query {
60    /// Named score bindings defined by `LET` clauses (VelesQL v1.10 Phase 3).
61    ///
62    /// Bindings are evaluated in order before ORDER BY; each binding can
63    /// reference earlier bindings, component scores, or literal values.
64    #[serde(default, skip_serializing_if = "Vec::is_empty")]
65    pub let_bindings: Vec<LetBinding>,
66    /// The SELECT statement.
67    pub select: SelectStatement,
68    /// Compound query (UNION/INTERSECT/EXCEPT) - EPIC-040 US-006.
69    #[serde(default)]
70    pub compound: Option<CompoundQuery>,
71    /// MATCH clause for graph pattern matching (EPIC-045 US-001).
72    #[serde(default)]
73    pub match_clause: Option<crate::velesql::MatchClause>,
74    /// Optional DML statement (INSERT/UPDATE/DELETE).
75    #[serde(default)]
76    pub dml: Option<DmlStatement>,
77    /// Optional TRAIN statement (TRAIN QUANTIZER).
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub train: Option<TrainStatement>,
80    /// Optional DDL statement (CREATE/DROP COLLECTION) -- VelesQL v3.3.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub ddl: Option<DdlStatement>,
83    /// Optional introspection statement (SHOW/DESCRIBE/EXPLAIN) -- VelesQL v3.4.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub introspection: Option<IntrospectionStatement>,
86    /// Optional admin statement (FLUSH) -- VelesQL v3.6.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub admin: Option<AdminStatement>,
89}
90
91impl Query {
92    /// Returns true if this is a MATCH query.
93    #[must_use]
94    pub fn is_match_query(&self) -> bool {
95        self.match_clause.is_some()
96    }
97
98    /// Returns true if this is a SELECT query.
99    #[must_use]
100    pub fn is_select_query(&self) -> bool {
101        self.match_clause.is_none()
102            && self.dml.is_none()
103            && self.train.is_none()
104            && self.ddl.is_none()
105            && self.introspection.is_none()
106            && self.admin.is_none()
107    }
108
109    /// Returns true if this is a DML query.
110    #[must_use]
111    pub fn is_dml_query(&self) -> bool {
112        self.dml.is_some()
113    }
114
115    /// Returns true if this is a TRAIN statement.
116    #[must_use]
117    pub fn is_train(&self) -> bool {
118        self.train.is_some()
119    }
120
121    /// Returns true if this is a DDL statement (CREATE/DROP COLLECTION).
122    #[must_use]
123    pub fn is_ddl_query(&self) -> bool {
124        self.ddl.is_some()
125    }
126
127    /// Returns true if this is an introspection statement (SHOW/DESCRIBE/EXPLAIN).
128    #[must_use]
129    pub fn is_introspection_query(&self) -> bool {
130        self.introspection.is_some()
131    }
132
133    /// Returns true if this is an admin statement (FLUSH).
134    #[must_use]
135    pub fn is_admin_query(&self) -> bool {
136        self.admin.is_some()
137    }
138
139    /// Returns true if this is a SELECT EDGES query.
140    #[must_use]
141    pub fn is_select_edges_query(&self) -> bool {
142        matches!(self.dml, Some(DmlStatement::SelectEdges(_)))
143    }
144
145    /// Returns true if this is an INSERT NODE query.
146    #[must_use]
147    pub fn is_insert_node_query(&self) -> bool {
148        matches!(self.dml, Some(DmlStatement::InsertNode(_)))
149    }
150
151    /// Extracts the collection name from a DML statement, if present.
152    #[must_use]
153    pub fn dml_collection_name(&self) -> Option<&str> {
154        let name = match self.dml.as_ref()? {
155            DmlStatement::Insert(s) | DmlStatement::Upsert(s) => &s.table,
156            DmlStatement::Update(s) => &s.table,
157            DmlStatement::Delete(s) => &s.table,
158            DmlStatement::InsertEdge(s) => &s.collection,
159            DmlStatement::DeleteEdge(s) => &s.collection,
160            DmlStatement::SelectEdges(s) => &s.collection,
161            DmlStatement::InsertNode(s) => &s.collection,
162        };
163        if name.is_empty() {
164            None
165        } else {
166            Some(name)
167        }
168    }
169
170    /// Creates a new SELECT query.
171    #[must_use]
172    pub fn new_select(select: SelectStatement) -> Self {
173        Self {
174            let_bindings: Vec::new(),
175            select,
176            compound: None,
177            match_clause: None,
178            dml: None,
179            train: None,
180            ddl: None,
181            introspection: None,
182            admin: None,
183        }
184    }
185
186    /// Creates a new MATCH query (EPIC-045).
187    #[must_use]
188    pub fn new_match(match_clause: crate::velesql::MatchClause) -> Self {
189        let mut select = SelectStatement::empty();
190        select.where_clause.clone_from(&match_clause.where_clause);
191        select.limit = match_clause.return_clause.limit;
192        Self {
193            let_bindings: Vec::new(),
194            select,
195            compound: None,
196            match_clause: Some(match_clause),
197            dml: None,
198            train: None,
199            ddl: None,
200            introspection: None,
201            admin: None,
202        }
203    }
204
205    /// Creates a new DML query.
206    #[must_use]
207    pub fn new_dml(dml: DmlStatement) -> Self {
208        Self {
209            let_bindings: Vec::new(),
210            select: SelectStatement::empty(),
211            compound: None,
212            match_clause: None,
213            dml: Some(dml),
214            train: None,
215            ddl: None,
216            introspection: None,
217            admin: None,
218        }
219    }
220
221    /// Creates a new TRAIN query.
222    #[must_use]
223    pub fn new_train(train: TrainStatement) -> Self {
224        Self {
225            let_bindings: Vec::new(),
226            select: SelectStatement::empty(),
227            compound: None,
228            match_clause: None,
229            dml: None,
230            train: Some(train),
231            ddl: None,
232            introspection: None,
233            admin: None,
234        }
235    }
236
237    /// Creates a new DDL query (CREATE/DROP COLLECTION).
238    #[must_use]
239    pub fn new_ddl(ddl: DdlStatement) -> Self {
240        Self {
241            let_bindings: Vec::new(),
242            select: SelectStatement::empty(),
243            compound: None,
244            match_clause: None,
245            dml: None,
246            train: None,
247            ddl: Some(ddl),
248            introspection: None,
249            admin: None,
250        }
251    }
252
253    /// Creates a new introspection query (SHOW/DESCRIBE/EXPLAIN).
254    #[must_use]
255    pub fn new_introspection(stmt: IntrospectionStatement) -> Self {
256        Self {
257            let_bindings: Vec::new(),
258            select: SelectStatement::empty(),
259            compound: None,
260            match_clause: None,
261            dml: None,
262            train: None,
263            ddl: None,
264            introspection: Some(stmt),
265            admin: None,
266        }
267    }
268
269    /// Creates a new admin query (FLUSH).
270    #[must_use]
271    pub fn new_admin(stmt: AdminStatement) -> Self {
272        Self {
273            let_bindings: Vec::new(),
274            select: SelectStatement::empty(),
275            compound: None,
276            match_clause: None,
277            dml: None,
278            train: None,
279            ddl: None,
280            introspection: None,
281            admin: Some(stmt),
282        }
283    }
284}
285
286/// SQL set operator for compound queries (EPIC-040 US-006).
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[non_exhaustive]
289pub enum SetOperator {
290    /// UNION - merge results, remove duplicates.
291    Union,
292    /// UNION ALL - merge results, keep duplicates.
293    UnionAll,
294    /// INTERSECT - keep only common results.
295    Intersect,
296    /// EXCEPT - subtract second query from first.
297    Except,
298}
299
300/// Compound query combining queries with set operators (UNION/INTERSECT/EXCEPT).
301///
302/// Supports N-ary chaining: `SELECT ... UNION SELECT ... INTERSECT SELECT ...`
303/// is represented as `operations: [(Union, B), (Intersect, C)]`, applied left-to-right.
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
305pub struct CompoundQuery {
306    /// Chained set operations: `(operator, right_select)` pairs, applied left-to-right.
307    pub operations: Vec<(SetOperator, SelectStatement)>,
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn test_with_clause_new() {
316        let clause = WithClause::new();
317        assert!(clause.options.is_empty());
318    }
319
320    #[test]
321    fn test_with_clause_with_option() {
322        let clause = WithClause::new()
323            .with_option("mode", WithValue::String("accurate".to_string()))
324            .with_option("ef_search", WithValue::Integer(512));
325        assert_eq!(clause.options.len(), 2);
326    }
327
328    #[test]
329    fn test_with_clause_get() {
330        let clause = WithClause::new().with_option("mode", WithValue::String("fast".to_string()));
331        assert!(clause.get("mode").is_some());
332        assert!(clause.get("MODE").is_some());
333        assert!(clause.get("unknown").is_none());
334    }
335
336    #[test]
337    fn test_with_clause_get_mode() {
338        let clause =
339            WithClause::new().with_option("mode", WithValue::String("accurate".to_string()));
340        assert_eq!(clause.get_mode(), Some("accurate"));
341    }
342
343    #[test]
344    fn test_with_value_as_str() {
345        let v = WithValue::String("test".to_string());
346        assert_eq!(v.as_str(), Some("test"));
347    }
348
349    #[test]
350    fn test_with_value_as_integer() {
351        let v = WithValue::Integer(100);
352        assert_eq!(v.as_integer(), Some(100));
353    }
354
355    #[test]
356    fn test_with_value_as_float() {
357        let v = WithValue::Float(1.234);
358        assert!((v.as_float().unwrap() - 1.234).abs() < 1e-5);
359    }
360
361    #[test]
362    fn test_interval_to_seconds() {
363        assert_eq!(
364            IntervalValue {
365                magnitude: 30,
366                unit: IntervalUnit::Seconds
367            }
368            .to_seconds(),
369            30
370        );
371        assert_eq!(
372            IntervalValue {
373                magnitude: 1,
374                unit: IntervalUnit::Days
375            }
376            .to_seconds(),
377            86400
378        );
379    }
380
381    #[test]
382    fn test_temporal_now() {
383        let expr = TemporalExpr::Now;
384        let epoch = expr.to_epoch_seconds();
385        assert!(epoch > 1_577_836_800);
386    }
387
388    #[test]
389    fn test_value_from_i64() {
390        let v: Value = 42i64.into();
391        assert_eq!(v, Value::Integer(42));
392    }
393
394    #[test]
395    fn test_fusion_config_default() {
396        let config = FusionConfig::default();
397        assert_eq!(config.strategy, "rrf");
398    }
399
400    #[test]
401    fn test_fusion_config_rrf() {
402        let config = FusionConfig::rrf();
403        assert_eq!(config.strategy, "rrf");
404        assert!((config.params.get("k").unwrap() - 60.0).abs() < 1e-5);
405    }
406
407    #[test]
408    fn test_fusion_clause_default() {
409        let clause = FusionClause::default();
410        assert_eq!(clause.strategy, FusionStrategyType::Rrf);
411        assert_eq!(clause.k, Some(60));
412    }
413
414    #[test]
415    fn test_group_by_clause_default() {
416        let clause = GroupByClause::default();
417        assert!(clause.columns.is_empty());
418    }
419
420    #[test]
421    fn test_having_clause_default() {
422        let clause = HavingClause::default();
423        assert!(clause.conditions.is_empty());
424    }
425}