linkrs_query/planning/statements/seeks/
variable_prop_index_seek.rs1use super::seek_strategy::SeekStrategy;
11use super::seek_strategy_base::{IndexInfo, SeekResult, SeekStrategyContext, SeekStrategyType};
12use crate::storage::StorageReader;
13use linkrs_core::{StorageError, Value};
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct VariablePropertyPredicate {
18 pub property: String,
19 pub op: VariablePredicateOp,
20 pub variable_name: String,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum VariablePredicateOp {
26 Eq, Ne, Lt, Le, Gt, Ge, In, }
34
35impl VariablePredicateOp {
36 pub fn to_predicate_op(&self) -> super::prop_index_seek::PredicateOp {
38 match self {
39 VariablePredicateOp::Eq => super::prop_index_seek::PredicateOp::Eq,
40 VariablePredicateOp::Ne => super::prop_index_seek::PredicateOp::Ne,
41 VariablePredicateOp::Lt => super::prop_index_seek::PredicateOp::Lt,
42 VariablePredicateOp::Le => super::prop_index_seek::PredicateOp::Le,
43 VariablePredicateOp::Gt => super::prop_index_seek::PredicateOp::Gt,
44 VariablePredicateOp::Ge => super::prop_index_seek::PredicateOp::Ge,
45 VariablePredicateOp::In => super::prop_index_seek::PredicateOp::In,
46 }
47 }
48}
49
50impl std::str::FromStr for VariablePredicateOp {
51 type Err = String;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 match s.to_uppercase().as_str() {
55 "=" | "==" => Ok(VariablePredicateOp::Eq),
56 "!=" | "<>" => Ok(VariablePredicateOp::Ne),
57 "<" => Ok(VariablePredicateOp::Lt),
58 "<=" => Ok(VariablePredicateOp::Le),
59 ">" => Ok(VariablePredicateOp::Gt),
60 ">=" => Ok(VariablePredicateOp::Ge),
61 "IN" => Ok(VariablePredicateOp::In),
62 _ => Err(format!("Invalid predicate operator: {}", s)),
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct VariablePropIndexSeek {
70 predicates: Vec<VariablePropertyPredicate>,
71 variable_values: std::collections::HashMap<String, Value>,
72}
73
74impl VariablePropIndexSeek {
75 pub fn new(predicates: Vec<VariablePropertyPredicate>) -> Self {
76 Self {
77 predicates,
78 variable_values: std::collections::HashMap::new(),
79 }
80 }
81
82 pub fn bind_variable(&mut self, name: &str, value: Value) {
84 self.variable_values.insert(name.to_string(), value);
85 }
86
87 pub fn bind_variables(&mut self, values: std::collections::HashMap<String, Value>) {
89 self.variable_values.extend(values);
90 }
91
92 pub fn all_variables_bound(&self) -> bool {
94 self.predicates
95 .iter()
96 .all(|pred| self.variable_values.contains_key(&pred.variable_name))
97 }
98
99 pub fn extract_predicates(
101 expressions: &[linkrs_core::Expression],
102 ) -> Vec<VariablePropertyPredicate> {
103 let mut predicates = Vec::new();
104
105 for expr in expressions {
106 if let Some(pred) = Self::extract_predicate(expr) {
107 predicates.push(pred);
108 }
109 }
110
111 predicates
112 }
113
114 fn extract_predicate(expr: &linkrs_core::Expression) -> Option<VariablePropertyPredicate> {
116 use linkrs_core::types::operators::BinaryOperator;
117
118 match expr {
119 linkrs_core::Expression::Binary { op, left, right } => {
120 let op_str = match op {
121 BinaryOperator::Equal => "=",
122 BinaryOperator::NotEqual => "!=",
123 BinaryOperator::LessThan => "<",
124 BinaryOperator::LessThanOrEqual => "<=",
125 BinaryOperator::GreaterThan => ">",
126 BinaryOperator::GreaterThanOrEqual => ">=",
127 _ => return None,
128 };
129
130 if let (Some(prop), Some(var_name)) =
132 (Self::extract_property(left), Self::extract_variable(right))
133 {
134 if let Ok(pred_op) = op_str.parse::<VariablePredicateOp>() {
135 return Some(VariablePropertyPredicate {
136 property: prop,
137 op: pred_op,
138 variable_name: var_name,
139 });
140 }
141 }
142
143 if let (Some(prop), Some(var_name)) =
145 (Self::extract_property(right), Self::extract_variable(left))
146 {
147 let swapped_op = match op_str {
148 "<" => VariablePredicateOp::Gt,
149 "<=" => VariablePredicateOp::Ge,
150 ">" => VariablePredicateOp::Lt,
151 ">=" => VariablePredicateOp::Le,
152 _ => op_str.parse::<VariablePredicateOp>().ok()?,
153 };
154 return Some(VariablePropertyPredicate {
155 property: prop,
156 op: swapped_op,
157 variable_name: var_name,
158 });
159 }
160
161 None
162 }
163 _ => None,
164 }
165 }
166
167 fn extract_property(expr: &linkrs_core::Expression) -> Option<String> {
169 match expr {
170 linkrs_core::Expression::Property { object, property } => {
171 if matches!(object.as_ref(), linkrs_core::Expression::Variable(_)) {
173 Some(property.clone())
174 } else {
175 None
176 }
177 }
178 _ => None,
179 }
180 }
181
182 fn extract_variable(expr: &linkrs_core::Expression) -> Option<String> {
184 match expr {
185 linkrs_core::Expression::Variable(name) if name.starts_with('$') => {
187 Some(name[1..].to_string())
188 }
189 _ => None,
190 }
191 }
192
193 fn find_best_index<'a>(
195 &'a self,
196 context: &'a SeekStrategyContext,
197 ) -> Option<(&'a IndexInfo, &'a VariablePropertyPredicate)> {
198 for pred in &self.predicates {
199 if let Some(index) = context.get_index_for_property(&pred.property) {
200 return Some((index, pred));
201 }
202 }
203 None
204 }
205
206 fn value_matches(&self, value: &Value, pred: &VariablePropertyPredicate) -> bool {
208 let var_value = match self.variable_values.get(&pred.variable_name) {
210 Some(v) => v,
211 None => return false, };
213
214 match pred.op {
215 VariablePredicateOp::Eq => value == var_value,
216 VariablePredicateOp::Ne => value != var_value,
217 VariablePredicateOp::Lt => Self::compare_values(value, var_value)
218 .map(|c| c < 0)
219 .unwrap_or(false),
220 VariablePredicateOp::Le => Self::compare_values(value, var_value)
221 .map(|c| c <= 0)
222 .unwrap_or(false),
223 VariablePredicateOp::Gt => Self::compare_values(value, var_value)
224 .map(|c| c > 0)
225 .unwrap_or(false),
226 VariablePredicateOp::Ge => Self::compare_values(value, var_value)
227 .map(|c| c >= 0)
228 .unwrap_or(false),
229 VariablePredicateOp::In => {
230 matches!(var_value, Value::List(list) if list.contains(value))
232 }
233 }
234 }
235
236 fn compare_values(left: &Value, right: &Value) -> Option<i32> {
238 match (left, right) {
239 (Value::SmallInt(i1), Value::SmallInt(i2)) => Some(i1.cmp(i2) as i32),
240 (Value::Int(i1), Value::Int(i2)) => Some(i1.cmp(i2) as i32),
241 (Value::BigInt(i1), Value::BigInt(i2)) => Some(i1.cmp(i2) as i32),
242 (Value::Float(f1), Value::Float(f2)) => f1.partial_cmp(f2).map(|c| c as i32),
243 (Value::Double(f1), Value::Double(f2)) => f1.partial_cmp(f2).map(|c| c as i32),
244 (Value::String(s1), Value::String(s2)) => Some(s1.cmp(s2) as i32),
245 _ => None,
246 }
247 }
248}
249
250fn has_distinct_multi_labels(labels: &[String]) -> bool {
251 labels
252 .iter()
253 .collect::<std::collections::HashSet<_>>()
254 .len()
255 > 1
256}
257
258fn vertex_matches_single_label(
259 vertex: &linkrs_core::Vertex,
260 pattern: &super::seek_strategy_base::NodePattern,
261) -> bool {
262 if pattern.labels.is_empty() {
263 return true;
264 }
265 pattern.labels.iter().all(|label| vertex.tag.name == *label)
266}
267
268impl SeekStrategy for VariablePropIndexSeek {
269 fn execute<S: StorageReader>(
270 &self,
271 storage: &S,
272 context: &SeekStrategyContext,
273 ) -> Result<SeekResult, StorageError> {
274 if !self.all_variables_bound() {
276 return Err(StorageError::invalid_input(
277 "Variable property lookups require all variables to be bound".to_string(),
278 ));
279 }
280
281 let mut vertex_ids = Vec::new();
282 let mut rows_scanned = 0;
283
284 if has_distinct_multi_labels(&context.node_pattern.labels) {
285 return Ok(SeekResult {
286 vertex_ids,
287 strategy_used: SeekStrategyType::VariablePropIndexSeek,
288 rows_scanned,
289 });
290 }
291
292 if let Some((index_info, primary_pred)) = self.find_best_index(context) {
294 let space_name = "default"; let vertices = storage.scan_vertices_by_tag(space_name, &index_info.target_name)?;
297 rows_scanned = vertices.len();
298
299 for vertex in vertices {
301 if !vertex_matches_single_label(&vertex, &context.node_pattern) {
302 continue;
303 }
304 let mut matches_all = true;
305
306 if let Some(prop_value) = vertex.property_value(&primary_pred.property) {
308 if !self.value_matches(&prop_value, primary_pred) {
309 matches_all = false;
310 }
311 } else {
312 matches_all = false;
313 }
314
315 if matches_all {
317 for pred in &self.predicates {
318 if pred.property != primary_pred.property {
319 if let Some(prop_value) = vertex.property_value(&pred.property) {
320 if !self.value_matches(&prop_value, pred) {
321 matches_all = false;
322 break;
323 }
324 } else {
325 matches_all = false;
326 break;
327 }
328 }
329 }
330 }
331
332 if matches_all {
333 vertex_ids.push(Value::from(*vertex.vid()));
334 }
335 }
336 }
337
338 Ok(SeekResult {
339 vertex_ids,
340 strategy_used: SeekStrategyType::VariablePropIndexSeek,
341 rows_scanned,
342 })
343 }
344
345 fn supports(&self, _context: &SeekStrategyContext) -> bool {
346 !self.predicates.is_empty()
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use linkrs_core::Expression;
355
356 #[test]
357 fn test_variable_predicate_op_from_str() {
358 assert_eq!(
359 "=".parse::<VariablePredicateOp>().ok(),
360 Some(VariablePredicateOp::Eq)
361 );
362 assert_eq!(
363 "<".parse::<VariablePredicateOp>().ok(),
364 Some(VariablePredicateOp::Lt)
365 );
366 assert_eq!(
367 ">=".parse::<VariablePredicateOp>().ok(),
368 Some(VariablePredicateOp::Ge)
369 );
370 assert_eq!(
371 "IN".parse::<VariablePredicateOp>().ok(),
372 Some(VariablePredicateOp::In)
373 );
374 assert!("unknown".parse::<VariablePredicateOp>().is_err());
375 }
376
377 #[test]
378 fn test_extract_variable_predicate() {
379 let expr = Expression::binary(
380 Expression::property(Expression::variable("v"), "name"),
381 linkrs_core::BinaryOperator::Equal,
382 Expression::variable("$varName"),
383 );
384
385 let pred = VariablePropIndexSeek::extract_predicate(&expr);
386 assert!(pred.is_some());
387
388 let pred = pred.expect("Failed to extract predicate");
389 assert_eq!(pred.property, "name");
390 assert_eq!(pred.op, VariablePredicateOp::Eq);
391 assert_eq!(pred.variable_name, "varName");
392 }
393
394 #[test]
395 fn test_variable_binding() {
396 let mut seek = VariablePropIndexSeek::new(vec![VariablePropertyPredicate {
397 property: "age".to_string(),
398 op: VariablePredicateOp::Gt,
399 variable_name: "minAge".to_string(),
400 }]);
401
402 assert!(!seek.all_variables_bound());
403
404 seek.bind_variable("minAge", Value::Int(18));
405
406 assert!(seek.all_variables_bound());
407 }
408}