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
7 changes: 6 additions & 1 deletion rust/lance-graph/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,16 @@ pub enum BooleanExpression {
expression: ValueExpression,
list: Vec<ValueExpression>,
},
/// LIKE pattern matching
/// LIKE pattern matching (case-sensitive)
Like {
expression: ValueExpression,
pattern: String,
},
/// ILIKE pattern matching (case-insensitive)
ILike {
expression: ValueExpression,
pattern: String,
},
/// CONTAINS substring matching
Contains {
expression: ValueExpression,
Expand Down
83 changes: 83 additions & 0 deletions rust/lance-graph/src/datafusion_planner/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ pub(crate) fn to_df_boolean_expr(expr: &BooleanExpression) -> Expr {
expression,
pattern,
} => create_like_expr(expression, pattern, false),
BE::ILike {
expression,
pattern,
} => create_like_expr(expression, pattern, true),
BE::Contains {
expression,
substring,
Expand Down Expand Up @@ -476,6 +480,85 @@ mod tests {
}
}

#[test]
fn test_boolean_expr_ilike() {
let expr = BooleanExpression::ILike {
expression: ValueExpression::Property(PropertyRef {
variable: "p".into(),
property: "name".into(),
}),
pattern: "alice%".into(),
};

if let Expr::Like(like_expr) = to_df_boolean_expr(&expr) {
assert!(!like_expr.negated, "Should not be negated");
assert!(
like_expr.case_insensitive,
"ILIKE should be case insensitive"
);
assert_eq!(like_expr.escape_char, None, "Should have no escape char");
match *like_expr.expr {
Expr::Column(ref col_expr) => {
assert_eq!(col_expr.name(), "p__name");
}
other => panic!("Expected column expression, got {:?}", other),
}
// Check pattern is a literal
match *like_expr.pattern {
Expr::Literal(ref scalar, _) => {
let s = format!("{:?}", scalar);
assert!(
s.contains("alice%"),
"Pattern should be 'alice%', got: {}",
s
);
}
other => panic!("Expected literal pattern, got {:?}", other),
}
} else {
panic!("Expected Like expression");
}
}

#[test]
fn test_boolean_expr_like_vs_ilike_case_sensitivity() {
// Test LIKE (case-sensitive)
let like_expr = BooleanExpression::Like {
expression: ValueExpression::Property(PropertyRef {
variable: "p".into(),
property: "name".into(),
}),
pattern: "Test%".into(),
};

if let Expr::Like(like) = to_df_boolean_expr(&like_expr) {
assert!(
!like.case_insensitive,
"LIKE should be case-sensitive (case_insensitive = false)"
);
} else {
panic!("Expected Like expression");
}

// Test ILIKE (case-insensitive)
let ilike_expr = BooleanExpression::ILike {
expression: ValueExpression::Property(PropertyRef {
variable: "p".into(),
property: "name".into(),
}),
pattern: "Test%".into(),
};

if let Expr::Like(ilike) = to_df_boolean_expr(&ilike_expr) {
assert!(
ilike.case_insensitive,
"ILIKE should be case-insensitive (case_insensitive = true)"
);
} else {
panic!("Expected Like expression");
}
}

#[test]
fn test_boolean_expr_like_with_wildcard() {
let expr = BooleanExpression::Like {
Expand Down
70 changes: 70 additions & 0 deletions rust/lance-graph/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,18 @@ fn comparison_expression(input: &str) -> IResult<&str, BooleanExpression> {
},
));
}
// Match ILIKE pattern (case-insensitive LIKE)
if let Ok((input_after_ilike, (_, _, pattern))) =
tuple((tag_no_case("ILIKE"), multispace0, string_literal))(input)
{
return Ok((
input_after_ilike,
BooleanExpression::ILike {
expression: left,
pattern,
},
));
}
// Match CONTAINS substring
if let Ok((input_after_contains, (_, _, substring))) =
tuple((tag_no_case("CONTAINS"), multispace0, string_literal))(input)
Expand Down Expand Up @@ -1265,4 +1277,62 @@ mod tests {
_ => panic!("Expected AND expression"),
}
}

#[test]
fn test_parse_ilike_pattern() {
let query = "MATCH (n:Person) WHERE n.name ILIKE 'alice%' RETURN n.name";
let result = parse_cypher_query(query);
assert!(result.is_ok(), "ILIKE pattern should parse successfully");

let ast = result.unwrap();
let where_clause = ast.where_clause.expect("Expected WHERE clause");

match where_clause.expression {
BooleanExpression::ILike {
expression,
pattern,
} => {
match expression {
ValueExpression::Property(prop) => {
assert_eq!(prop.variable, "n");
assert_eq!(prop.property, "name");
}
_ => panic!("Expected property expression"),
}
assert_eq!(pattern, "alice%");
}
_ => panic!("Expected ILIKE expression"),
}
}

#[test]
fn test_parse_like_and_ilike_together() {
let query =
"MATCH (n:Person) WHERE n.name LIKE 'Alice%' OR n.name ILIKE 'bob%' RETURN n.name";
let result = parse_cypher_query(query);
assert!(result.is_ok(), "LIKE and ILIKE together should parse");

let ast = result.unwrap();
let where_clause = ast.where_clause.expect("Expected WHERE clause");

match where_clause.expression {
BooleanExpression::Or(left, right) => {
// Left should be LIKE (case-sensitive)
match *left {
BooleanExpression::Like { pattern, .. } => {
assert_eq!(pattern, "Alice%");
}
_ => panic!("Expected LIKE expression on left"),
}
// Right should be ILIKE (case-insensitive)
match *right {
BooleanExpression::ILike { pattern, .. } => {
assert_eq!(pattern, "bob%");
}
_ => panic!("Expected ILIKE expression on right"),
}
}
_ => panic!("Expected OR expression"),
}
}
}
3 changes: 3 additions & 0 deletions rust/lance-graph/src/semantic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ impl SemanticAnalyzer {
BooleanExpression::Like { expression, .. } => {
self.analyze_value_expression(expression)?;
}
BooleanExpression::ILike { expression, .. } => {
self.analyze_value_expression(expression)?;
}
BooleanExpression::Contains { expression, .. } => {
self.analyze_value_expression(expression)?;
}
Expand Down
Loading