Skip to main content

velesdb_core/velesql/ast/
condition.rs

1//! WHERE clause condition types for VelesQL.
2//!
3//! This module defines all condition types used in WHERE clauses,
4//! including vector search, comparisons, and logical operators.
5
6use serde::{Deserialize, Serialize};
7
8use super::fusion::FusionConfig;
9use super::values::{Value, VectorExpr};
10use crate::sparse_index::SparseVector;
11use crate::velesql::GraphPattern;
12
13/// A condition in a WHERE clause.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[non_exhaustive]
16pub enum Condition {
17    /// Vector similarity search: `vector NEAR [metric] $param`
18    VectorSearch(VectorSearch),
19    /// Multi-vector fused search: `vector NEAR_FUSED [$v1, $v2] USING FUSION 'rrf'`
20    VectorFusedSearch(VectorFusedSearch),
21    /// Sparse vector search: `vector SPARSE_NEAR $sv [USING 'index-name']`
22    SparseVectorSearch(SparseVectorSearch),
23    /// Similarity function: `similarity(field, $vector) > threshold`
24    Similarity(SimilarityCondition),
25    /// Comparison: column op value
26    Comparison(Comparison),
27    /// IN operator: column IN (values)
28    In(InCondition),
29    /// BETWEEN operator: column BETWEEN a AND b
30    Between(BetweenCondition),
31    /// LIKE operator: column LIKE pattern
32    Like(LikeCondition),
33    /// IS NULL / IS NOT NULL
34    IsNull(IsNullCondition),
35    /// Full-text search: column MATCH 'query'
36    Match(MatchCondition),
37    /// Graph match predicate inside WHERE: `MATCH (a)-[:REL]->(b)`
38    GraphMatch(GraphMatchPredicate),
39    /// Array containment: `column CONTAINS value` / `CONTAINS ANY` / `CONTAINS ALL`
40    Contains(ContainsCondition),
41    /// Strict text substring filter: `column CONTAINS_TEXT 'query'`
42    ContainsText(ContainsTextCondition),
43    /// Geospatial distance: `GEO_DISTANCE(column, lat, lng) op threshold`
44    GeoDistance(GeoDistanceCondition),
45    /// Geospatial bounding box: `GEO_BBOX(column, lat_min, lng_min, lat_max, lng_max)`
46    GeoBbox(GeoBboxCondition),
47    /// Logical AND
48    And(Box<Condition>, Box<Condition>),
49    /// Logical OR
50    Or(Box<Condition>, Box<Condition>),
51    /// Logical NOT
52    Not(Box<Condition>),
53    /// Grouped condition (parentheses)
54    Group(Box<Condition>),
55}
56
57/// Graph predicate condition used in SELECT WHERE clauses.
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct GraphMatchPredicate {
60    /// Graph pattern to evaluate.
61    pub pattern: GraphPattern,
62}
63
64/// Vector similarity search condition.
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct VectorSearch {
67    /// Vector expression (literal or parameter).
68    pub vector: VectorExpr,
69}
70
71/// Multi-vector fused search condition.
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct VectorFusedSearch {
74    /// List of vector expressions (literals or parameters).
75    pub vectors: Vec<VectorExpr>,
76    /// Fusion strategy configuration.
77    pub fusion: FusionConfig,
78}
79
80/// Sparse vector search condition.
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct SparseVectorSearch {
83    /// Sparse vector expression (literal or parameter).
84    pub vector: SparseVectorExpr,
85    /// Optional named sparse index (from USING clause).
86    pub index_name: Option<String>,
87}
88
89/// Expression representing a sparse vector value in a query.
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91#[non_exhaustive]
92pub enum SparseVectorExpr {
93    /// Inline sparse literal: `{12: 0.8, 45: 0.3}`
94    Literal(SparseVector),
95    /// Bind parameter: `$sv`
96    Parameter(String),
97}
98
99/// Similarity function condition: `similarity(field, vector) op threshold`
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub struct SimilarityCondition {
102    /// Field name containing the embedding.
103    pub field: String,
104    /// Vector to compare against.
105    pub vector: VectorExpr,
106    /// Comparison operator.
107    pub operator: CompareOp,
108    /// Similarity threshold.
109    pub threshold: f64,
110}
111
112/// Comparison condition.
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114pub struct Comparison {
115    /// Column name.
116    pub column: String,
117    /// Comparison operator.
118    pub operator: CompareOp,
119    /// Value to compare against.
120    pub value: Value,
121}
122
123/// Comparison operators.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125#[non_exhaustive]
126pub enum CompareOp {
127    /// Equal (=)
128    Eq,
129    /// Not equal (!= or <>)
130    NotEq,
131    /// Greater than (>)
132    Gt,
133    /// Greater than or equal (>=)
134    Gte,
135    /// Less than (<)
136    Lt,
137    /// Less than or equal (<=)
138    Lte,
139}
140
141/// IN / NOT IN condition: `column [NOT] IN (value1, value2, ...)`
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143pub struct InCondition {
144    /// Column name.
145    pub column: String,
146    /// List of values.
147    pub values: Vec<Value>,
148    /// `true` when this is a `NOT IN` condition.
149    #[serde(default)]
150    pub negated: bool,
151}
152
153/// BETWEEN condition: column BETWEEN low AND high
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct BetweenCondition {
156    /// Column name.
157    pub column: String,
158    /// Low value.
159    pub low: Value,
160    /// High value.
161    pub high: Value,
162}
163
164/// LIKE/ILIKE condition.
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub struct LikeCondition {
167    /// Column name.
168    pub column: String,
169    /// Pattern (with % and _ wildcards).
170    pub pattern: String,
171    /// True for ILIKE (case-insensitive).
172    #[serde(default)]
173    pub case_insensitive: bool,
174}
175
176/// IS NULL condition.
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178pub struct IsNullCondition {
179    /// Column name.
180    pub column: String,
181    /// True for IS NULL, false for IS NOT NULL.
182    pub is_null: bool,
183}
184
185/// MATCH condition for full-text search.
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct MatchCondition {
188    /// Column name.
189    pub column: String,
190    /// Search query.
191    pub query: String,
192}
193
194/// Strict text substring filter: `column CONTAINS_TEXT 'query'`
195///
196/// Unlike [`MatchCondition`] (RRF-boosted text search), this performs
197/// exact case-sensitive substring matching as a metadata filter.
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub struct ContainsTextCondition {
200    /// Column name to search within.
201    pub column: String,
202    /// Substring to search for.
203    pub query: String,
204}
205
206/// Containment mode for array CONTAINS operations.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208#[non_exhaustive]
209pub enum ContainsMode {
210    /// Single value: `column CONTAINS value`
211    Single,
212    /// At least one: `column CONTAINS ANY (v1, v2, ...)`
213    Any,
214    /// All values: `column CONTAINS ALL (v1, v2, ...)`
215    All,
216}
217
218/// CONTAINS condition for array columns.
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
220pub struct ContainsCondition {
221    /// Column name.
222    pub column: String,
223    /// Containment mode.
224    pub mode: ContainsMode,
225    /// Values to check for containment.
226    pub values: Vec<Value>,
227}
228
229/// GEO_DISTANCE condition: `GEO_DISTANCE(column, lat, lng) op threshold`
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
231pub struct GeoDistanceCondition {
232    /// Column name containing GeoPoint data.
233    pub column: String,
234    /// Reference latitude in degrees.
235    pub lat: f64,
236    /// Reference longitude in degrees.
237    pub lng: f64,
238    /// Comparison operator.
239    pub operator: CompareOp,
240    /// Distance threshold in meters.
241    pub threshold: f64,
242}
243
244/// GEO_BBOX condition: `GEO_BBOX(column, lat_min, lng_min, lat_max, lng_max)`
245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
246pub struct GeoBboxCondition {
247    /// Column name containing GeoPoint data.
248    pub column: String,
249    /// Minimum latitude of the bounding box.
250    pub lat_min: f64,
251    /// Minimum longitude of the bounding box.
252    pub lng_min: f64,
253    /// Maximum latitude of the bounding box.
254    pub lat_max: f64,
255    /// Maximum longitude of the bounding box.
256    pub lng_max: f64,
257}
258
259impl Condition {
260    /// Returns `true` if this condition (or any nested sub-condition) contains
261    /// a vector search (`NEAR`, `NEAR_FUSED`, or `SPARSE_NEAR`).
262    #[must_use]
263    pub fn has_vector_search(&self) -> bool {
264        match self {
265            Self::VectorSearch(_) | Self::VectorFusedSearch(_) | Self::SparseVectorSearch(_) => {
266                true
267            }
268            Self::And(l, r) | Self::Or(l, r) => l.has_vector_search() || r.has_vector_search(),
269            Self::Group(inner) | Self::Not(inner) => inner.has_vector_search(),
270            Self::Contains(_)
271            | Self::ContainsText(_)
272            | Self::Comparison(_)
273            | Self::In(_)
274            | Self::Between(_)
275            | Self::Like(_)
276            | Self::IsNull(_)
277            | Self::Match(_)
278            | Self::GraphMatch(_)
279            | Self::Similarity(_)
280            | Self::GeoDistance(_)
281            | Self::GeoBbox(_) => false,
282        }
283    }
284
285    /// Returns `true` if this condition (or any nested sub-condition) compares
286    /// against a subquery value.
287    ///
288    /// Subqueries parse but are not yet executed; left unchecked they silently
289    /// evaluate to `NULL`, producing wrong or empty results.
290    #[must_use]
291    pub fn has_subquery(&self) -> bool {
292        self.leaf_has_subquery()
293            || match self {
294                Self::And(l, r) | Self::Or(l, r) => l.has_subquery() || r.has_subquery(),
295                Self::Group(inner) | Self::Not(inner) => inner.has_subquery(),
296                _ => false,
297            }
298    }
299
300    /// Returns `true` if this condition (or any nested sub-condition) compares
301    /// against a subquery that is **genuinely correlated** against `outer_tables`
302    /// (its inner WHERE references one of the outer query's tables/aliases).
303    ///
304    /// Scalar (non-correlated) subqueries — including those whose inner WHERE
305    /// merely filters on a payload path like `meta.cat` — are executable and
306    /// resolved before the outer query runs; correlated ones are rejected by
307    /// validation.
308    #[must_use]
309    pub fn has_correlated_subquery(&self, outer_tables: &[&str]) -> bool {
310        self.leaf_has_correlated_subquery(outer_tables)
311            || match self {
312                Self::And(l, r) | Self::Or(l, r) => {
313                    l.has_correlated_subquery(outer_tables)
314                        || r.has_correlated_subquery(outer_tables)
315                }
316                Self::Group(inner) | Self::Not(inner) => {
317                    inner.has_correlated_subquery(outer_tables)
318                }
319                _ => false,
320            }
321    }
322
323    /// Returns `true` if a value compared directly in this condition is a
324    /// subquery correlated against `outer_tables` (no nested logical operators).
325    fn leaf_has_correlated_subquery(&self, outer_tables: &[&str]) -> bool {
326        let is_corr = |v: &Value| v.is_correlated_subquery_with(outer_tables);
327        match self {
328            Self::Comparison(c) => is_corr(&c.value),
329            Self::Between(c) => [&c.low, &c.high].into_iter().any(&is_corr),
330            Self::In(c) => c.values.iter().any(&is_corr),
331            Self::Contains(c) => c.values.iter().any(&is_corr),
332            _ => false,
333        }
334    }
335
336    /// Returns `true` if a value compared directly in this condition (ignoring
337    /// nested logical operators) is a subquery.
338    ///
339    /// Matched exhaustively (like [`Self::has_vector_search`]) so that a future
340    /// value-bearing condition variant fails to compile until it is classified
341    /// here, rather than silently escaping subquery detection.
342    fn leaf_has_subquery(&self) -> bool {
343        match self {
344            Self::Comparison(c) => c.value.is_subquery(),
345            Self::Between(c) => [&c.low, &c.high].into_iter().any(Value::is_subquery),
346            Self::In(c) => c.values.iter().any(Value::is_subquery),
347            Self::Contains(c) => c.values.iter().any(Value::is_subquery),
348            Self::VectorSearch(_)
349            | Self::VectorFusedSearch(_)
350            | Self::SparseVectorSearch(_)
351            | Self::Similarity(_)
352            | Self::Like(_)
353            | Self::IsNull(_)
354            | Self::Match(_)
355            | Self::GraphMatch(_)
356            | Self::ContainsText(_)
357            | Self::GeoDistance(_)
358            | Self::GeoBbox(_)
359            | Self::And(..)
360            | Self::Or(..)
361            | Self::Not(_)
362            | Self::Group(_) => false,
363        }
364    }
365}