velesdb-core 5.1.0

High-performance vector database engine written in Rust
Documentation
//! Abstract Syntax Tree (AST) for VelesQL queries.
//!
//! This module defines the data structures representing parsed VelesQL queries.

mod admin;
mod aggregation;
pub(crate) mod condition;
mod ddl;
mod dml;
mod fusion;
mod introspection;
mod join;
mod select;
mod train;
mod values;
mod window;
mod with_clause;

use serde::{Deserialize, Serialize};

// Re-export all types for backward compatibility
pub use admin::{AdminStatement, FlushStatement};
pub use aggregation::{
    AggregateArg, AggregateFunction, AggregateType, GroupByClause, HavingClause, HavingCondition,
    LogicalOp,
};
pub use condition::{
    BetweenCondition, CompareOp, Comparison, Condition, ContainsCondition, ContainsMode,
    ContainsTextCondition, GeoBboxCondition, GeoDistanceCondition, GraphMatchPredicate,
    InCondition, IsNullCondition, LikeCondition, MatchCondition, SimilarityCondition,
    SparseVectorExpr, SparseVectorSearch, VectorFusedSearch, VectorSearch,
};
pub use ddl::{
    AlterCollectionStatement, AnalyzeStatement, CreateCollectionKind, CreateCollectionStatement,
    CreateIndexStatement, DdlStatement, DropCollectionStatement, DropIndexStatement,
    GraphCollectionParams, GraphSchemaMode, SchemaDefinition, TruncateStatement,
    VectorCollectionParams,
};
pub use dml::{
    DeleteEdgeStatement, DeleteStatement, DmlStatement, InsertEdgeStatement, InsertNodeStatement,
    InsertStatement, SelectEdgesStatement, UpdateAssignment, UpdateStatement,
};
pub use fusion::{FusionClause, FusionConfig, FusionStrategyType};
pub use introspection::{DescribeCollectionStatement, IntrospectionStatement};
pub use join::{ColumnRef, JoinClause, JoinCondition, JoinType};
pub use select::{
    ArithmeticExpr, ArithmeticOp, Column, DistinctMode, LetBinding, OrderByExpr, SelectColumns,
    SelectOrderBy, SelectStatement, SimilarityOrderBy, SimilarityScoreExpr, DEFAULT_SELECT_LIMIT,
};
pub use train::TrainStatement;
pub use values::{
    CorrelatedColumn, IntervalUnit, IntervalValue, Subquery, TemporalExpr, Value, VectorExpr,
};
pub use window::{OverClause, WindowFunction, WindowFunctionType, WindowOrderBy};
pub use with_clause::{QuantizationMode, WithClause, WithOption, WithValue};

/// A complete VelesQL query.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Query {
    /// Named score bindings defined by `LET` clauses (VelesQL v1.10 Phase 3).
    ///
    /// Bindings are evaluated in order before ORDER BY; each binding can
    /// reference earlier bindings, component scores, or literal values.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub let_bindings: Vec<LetBinding>,
    /// The SELECT statement.
    pub select: SelectStatement,
    /// Compound query (UNION/INTERSECT/EXCEPT) - EPIC-040 US-006.
    #[serde(default)]
    pub compound: Option<CompoundQuery>,
    /// MATCH clause for graph pattern matching (EPIC-045 US-001).
    #[serde(default)]
    pub match_clause: Option<crate::velesql::MatchClause>,
    /// Optional DML statement (INSERT/UPDATE/DELETE).
    #[serde(default)]
    pub dml: Option<DmlStatement>,
    /// Optional TRAIN statement (TRAIN QUANTIZER).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub train: Option<TrainStatement>,
    /// Optional DDL statement (CREATE/DROP COLLECTION) -- VelesQL v3.3.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ddl: Option<DdlStatement>,
    /// Optional introspection statement (SHOW/DESCRIBE/EXPLAIN) -- VelesQL v3.4.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub introspection: Option<IntrospectionStatement>,
    /// Optional admin statement (FLUSH) -- VelesQL v3.6.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub admin: Option<AdminStatement>,
}

impl Query {
    /// Wraps a bare [`SelectStatement`] into a plain SELECT [`Query`].
    ///
    /// Used to execute the inner SELECT of a scalar subquery (EPIC-039), which
    /// the parser stores as a `SelectStatement` rather than a full `Query`.
    /// Alias of [`Self::new_select`] for call-site readability at the subquery
    /// boundary.
    #[must_use]
    pub fn from_select(select: SelectStatement) -> Self {
        Self::new_select(select)
    }

    /// Returns true if this is a MATCH query.
    #[must_use]
    pub fn is_match_query(&self) -> bool {
        self.match_clause.is_some()
    }

    /// Returns true if this is a SELECT query.
    #[must_use]
    pub fn is_select_query(&self) -> bool {
        self.match_clause.is_none()
            && self.dml.is_none()
            && self.train.is_none()
            && self.ddl.is_none()
            && self.introspection.is_none()
            && self.admin.is_none()
    }

    /// Returns true if this is a DML query.
    #[must_use]
    pub fn is_dml_query(&self) -> bool {
        self.dml.is_some()
    }

    /// Returns true if this is a TRAIN statement.
    #[must_use]
    pub fn is_train(&self) -> bool {
        self.train.is_some()
    }

    /// Returns true if this is a DDL statement (CREATE/DROP COLLECTION).
    #[must_use]
    pub fn is_ddl_query(&self) -> bool {
        self.ddl.is_some()
    }

    /// Returns true if this is an introspection statement (SHOW/DESCRIBE/EXPLAIN).
    #[must_use]
    pub fn is_introspection_query(&self) -> bool {
        self.introspection.is_some()
    }

    /// Returns true if this is an admin statement (FLUSH).
    #[must_use]
    pub fn is_admin_query(&self) -> bool {
        self.admin.is_some()
    }

    /// Returns true if this is a SELECT EDGES query.
    #[must_use]
    pub fn is_select_edges_query(&self) -> bool {
        matches!(self.dml, Some(DmlStatement::SelectEdges(_)))
    }

    /// Iterates every HAVING clause of the query — the main SELECT plus every
    /// compound operand (UNION/INTERSECT/EXCEPT) — paired with its owning
    /// statement (whose FROM/aliases scope any correlated subquery). HAVING
    /// thresholds live outside the WHERE condition tree, so callers that walk
    /// WHERE must check these too.
    fn having_clauses(&self) -> impl Iterator<Item = (&SelectStatement, &HavingClause)> {
        let compound_stmts = self
            .compound
            .iter()
            .flat_map(|c| c.operations.iter().map(|(_, stmt)| stmt));
        std::iter::once(&self.select)
            .chain(compound_stmts)
            .filter_map(|stmt| stmt.having.as_ref().map(|h| (stmt, h)))
    }

    /// Returns `true` if any HAVING threshold value is a scalar subquery.
    #[must_use]
    pub fn has_having_subquery(&self) -> bool {
        self.having_clauses()
            .any(|(_, having)| having.has_subquery())
    }

    /// Returns `true` if any HAVING threshold is a subquery **genuinely
    /// correlated** against its owning SELECT's tables/aliases (rejected by
    /// validation). A HAVING subquery that only filters on a payload path is
    /// resolvable, not correlated.
    #[must_use]
    pub fn has_correlated_having_subquery(&self) -> bool {
        self.having_clauses()
            .any(|(stmt, having)| having.has_correlated_subquery(&stmt.outer_table_scope()))
    }

    /// Returns true if this is an INSERT NODE query.
    #[must_use]
    pub fn is_insert_node_query(&self) -> bool {
        matches!(self.dml, Some(DmlStatement::InsertNode(_)))
    }

    /// Extracts the collection name from a DML statement, if present.
    #[must_use]
    pub fn dml_collection_name(&self) -> Option<&str> {
        let name = match self.dml.as_ref()? {
            DmlStatement::Insert(s) | DmlStatement::Upsert(s) => &s.table,
            DmlStatement::Update(s) => &s.table,
            DmlStatement::Delete(s) => &s.table,
            DmlStatement::InsertEdge(s) => &s.collection,
            DmlStatement::DeleteEdge(s) => &s.collection,
            DmlStatement::SelectEdges(s) => &s.collection,
            DmlStatement::InsertNode(s) => &s.collection,
        };
        if name.is_empty() {
            None
        } else {
            Some(name)
        }
    }

    /// Creates a new SELECT query.
    #[must_use]
    pub fn new_select(select: SelectStatement) -> Self {
        Self {
            let_bindings: Vec::new(),
            select,
            compound: None,
            match_clause: None,
            dml: None,
            train: None,
            ddl: None,
            introspection: None,
            admin: None,
        }
    }

    /// Creates a new MATCH query (EPIC-045).
    #[must_use]
    pub fn new_match(match_clause: crate::velesql::MatchClause) -> Self {
        let mut select = SelectStatement::empty();
        select.where_clause.clone_from(&match_clause.where_clause);
        select.limit = match_clause.return_clause.limit;
        Self {
            let_bindings: Vec::new(),
            select,
            compound: None,
            match_clause: Some(match_clause),
            dml: None,
            train: None,
            ddl: None,
            introspection: None,
            admin: None,
        }
    }

    /// Creates a new DML query.
    #[must_use]
    pub fn new_dml(dml: DmlStatement) -> Self {
        Self {
            let_bindings: Vec::new(),
            select: SelectStatement::empty(),
            compound: None,
            match_clause: None,
            dml: Some(dml),
            train: None,
            ddl: None,
            introspection: None,
            admin: None,
        }
    }

    /// Creates a new TRAIN query.
    #[must_use]
    pub fn new_train(train: TrainStatement) -> Self {
        Self {
            let_bindings: Vec::new(),
            select: SelectStatement::empty(),
            compound: None,
            match_clause: None,
            dml: None,
            train: Some(train),
            ddl: None,
            introspection: None,
            admin: None,
        }
    }

    /// Creates a new DDL query (CREATE/DROP COLLECTION).
    #[must_use]
    pub fn new_ddl(ddl: DdlStatement) -> Self {
        Self {
            let_bindings: Vec::new(),
            select: SelectStatement::empty(),
            compound: None,
            match_clause: None,
            dml: None,
            train: None,
            ddl: Some(ddl),
            introspection: None,
            admin: None,
        }
    }

    /// Creates a new introspection query (SHOW/DESCRIBE/EXPLAIN).
    #[must_use]
    pub fn new_introspection(stmt: IntrospectionStatement) -> Self {
        Self {
            let_bindings: Vec::new(),
            select: SelectStatement::empty(),
            compound: None,
            match_clause: None,
            dml: None,
            train: None,
            ddl: None,
            introspection: Some(stmt),
            admin: None,
        }
    }

    /// Creates a new admin query (FLUSH).
    #[must_use]
    pub fn new_admin(stmt: AdminStatement) -> Self {
        Self {
            let_bindings: Vec::new(),
            select: SelectStatement::empty(),
            compound: None,
            match_clause: None,
            dml: None,
            train: None,
            ddl: None,
            introspection: None,
            admin: Some(stmt),
        }
    }
}

/// SQL set operator for compound queries (EPIC-040 US-006).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SetOperator {
    /// UNION - merge results, remove duplicates.
    Union,
    /// UNION ALL - merge results, keep duplicates.
    UnionAll,
    /// INTERSECT - keep only common results.
    Intersect,
    /// EXCEPT - subtract second query from first.
    Except,
}

/// Compound query combining queries with set operators (UNION/INTERSECT/EXCEPT).
///
/// Supports N-ary chaining: `SELECT ... UNION SELECT ... INTERSECT SELECT ...`
/// is represented as `operations: [(Union, B), (Intersect, C)]`, applied left-to-right.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompoundQuery {
    /// Chained set operations: `(operator, right_select)` pairs, applied left-to-right.
    pub operations: Vec<(SetOperator, SelectStatement)>,
}

#[cfg(test)]
#[path = "ast_tests.rs"]
mod tests;