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, DEFAULT_SELECT_LIMIT,
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 any HAVING threshold value — in the main SELECT or
146    /// in compound operands (UNION/INTERSECT/EXCEPT) — is a scalar subquery.
147    ///
148    /// HAVING thresholds are not part of the WHERE condition tree, so
149    /// [`Condition::has_subquery`] never visits them; V010 validation checks
150    /// both.
151    #[must_use]
152    pub fn has_having_subquery(&self) -> bool {
153        let compound_stmts = self
154            .compound
155            .iter()
156            .flat_map(|c| c.operations.iter().map(|(_, stmt)| stmt));
157        std::iter::once(&self.select)
158            .chain(compound_stmts)
159            .filter_map(|stmt| stmt.having.as_ref())
160            .any(HavingClause::has_subquery)
161    }
162
163    /// Returns true if this is an INSERT NODE query.
164    #[must_use]
165    pub fn is_insert_node_query(&self) -> bool {
166        matches!(self.dml, Some(DmlStatement::InsertNode(_)))
167    }
168
169    /// Extracts the collection name from a DML statement, if present.
170    #[must_use]
171    pub fn dml_collection_name(&self) -> Option<&str> {
172        let name = match self.dml.as_ref()? {
173            DmlStatement::Insert(s) | DmlStatement::Upsert(s) => &s.table,
174            DmlStatement::Update(s) => &s.table,
175            DmlStatement::Delete(s) => &s.table,
176            DmlStatement::InsertEdge(s) => &s.collection,
177            DmlStatement::DeleteEdge(s) => &s.collection,
178            DmlStatement::SelectEdges(s) => &s.collection,
179            DmlStatement::InsertNode(s) => &s.collection,
180        };
181        if name.is_empty() {
182            None
183        } else {
184            Some(name)
185        }
186    }
187
188    /// Creates a new SELECT query.
189    #[must_use]
190    pub fn new_select(select: SelectStatement) -> Self {
191        Self {
192            let_bindings: Vec::new(),
193            select,
194            compound: None,
195            match_clause: None,
196            dml: None,
197            train: None,
198            ddl: None,
199            introspection: None,
200            admin: None,
201        }
202    }
203
204    /// Creates a new MATCH query (EPIC-045).
205    #[must_use]
206    pub fn new_match(match_clause: crate::velesql::MatchClause) -> Self {
207        let mut select = SelectStatement::empty();
208        select.where_clause.clone_from(&match_clause.where_clause);
209        select.limit = match_clause.return_clause.limit;
210        Self {
211            let_bindings: Vec::new(),
212            select,
213            compound: None,
214            match_clause: Some(match_clause),
215            dml: None,
216            train: None,
217            ddl: None,
218            introspection: None,
219            admin: None,
220        }
221    }
222
223    /// Creates a new DML query.
224    #[must_use]
225    pub fn new_dml(dml: DmlStatement) -> Self {
226        Self {
227            let_bindings: Vec::new(),
228            select: SelectStatement::empty(),
229            compound: None,
230            match_clause: None,
231            dml: Some(dml),
232            train: None,
233            ddl: None,
234            introspection: None,
235            admin: None,
236        }
237    }
238
239    /// Creates a new TRAIN query.
240    #[must_use]
241    pub fn new_train(train: TrainStatement) -> Self {
242        Self {
243            let_bindings: Vec::new(),
244            select: SelectStatement::empty(),
245            compound: None,
246            match_clause: None,
247            dml: None,
248            train: Some(train),
249            ddl: None,
250            introspection: None,
251            admin: None,
252        }
253    }
254
255    /// Creates a new DDL query (CREATE/DROP COLLECTION).
256    #[must_use]
257    pub fn new_ddl(ddl: DdlStatement) -> Self {
258        Self {
259            let_bindings: Vec::new(),
260            select: SelectStatement::empty(),
261            compound: None,
262            match_clause: None,
263            dml: None,
264            train: None,
265            ddl: Some(ddl),
266            introspection: None,
267            admin: None,
268        }
269    }
270
271    /// Creates a new introspection query (SHOW/DESCRIBE/EXPLAIN).
272    #[must_use]
273    pub fn new_introspection(stmt: IntrospectionStatement) -> Self {
274        Self {
275            let_bindings: Vec::new(),
276            select: SelectStatement::empty(),
277            compound: None,
278            match_clause: None,
279            dml: None,
280            train: None,
281            ddl: None,
282            introspection: Some(stmt),
283            admin: None,
284        }
285    }
286
287    /// Creates a new admin query (FLUSH).
288    #[must_use]
289    pub fn new_admin(stmt: AdminStatement) -> Self {
290        Self {
291            let_bindings: Vec::new(),
292            select: SelectStatement::empty(),
293            compound: None,
294            match_clause: None,
295            dml: None,
296            train: None,
297            ddl: None,
298            introspection: None,
299            admin: Some(stmt),
300        }
301    }
302}
303
304/// SQL set operator for compound queries (EPIC-040 US-006).
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
306#[non_exhaustive]
307pub enum SetOperator {
308    /// UNION - merge results, remove duplicates.
309    Union,
310    /// UNION ALL - merge results, keep duplicates.
311    UnionAll,
312    /// INTERSECT - keep only common results.
313    Intersect,
314    /// EXCEPT - subtract second query from first.
315    Except,
316}
317
318/// Compound query combining queries with set operators (UNION/INTERSECT/EXCEPT).
319///
320/// Supports N-ary chaining: `SELECT ... UNION SELECT ... INTERSECT SELECT ...`
321/// is represented as `operations: [(Union, B), (Intersect, C)]`, applied left-to-right.
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323pub struct CompoundQuery {
324    /// Chained set operations: `(operator, right_select)` pairs, applied left-to-right.
325    pub operations: Vec<(SetOperator, SelectStatement)>,
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_with_clause_new() {
334        let clause = WithClause::new();
335        assert!(clause.options.is_empty());
336    }
337
338    #[test]
339    fn test_with_clause_with_option() {
340        let clause = WithClause::new()
341            .with_option("mode", WithValue::String("accurate".to_string()))
342            .with_option("ef_search", WithValue::Integer(512));
343        assert_eq!(clause.options.len(), 2);
344    }
345
346    #[test]
347    fn test_with_clause_get() {
348        let clause = WithClause::new().with_option("mode", WithValue::String("fast".to_string()));
349        assert!(clause.get("mode").is_some());
350        assert!(clause.get("MODE").is_some());
351        assert!(clause.get("unknown").is_none());
352    }
353
354    #[test]
355    fn test_with_clause_get_mode() {
356        let clause =
357            WithClause::new().with_option("mode", WithValue::String("accurate".to_string()));
358        assert_eq!(clause.get_mode(), Some("accurate"));
359    }
360
361    #[test]
362    fn test_with_value_as_str() {
363        let v = WithValue::String("test".to_string());
364        assert_eq!(v.as_str(), Some("test"));
365    }
366
367    #[test]
368    fn test_with_value_as_integer() {
369        let v = WithValue::Integer(100);
370        assert_eq!(v.as_integer(), Some(100));
371    }
372
373    #[test]
374    fn test_with_value_as_float() {
375        let v = WithValue::Float(1.234);
376        assert!((v.as_float().unwrap() - 1.234).abs() < 1e-5);
377    }
378
379    #[test]
380    fn test_interval_to_seconds() {
381        assert_eq!(
382            IntervalValue {
383                magnitude: 30,
384                unit: IntervalUnit::Seconds
385            }
386            .to_seconds(),
387            30
388        );
389        assert_eq!(
390            IntervalValue {
391                magnitude: 1,
392                unit: IntervalUnit::Days
393            }
394            .to_seconds(),
395            86400
396        );
397    }
398
399    #[test]
400    fn test_temporal_now() {
401        let expr = TemporalExpr::Now;
402        let epoch = expr.to_epoch_seconds();
403        assert!(epoch > 1_577_836_800);
404    }
405
406    #[test]
407    fn test_value_from_i64() {
408        let v: Value = 42i64.into();
409        assert_eq!(v, Value::Integer(42));
410    }
411
412    #[test]
413    fn test_fusion_config_default() {
414        let config = FusionConfig::default();
415        assert_eq!(config.strategy, "rrf");
416    }
417
418    #[test]
419    fn test_fusion_config_rrf() {
420        let config = FusionConfig::rrf();
421        assert_eq!(config.strategy, "rrf");
422        assert!((config.params.get("k").unwrap() - 60.0).abs() < 1e-5);
423    }
424
425    #[test]
426    fn test_fusion_clause_default() {
427        let clause = FusionClause::default();
428        assert_eq!(clause.strategy, FusionStrategyType::Rrf);
429        assert_eq!(clause.k, Some(60));
430    }
431
432    #[test]
433    fn test_group_by_clause_default() {
434        let clause = GroupByClause::default();
435        assert!(clause.columns.is_empty());
436    }
437
438    #[test]
439    fn test_having_clause_default() {
440        let clause = HavingClause::default();
441        assert!(clause.conditions.is_empty());
442    }
443}