Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 96 additions & 35 deletions helix-db/src/helixc/analyzer/methods/traversal_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::helixc::analyzer::error_codes::*;
use crate::helixc::analyzer::utils::{
DEFAULT_VAR_NAME, VariableInfo, check_identifier_is_fieldtype,
};
use crate::helixc::generator::bool_ops::{Contains, IsIn};
use crate::helixc::generator::bool_ops::{Contains, IsIn, PropertyEq, PropertyNeq};
use crate::helixc::generator::source_steps::{SearchVector, VFromID, VFromType};
use crate::helixc::generator::traversal_steps::{AggregateBy, GroupBy};
use crate::helixc::generator::utils::{EmbedData, VecData};
Expand Down Expand Up @@ -94,6 +94,45 @@ fn get_reserved_property_type(prop_name: &str, item_type: &Type) -> Option<Field
}
}

/// Checks if a traversal is a "simple" property access (no graph navigation steps)
/// and returns the variable name and property name if so.
///
/// A simple traversal is one that only accesses properties on an already-bound variable,
/// without any graph navigation (Out, In, etc.). For example: `toUser::{login}`
///
/// Returns: Some((variable_name, property_name)) if simple, None otherwise
fn is_simple_property_traversal(tr: &Traversal) -> Option<(String, String)> {
// Check if the start is an identifier (not a type-based query)
let var_name = match &tr.start {
StartNode::Identifier(id) => id.clone(),
_ => return None,
};

// Check if there's exactly one step and it's an Object (property access)
if tr.steps.len() != 1 {
return None;
}

// Check if the single step is an Object step (property access like {login})
match &tr.steps[0].step {
StepType::Object(obj) => {
// Check if it's a simple property fetch (single field, no spread)
if obj.fields.len() == 1 && !obj.should_spread {
let field = &obj.fields[0];
// Check if it's a simple field selection (Empty or Identifier, not a complex expression)
match &field.value.value {
FieldValueType::Empty | FieldValueType::Identifier(_) => {
return Some((var_name, field.key.clone()));
}
_ => return None,
}
}
None
}
_ => None,
}
}

/// Validates the traversal and returns the end type of the traversal
///
/// This method also builds the generated traversal (`gen_traversal`) as it analyzes the traversal
Expand Down Expand Up @@ -1170,7 +1209,32 @@ pub(crate) fn validate_traversal<'a>(
})
}
BooleanOpType::Equal(expr) => {
let v = match &expr.expr {
// Check if the right-hand side is a simple property traversal
if let ExpressionType::Traversal(traversal) = &expr.expr {
if let Some((var, property)) = is_simple_property_traversal(traversal) {
// Use PropertyEq for simple traversals to avoid unnecessary G::from_iter
BoolOp::PropertyEq(PropertyEq { var, property })
} else {
// Complex traversal - parse normally
let mut gen_traversal = GeneratedTraversal::default();
validate_traversal(
ctx,
traversal,
scope,
original_query,
parent_ty.clone(),
&mut gen_traversal,
gen_query,
);
gen_traversal.should_collect = ShouldCollect::ToValue;
let v = GeneratedValue::Traversal(Box::new(gen_traversal));
BoolOp::Eq(Eq {
left: GeneratedValue::Primitive(GenRef::Std("*v".to_string())),
right: v,
})
}
} else {
let v = match &expr.expr {
ExpressionType::BooleanLiteral(b) => {
GeneratedValue::Primitive(GenRef::Std(b.to_string()))
}
Expand All @@ -1192,8 +1256,24 @@ pub(crate) fn validate_traversal<'a>(
);
gen_identifier_or_param(original_query, i.as_str(), false, true)
}
ExpressionType::Traversal(traversal) => {
// parse traversal
_ => {
unreachable!("Cannot reach here");
}
};
BoolOp::Eq(Eq {
left: GeneratedValue::Primitive(GenRef::Std("*v".to_string())),
right: v,
})
}
}
BooleanOpType::NotEqual(expr) => {
// Check if the right-hand side is a simple property traversal
if let ExpressionType::Traversal(traversal) = &expr.expr {
if let Some((var, property)) = is_simple_property_traversal(traversal) {
// Use PropertyNeq for simple traversals to avoid unnecessary G::from_iter
BoolOp::PropertyNeq(PropertyNeq { var, property })
} else {
// Complex traversal - parse normally
let mut gen_traversal = GeneratedTraversal::default();
validate_traversal(
ctx,
Expand All @@ -1205,19 +1285,14 @@ pub(crate) fn validate_traversal<'a>(
gen_query,
);
gen_traversal.should_collect = ShouldCollect::ToValue;
GeneratedValue::Traversal(Box::new(gen_traversal))
}
_ => {
unreachable!("Cannot reach here");
let v = GeneratedValue::Traversal(Box::new(gen_traversal));
BoolOp::Neq(Neq {
left: GeneratedValue::Primitive(GenRef::Std("*v".to_string())),
right: v,
})
}
};
BoolOp::Eq(Eq {
left: GeneratedValue::Primitive(GenRef::Std("*v".to_string())),
right: v,
})
}
BooleanOpType::NotEqual(expr) => {
let v = match &expr.expr {
} else {
let v = match &expr.expr {
ExpressionType::BooleanLiteral(b) => {
GeneratedValue::Primitive(GenRef::Std(b.to_string()))
}
Expand All @@ -1239,27 +1314,13 @@ pub(crate) fn validate_traversal<'a>(
);
gen_identifier_or_param(original_query, i.as_str(), false, true)
}
ExpressionType::Traversal(traversal) => {
// parse traversal
let mut gen_traversal = GeneratedTraversal::default();
validate_traversal(
ctx,
traversal,
scope,
original_query,
parent_ty.clone(),
&mut gen_traversal,
gen_query,
);
gen_traversal.should_collect = ShouldCollect::ToValue;
GeneratedValue::Traversal(Box::new(gen_traversal))
}
_ => unreachable!("Cannot reach here"),
};
BoolOp::Neq(Neq {
left: GeneratedValue::Primitive(GenRef::Std("*v".to_string())),
right: v,
})
BoolOp::Neq(Neq {
left: GeneratedValue::Primitive(GenRef::Std("*v".to_string())),
right: v,
})
}
}
BooleanOpType::Contains(expr) => {
let v = match &expr.expr {
Expand Down
28 changes: 28 additions & 0 deletions helix-db/src/helixc/generator/bool_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub enum BoolOp {
Neq(Neq),
Contains(Contains),
IsIn(IsIn),
PropertyEq(PropertyEq),
PropertyNeq(PropertyNeq),
}
impl Display for BoolOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand All @@ -30,6 +32,8 @@ impl Display for BoolOp {
BoolOp::Neq(neq) => format!("{neq}"),
BoolOp::Contains(contains) => format!("v{contains}"),
BoolOp::IsIn(is_in) => format!("v{is_in}"),
BoolOp::PropertyEq(prop_eq) => format!("{prop_eq}"),
BoolOp::PropertyNeq(prop_neq) => format!("{prop_neq}"),
};
write!(f, "map_value_or(false, |v| {s})?")
}
Expand Down Expand Up @@ -100,6 +104,28 @@ impl Display for Neq {
}
}

#[derive(Clone, Debug)]
pub struct PropertyEq {
pub var: String,
pub property: String,
}
impl Display for PropertyEq {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.get_property(\"{}\").map_or(false, |w| w == v)", self.var, self.property)
}
}

#[derive(Clone, Debug)]
pub struct PropertyNeq {
pub var: String,
pub property: String,
}
impl Display for PropertyNeq {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.get_property(\"{}\").map_or(false, |w| w != v)", self.var, self.property)
}
}

#[derive(Clone, Debug)]
pub struct Contains {
pub value: GeneratedValue,
Expand Down Expand Up @@ -241,6 +267,8 @@ impl Display for BoExp {
BoolOp::Neq(neq) => format!("{neq}"),
BoolOp::Contains(contains) => format!("v{contains}"),
BoolOp::IsIn(is_in) => format!("v{is_in}"),
BoolOp::PropertyEq(prop_eq) => format!("{prop_eq}"),
BoolOp::PropertyNeq(prop_neq) => format!("{prop_neq}"),
};
return write!(
f,
Expand Down
5 changes: 5 additions & 0 deletions helix-db/src/helixc/generator/traversal_steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,9 @@ impl Display for WhereRef {
BoolOp::Neq(neq) => format!("{} != {}", value_expr, neq.right),
BoolOp::Contains(contains) => format!("{}{}", value_expr, contains),
BoolOp::IsIn(is_in) => format!("{}{}", value_expr, is_in),
BoolOp::PropertyEq(_) | BoolOp::PropertyNeq(_) => {
unreachable!("PropertyEq/PropertyNeq should not be used with reserved properties")
}
};
return write!(
f,
Expand All @@ -501,6 +504,8 @@ impl Display for WhereRef {
BoolOp::Neq(neq) => format!("{neq}"),
BoolOp::Contains(contains) => format!("v{contains}"),
BoolOp::IsIn(is_in) => format!("v{is_in}"),
BoolOp::PropertyEq(prop_eq) => format!("{prop_eq}"),
BoolOp::PropertyNeq(prop_neq) => format!("{prop_neq}"),
};
return write!(
f,
Expand Down
8 changes: 2 additions & 6 deletions hql-tests/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,12 @@ fi
file_name=$1


helix compile --path "/Users/xav/GitHub/helix-db/hql-tests/tests/$file_name" --output "/Users/xav/GitHub/helix-db/helix-container/src"
output=$(cargo check --manifest-path "/Users/xav/GitHub/helix-db/helix-container/Cargo.toml")
helix compile --path "/Users/xav/GitHub/helix-db-core/hql-tests/tests/$file_name" --output "/Users/xav/GitHub/helix-db-core/helix-container/src"
output=$(cargo check --manifest-path "/Users/xav/GitHub/helix-db-core/helix-container/Cargo.toml")
if [ $? -ne 0 ]; then
echo "Error: Cargo check failed"
echo "Cargo check output: $output"
exit 1
fi

echo "Cargo check passed"




9 changes: 9 additions & 0 deletions hql-tests/tests/user_test_2/helix.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
name = "where_filter"
queries = "."

[local.dev]
port = 6969
build_mode = "debug"

[cloud]
Loading
Loading