From 0dfbcd2d54a4a7c052bd20bf251f7fe5b750591b Mon Sep 17 00:00:00 2001 From: yuou lei Date: Sun, 9 Nov 2025 16:42:18 -0800 Subject: [PATCH 1/4] fixup --- rust/lance-graph/src/ast.rs | 4 ++ rust/lance-graph/src/parser.rs | 71 ++++++++++++++++++++++++++++++++ rust/lance-graph/src/semantic.rs | 6 +++ 3 files changed, 81 insertions(+) diff --git a/rust/lance-graph/src/ast.rs b/rust/lance-graph/src/ast.rs index ff795c30..972105bd 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..feab0b04 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,28 @@ fn return_item(input: &str) -> IResult<&str, ReturnItem> { )) } +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, ())) +} + +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 +858,47 @@ 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), + } + } + + 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(()) } From b8d0832504acefac07be5e8cf7ebe36ba0432746 Mon Sep 17 00:00:00 2001 From: yuou lei Date: Sun, 9 Nov 2025 16:47:17 -0800 Subject: [PATCH 2/4] add comment --- rust/lance-graph/src/parser.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/lance-graph/src/parser.rs b/rust/lance-graph/src/parser.rs index feab0b04..774e92e8 100644 --- a/rust/lance-graph/src/parser.rs +++ b/rust/lance-graph/src/parser.rs @@ -484,6 +484,7 @@ 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)?; @@ -494,6 +495,7 @@ fn is_null_comparison(input: &str) -> IResult<&str, ()> { 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)?; From c15f6226b79cccb1abcac626ca574c09a15fae09 Mon Sep 17 00:00:00 2001 From: yuou lei Date: Sun, 9 Nov 2025 19:35:40 -0800 Subject: [PATCH 3/4] fmt --- rust/lance-graph/src/ast.rs | 4 ++-- rust/lance-graph/src/parser.rs | 28 ++++++++++++---------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/rust/lance-graph/src/ast.rs b/rust/lance-graph/src/ast.rs index 972105bd..fa1c8cec 100644 --- a/rust/lance-graph/src/ast.rs +++ b/rust/lance-graph/src/ast.rs @@ -224,9 +224,9 @@ pub enum BooleanExpression { pattern: String, }, /// IS NULL pattern matching - IsNull (ValueExpression), + IsNull(ValueExpression), /// IS NOT NULL pattern matching - IsNotNull (ValueExpression), + IsNotNull(ValueExpression), } /// Comparison operators diff --git a/rust/lance-graph/src/parser.rs b/rust/lance-graph/src/parser.rs index 774e92e8..4fe00c46 100644 --- a/rust/lance-graph/src/parser.rs +++ b/rust/lance-graph/src/parser.rs @@ -868,15 +868,13 @@ mod tests { 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"), + 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), } } @@ -888,15 +886,13 @@ mod tests { 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"), + 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), } } From f9727011a73eefc529065511aaab4cec77f134c2 Mon Sep 17 00:00:00 2001 From: leiyuou Date: Sun, 9 Nov 2025 20:31:27 -0800 Subject: [PATCH 4/4] fixup --- rust/lance-graph/src/parser.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/lance-graph/src/parser.rs b/rust/lance-graph/src/parser.rs index 4fe00c46..4d0f33ec 100644 --- a/rust/lance-graph/src/parser.rs +++ b/rust/lance-graph/src/parser.rs @@ -879,6 +879,7 @@ mod tests { } } + #[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();