diff --git a/rust/lance-graph/src/ast.rs b/rust/lance-graph/src/ast.rs index ff795c30..fa1c8cec 100644 --- a/rust/lance-graph/src/ast.rs +++ b/rust/lance-graph/src/ast.rs @@ -223,6 +223,10 @@ pub enum BooleanExpression { expression: ValueExpression, pattern: String, }, + /// IS NULL pattern matching + IsNull(ValueExpression), + /// IS NOT NULL pattern matching + IsNotNull(ValueExpression), } /// Comparison operators diff --git a/rust/lance-graph/src/parser.rs b/rust/lance-graph/src/parser.rs index 813b4578..4d0f33ec 100644 --- a/rust/lance-graph/src/parser.rs +++ b/rust/lance-graph/src/parser.rs @@ -329,6 +329,14 @@ fn comparison_expression(input: &str) -> IResult<&str, BooleanExpression> { }, )); } + // Match is null + if let Ok((rest, ())) = is_null_comparison(input) { + return Ok((rest, BooleanExpression::IsNull(left_clone))); + } + // Match is not null + if let Ok((rest, ())) = is_not_null_comparison(input) { + return Ok((rest, BooleanExpression::IsNotNull(left_clone))); + } let (input, operator) = comparison_operator(input)?; let (input, _) = multispace0(input)?; @@ -476,6 +484,30 @@ fn return_item(input: &str) -> IResult<&str, ReturnItem> { )) } +// Match IS NULL in WHERE clause +fn is_null_comparison(input: &str) -> IResult<&str, ()> { + let (input, _) = multispace0(input)?; + let (input, _) = tag_no_case("IS")(input)?; + let (input, _) = multispace1(input)?; + let (input, _) = tag_no_case("NULL")(input)?; + let (input, _) = multispace0(input)?; + + Ok((input, ())) +} + +// Match IS NOT NULL in WHERE clause +fn is_not_null_comparison(input: &str) -> IResult<&str, ()> { + let (input, _) = multispace0(input)?; + let (input, _) = tag_no_case("IS")(input)?; + let (input, _) = multispace1(input)?; + let (input, _) = tag_no_case("NOT")(input)?; + let (input, _) = multispace1(input)?; + let (input, _) = tag_no_case("NULL")(input)?; + let (input, _) = multispace0(input)?; + + Ok((input, ())) +} + // Parse an ORDER BY clause fn order_by_clause(input: &str) -> IResult<&str, OrderByClause> { let (input, _) = multispace0(input)?; @@ -828,6 +860,44 @@ mod tests { } } + #[test] + fn test_parse_query_with_is_null() { + let query = "MATCH (n:Person) WHERE n.age IS NULL RETURN n.name"; + let result = parse_cypher_query(query).unwrap(); + + let where_clause = result.where_clause.expect("Expected WHERE clause"); + + match where_clause.expression { + BooleanExpression::IsNull(expr) => match expr { + ValueExpression::Property(prop_ref) => { + assert_eq!(prop_ref.variable, "n"); + assert_eq!(prop_ref.property, "age"); + } + _ => panic!("Expected property reference in IS NULL expression"), + }, + other => panic!("Expected IS NULL expression, got {:?}", other), + } + } + + #[test] + fn test_parse_query_with_is_not_null() { + let query = "MATCH (n:Person) WHERE n.age IS NOT NULL RETURN n.name"; + let result = parse_cypher_query(query).unwrap(); + + let where_clause = result.where_clause.expect("Expected WHERE clause"); + + match where_clause.expression { + BooleanExpression::IsNotNull(expr) => match expr { + ValueExpression::Property(prop_ref) => { + assert_eq!(prop_ref.variable, "n"); + assert_eq!(prop_ref.property, "age"); + } + _ => panic!("Expected property reference in IS NOT NULL expression"), + }, + other => panic!("Expected IS NOT NULL expression, got {:?}", other), + } + } + #[test] fn test_parse_query_with_limit() { let query = "MATCH (n:Person) RETURN n.name LIMIT 10"; diff --git a/rust/lance-graph/src/semantic.rs b/rust/lance-graph/src/semantic.rs index 10082042..b613fa69 100644 --- a/rust/lance-graph/src/semantic.rs +++ b/rust/lance-graph/src/semantic.rs @@ -247,6 +247,12 @@ impl SemanticAnalyzer { BooleanExpression::Like { expression, .. } => { self.analyze_value_expression(expression)?; } + BooleanExpression::IsNull(expression) => { + self.analyze_value_expression(expression)?; + } + BooleanExpression::IsNotNull(expression) => { + self.analyze_value_expression(expression)?; + } } Ok(()) }