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 a value compared directly in this condition (ignoring
301 /// nested logical operators) is a subquery.
302 ///
303 /// Matched exhaustively (like [`Self::has_vector_search`]) so that a future
304 /// value-bearing condition variant fails to compile until it is classified
305 /// here, rather than silently escaping subquery detection.
306 fn leaf_has_subquery(&self) -> bool {
307 match self {
308 Self::Comparison(c) => c.value.is_subquery(),
309 Self::Between(c) => [&c.low, &c.high].into_iter().any(Value::is_subquery),
310 Self::In(c) => c.values.iter().any(Value::is_subquery),
311 Self::Contains(c) => c.values.iter().any(Value::is_subquery),
312 Self::VectorSearch(_)
313 | Self::VectorFusedSearch(_)
314 | Self::SparseVectorSearch(_)
315 | Self::Similarity(_)
316 | Self::Like(_)
317 | Self::IsNull(_)
318 | Self::Match(_)
319 | Self::GraphMatch(_)
320 | Self::ContainsText(_)
321 | Self::GeoDistance(_)
322 | Self::GeoBbox(_)
323 | Self::And(..)
324 | Self::Or(..)
325 | Self::Not(_)
326 | Self::Group(_) => false,
327 }
328 }
329}