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    /// Wraps a bare [`SelectStatement`] into a plain SELECT [`Query`].
93    ///
94    /// Used to execute the inner SELECT of a scalar subquery (EPIC-039), which
95    /// the parser stores as a `SelectStatement` rather than a full `Query`.
96    /// Alias of [`Self::new_select`] for call-site readability at the subquery
97    /// boundary.
98    #[must_use]
99    pub fn from_select(select: SelectStatement) -> Self {
100        Self::new_select(select)
101    }
102
103    /// Returns true if this is a MATCH query.
104    #[must_use]
105    pub fn is_match_query(&self) -> bool {
106        self.match_clause.is_some()
107    }
108
109    /// Returns true if this is a SELECT query.
110    #[must_use]
111    pub fn is_select_query(&self) -> bool {
112        self.match_clause.is_none()
113            && self.dml.is_none()
114            && self.train.is_none()
115            && self.ddl.is_none()
116            && self.introspection.is_none()
117            && self.admin.is_none()
118    }
119
120    /// Returns true if this is a DML query.
121    #[must_use]
122    pub fn is_dml_query(&self) -> bool {
123        self.dml.is_some()
124    }
125
126    /// Returns true if this is a TRAIN statement.
127    #[must_use]
128    pub fn is_train(&self) -> bool {
129        self.train.is_some()
130    }
131
132    /// Returns true if this is a DDL statement (CREATE/DROP COLLECTION).
133    #[must_use]
134    pub fn is_ddl_query(&self) -> bool {
135        self.ddl.is_some()
136    }
137
138    /// Returns true if this is an introspection statement (SHOW/DESCRIBE/EXPLAIN).
139    #[must_use]
140    pub fn is_introspection_query(&self) -> bool {
141        self.introspection.is_some()
142    }
143
144    /// Returns true if this is an admin statement (FLUSH).
145    #[must_use]
146    pub fn is_admin_query(&self) -> bool {
147        self.admin.is_some()
148    }
149
150    /// Returns true if this is a SELECT EDGES query.
151    #[must_use]
152    pub fn is_select_edges_query(&self) -> bool {
153        matches!(self.dml, Some(DmlStatement::SelectEdges(_)))
154    }
155
156    /// Iterates every HAVING clause of the query — the main SELECT plus every
157    /// compound operand (UNION/INTERSECT/EXCEPT) — paired with its owning
158    /// statement (whose FROM/aliases scope any correlated subquery). HAVING
159    /// thresholds live outside the WHERE condition tree, so callers that walk
160    /// WHERE must check these too.
161    fn having_clauses(&self) -> impl Iterator<Item = (&SelectStatement, &HavingClause)> {
162        let compound_stmts = self
163            .compound
164            .iter()
165            .flat_map(|c| c.operations.iter().map(|(_, stmt)| stmt));
166        std::iter::once(&self.select)
167            .chain(compound_stmts)
168            .filter_map(|stmt| stmt.having.as_ref().map(|h| (stmt, h)))
169    }
170
171    /// Returns `true` if any HAVING threshold value is a scalar subquery.
172    #[must_use]
173    pub fn has_having_subquery(&self) -> bool {
174        self.having_clauses()
175            .any(|(_, having)| having.has_subquery())
176    }
177
178    /// Returns `true` if any HAVING threshold is a subquery **genuinely
179    /// correlated** against its owning SELECT's tables/aliases (rejected by
180    /// validation). A HAVING subquery that only filters on a payload path is
181    /// resolvable, not correlated.
182    #[must_use]
183    pub fn has_correlated_having_subquery(&self) -> bool {
184        self.having_clauses()
185            .any(|(stmt, having)| having.has_correlated_subquery(&stmt.outer_table_scope()))
186    }
187
188    /// Returns true if this is an INSERT NODE query.
189    #[must_use]
190    pub fn is_insert_node_query(&self) -> bool {
191        matches!(self.dml, Some(DmlStatement::InsertNode(_)))
192    }
193
194    /// Extracts the collection name from a DML statement, if present.
195    #[must_use]
196    pub fn dml_collection_name(&self) -> Option<&str> {
197        let name = match self.dml.as_ref()? {
198            DmlStatement::Insert(s) | DmlStatement::Upsert(s) => &s.table,
199            DmlStatement::Update(s) => &s.table,
200            DmlStatement::Delete(s) => &s.table,
201            DmlStatement::InsertEdge(s) => &s.collection,
202            DmlStatement::DeleteEdge(s) => &s.collection,
203            DmlStatement::SelectEdges(s) => &s.collection,
204            DmlStatement::InsertNode(s) => &s.collection,
205        };
206        if name.is_empty() {
207            None
208        } else {
209            Some(name)
210        }
211    }
212
213    /// Creates a new SELECT query.
214    #[must_use]
215    pub fn new_select(select: SelectStatement) -> Self {
216        Self {
217            let_bindings: Vec::new(),
218            select,
219            compound: None,
220            match_clause: None,
221            dml: None,
222            train: None,
223            ddl: None,
224            introspection: None,
225            admin: None,
226        }
227    }
228
229    /// Creates a new MATCH query (EPIC-045).
230    #[must_use]
231    pub fn new_match(match_clause: crate::velesql::MatchClause) -> Self {
232        let mut select = SelectStatement::empty();
233        select.where_clause.clone_from(&match_clause.where_clause);
234        select.limit = match_clause.return_clause.limit;
235        Self {
236            let_bindings: Vec::new(),
237            select,
238            compound: None,
239            match_clause: Some(match_clause),
240            dml: None,
241            train: None,
242            ddl: None,
243            introspection: None,
244            admin: None,
245        }
246    }
247
248    /// Creates a new DML query.
249    #[must_use]
250    pub fn new_dml(dml: DmlStatement) -> Self {
251        Self {
252            let_bindings: Vec::new(),
253            select: SelectStatement::empty(),
254            compound: None,
255            match_clause: None,
256            dml: Some(dml),
257            train: None,
258            ddl: None,
259            introspection: None,
260            admin: None,
261        }
262    }
263
264    /// Creates a new TRAIN query.
265    #[must_use]
266    pub fn new_train(train: TrainStatement) -> Self {
267        Self {
268            let_bindings: Vec::new(),
269            select: SelectStatement::empty(),
270            compound: None,
271            match_clause: None,
272            dml: None,
273            train: Some(train),
274            ddl: None,
275            introspection: None,
276            admin: None,
277        }
278    }
279
280    /// Creates a new DDL query (CREATE/DROP COLLECTION).
281    #[must_use]
282    pub fn new_ddl(ddl: DdlStatement) -> Self {
283        Self {
284            let_bindings: Vec::new(),
285            select: SelectStatement::empty(),
286            compound: None,
287            match_clause: None,
288            dml: None,
289            train: None,
290            ddl: Some(ddl),
291            introspection: None,
292            admin: None,
293        }
294    }
295
296    /// Creates a new introspection query (SHOW/DESCRIBE/EXPLAIN).
297    #[must_use]
298    pub fn new_introspection(stmt: IntrospectionStatement) -> Self {
299        Self {
300            let_bindings: Vec::new(),
301            select: SelectStatement::empty(),
302            compound: None,
303            match_clause: None,
304            dml: None,
305            train: None,
306            ddl: None,
307            introspection: Some(stmt),
308            admin: None,
309        }
310    }
311
312    /// Creates a new admin query (FLUSH).
313    #[must_use]
314    pub fn new_admin(stmt: AdminStatement) -> Self {
315        Self {
316            let_bindings: Vec::new(),
317            select: SelectStatement::empty(),
318            compound: None,
319            match_clause: None,
320            dml: None,
321            train: None,
322            ddl: None,
323            introspection: None,
324            admin: Some(stmt),
325        }
326    }
327}
328
329/// SQL set operator for compound queries (EPIC-040 US-006).
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
331#[non_exhaustive]
332pub enum SetOperator {
333    /// UNION - merge results, remove duplicates.
334    Union,
335    /// UNION ALL - merge results, keep duplicates.
336    UnionAll,
337    /// INTERSECT - keep only common results.
338    Intersect,
339    /// EXCEPT - subtract second query from first.
340    Except,
341}
342
343/// Compound query combining queries with set operators (UNION/INTERSECT/EXCEPT).
344///
345/// Supports N-ary chaining: `SELECT ... UNION SELECT ... INTERSECT SELECT ...`
346/// is represented as `operations: [(Union, B), (Intersect, C)]`, applied left-to-right.
347#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
348pub struct CompoundQuery {
349    /// Chained set operations: `(operator, right_select)` pairs, applied left-to-right.
350    pub operations: Vec<(SetOperator, SelectStatement)>,
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_with_clause_new() {
359        let clause = WithClause::new();
360        assert!(clause.options.is_empty());
361    }
362
363    #[test]
364    fn test_with_clause_with_option() {
365        let clause = WithClause::new()
366            .with_option("mode", WithValue::String("accurate".to_string()))
367            .with_option("ef_search", WithValue::Integer(512));
368        assert_eq!(clause.options.len(), 2);
369    }
370
371    #[test]
372    fn test_with_clause_get() {
373        let clause = WithClause::new().with_option("mode", WithValue::String("fast".to_string()));
374        assert!(clause.get("mode").is_some());
375        assert!(clause.get("MODE").is_some());
376        assert!(clause.get("unknown").is_none());
377    }
378
379    #[test]
380    fn test_with_clause_get_mode() {
381        let clause =
382            WithClause::new().with_option("mode", WithValue::String("accurate".to_string()));
383        assert_eq!(clause.get_mode(), Some("accurate"));
384    }
385
386    #[test]
387    fn test_with_value_as_str() {
388        let v = WithValue::String("test".to_string());
389        assert_eq!(v.as_str(), Some("test"));
390    }
391
392    #[test]
393    fn test_with_value_as_integer() {
394        let v = WithValue::Integer(100);
395        assert_eq!(v.as_integer(), Some(100));
396    }
397
398    #[test]
399    fn test_with_value_as_float() {
400        let v = WithValue::Float(1.234);
401        assert!((v.as_float().unwrap() - 1.234).abs() < 1e-5);
402    }
403
404    #[test]
405    fn test_interval_to_seconds() {
406        assert_eq!(
407            IntervalValue {
408                magnitude: 30,
409                unit: IntervalUnit::Seconds
410            }
411            .to_seconds(),
412            30
413        );
414        assert_eq!(
415            IntervalValue {
416                magnitude: 1,
417                unit: IntervalUnit::Days
418            }
419            .to_seconds(),
420            86400
421        );
422    }
423
424    #[test]
425    fn test_temporal_now() {
426        let expr = TemporalExpr::Now;
427        let epoch = expr.to_epoch_seconds();
428        assert!(epoch > 1_577_836_800);
429    }
430
431    #[test]
432    fn test_value_from_i64() {
433        let v: Value = 42i64.into();
434        assert_eq!(v, Value::Integer(42));
435    }
436
437    #[test]
438    fn test_fusion_config_default() {
439        let config = FusionConfig::default();
440        assert_eq!(config.strategy, "rrf");
441    }
442
443    #[test]
444    fn test_fusion_config_rrf() {
445        let config = FusionConfig::rrf();
446        assert_eq!(config.strategy, "rrf");
447        assert!((config.params.get("k").unwrap() - 60.0).abs() < 1e-5);
448    }
449
450    #[test]
451    fn test_fusion_clause_default() {
452        let clause = FusionClause::default();
453        assert_eq!(clause.strategy, FusionStrategyType::Rrf);
454        assert_eq!(clause.k, Some(60));
455    }
456
457    #[test]
458    fn test_group_by_clause_default() {
459        let clause = GroupByClause::default();
460        assert!(clause.columns.is_empty());
461    }
462
463    #[test]
464    fn test_having_clause_default() {
465        let clause = HavingClause::default();
466        assert!(clause.conditions.is_empty());
467    }
468}