Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(interactive): support isNull expression evaluation in GIE #3128

Merged
merged 14 commits into from
Aug 22, 2023
Merged
9 changes: 9 additions & 0 deletions docs/interactive_engine/tinkerpop/supported_gremlin_steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ Expression(s) in project or filter:
g.V().where(expr("@.name == \"marko\"")) # = g.V().has("name", "marko")
g.V().where(expr("@.age > 10")) # = g.V().has("age", P.gt(10))
g.V().as("a").out().where(expr("@.name == \"marko\" || (@a.age > 10)"))
g.V().where(expr("@.age isNull"))
```
* project: select(expr("..."))
```bash
Expand All @@ -715,6 +716,14 @@ gremlin> g.V().as("a").where(expr("@a.name == \"marko\" || (@a.age > 10)"))
==>v[1]
==>v[4]
==>v[6]
gremlin> g.V().where(expr("@.age isNull")).values("name")
==>ripple
==>lop
gremlin> g.V().where(expr("!(@.age isNull)")).values("name")
==>marko
==>vadas
==>josh
==>peter
gremlin> g.V().select(expr("@.name"))
==>marko
==>vadas
Expand Down
4 changes: 3 additions & 1 deletion interactive_engine/executor/ir/common/src/expr_parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ impl TryFrom<Token> for pb::ExprOpr {
}
Token::IdentArray(idents) => Ok((idents_to_vars(idents)?, false).into()),
Token::IdentMap(idents) => Ok((idents_to_vars(idents)?, true).into()),
Token::IsNull => Ok(pb::Logical::Isnull.into()),
}
}
}
Expand Down Expand Up @@ -155,7 +156,8 @@ impl ExprToken for pb::ExprOpr {
| pb::Logical::Within
| pb::Logical::Without
| pb::Logical::Startswith
| pb::Logical::Endswith => 80,
| pb::Logical::Endswith
| pb::Logical::Isnull => 80,
pb::Logical::And => 75,
pb::Logical::Or => 70,
pb::Logical::Not => 110,
Expand Down
10 changes: 8 additions & 2 deletions interactive_engine/executor/ir/common/src/expr_parse/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub enum Token {
Without, // Without
StartsWith, // String StartsWith
EndsWith, // String EndsWith

IsNull, // IsNull
// Precedence
LBrace, // (
RBrace, // )
Expand Down Expand Up @@ -93,7 +93,7 @@ impl ExprToken for Token {
Star | Slash | Percent => 100,
Power => 120,

Eq | Ne | Gt | Lt | Ge | Le | Within | Without | StartsWith | EndsWith => 80,
Eq | Ne | Gt | Lt | Ge | Le | Within | Without | StartsWith | EndsWith | IsNull => 80,
And => 75,
Or => 70,
Not => 110,
Expand Down Expand Up @@ -361,6 +361,8 @@ fn partial_tokens_to_tokens(mut tokens: &[PartialToken]) -> ExprResult<Vec<Token
Some(Token::StartsWith)
} else if literal.to_lowercase().as_str() == "endswith" {
Some(Token::EndsWith)
} else if literal.to_lowercase().as_str() == "isnull" {
Some(Token::IsNull)
} else {
// To parse the float of the form `<coefficient>e{+,-}<exponent>`,
// for example [Literal("10e"), Minus, Literal("3")] => "1e-3".parse().
Expand Down Expand Up @@ -579,6 +581,10 @@ mod tests {
let case4 = tokenize("1 + -2 + 2").unwrap();
let expected_case4 = vec![Token::Int(1), Token::Plus, Token::Int(-2), Token::Plus, Token::Int(2)];
assert_eq!(case4, expected_case4);

let case5 = tokenize("@a isNull");
let expected_case5 = vec![Token::Identifier("@a".to_string()), Token::IsNull];
assert_eq!(case5.unwrap(), expected_case5);
}

#[test]
Expand Down
92 changes: 82 additions & 10 deletions interactive_engine/executor/ir/graph_proxy/src/utils/expr/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub struct Evaluator {
suffix_tree: Vec<InnerOpr>,
/// A stack for evaluating the suffix-tree-based expression
/// Wrap it in a `RefCell` to avoid conflict mutable reference
stack: RefCell<Vec<Object>>,
stack: RefCell<Vec<ExprEvalResult<Object>>>,
}

unsafe impl Sync for Evaluator {}
Expand Down Expand Up @@ -166,6 +166,8 @@ pub(crate) fn apply_logical<'a>(
use common_pb::Logical::*;
if logical == &Not {
return Ok((!a.eval_bool::<(), NoneContext>(None)?).into());
} else if logical == &Isnull {
return Ok(a.eq(&BorrowObject::None).into());
} else {
if b_opt.is_some() {
let b = b_opt.unwrap();
Expand Down Expand Up @@ -193,6 +195,7 @@ pub(crate) fn apply_logical<'a>(
.ends_with(b.as_str()?.as_ref())
.into()),
Not => unreachable!(),
Isnull => unreachable!(),
}
} else {
Err(ExprEvalError::MissingOperands(InnerOpr::Logical(*logical).into()))
Expand All @@ -217,7 +220,14 @@ impl Evaluator {
let first = _first.unwrap();
let second = _second.unwrap();
if let InnerOpr::Logical(logical) = second {
Ok(apply_logical(logical, first.eval(context)?.as_borrow(), None)?)
let first = match first.eval(context) {
Ok(first) => Ok(first),
Err(err) => match err {
ExprEvalError::GetNoneFromContext => Ok(Object::None),
_ => Err(err),
},
};
Ok(apply_logical(logical, first?.as_borrow(), None)?)
} else {
if !second.is_operand() {
Err(ExprEvalError::MissingOperands(second.into()))
Expand All @@ -229,8 +239,27 @@ impl Evaluator {
let first = _first.unwrap();
let second = _second.unwrap();
let third = _third.unwrap();

if let InnerOpr::Logical(logical) = third {
// to deal with two unary operators cases, e.g., !(!true), !(a isNull) etc.
if common_pb::Logical::Not.eq(logical) || common_pb::Logical::Isnull.eq(logical) {
if let InnerOpr::Logical(inner_logical) = second {
let mut inner_first = first.eval(context);
if common_pb::Logical::Isnull.eq(inner_logical) {
match inner_first {
Err(ExprEvalError::GetNoneFromContext) => inner_first = Ok(Object::None),
_ => {}
}
}
let mut first = Ok(apply_logical(inner_logical, inner_first?.as_borrow(), None)?);
if common_pb::Logical::Isnull.eq(logical) {
match first {
Err(ExprEvalError::GetNoneFromContext) => first = Ok(Object::None),
_ => {}
}
}
return Ok(apply_logical(logical, first?.as_borrow(), None)?);
}
}
let a = first.eval(context)?;
let b = second.eval(context)?;
Ok(apply_logical(logical, a.as_borrow(), Some(b.as_borrow()))?)
Expand Down Expand Up @@ -307,38 +336,47 @@ impl Evaluate for Evaluator {
stack.clear();
for opr in &self.suffix_tree {
if opr.is_operand() {
stack.push(opr.eval(context)?);
stack.push(opr.eval(context));
} else {
if let Some(first) = stack.pop() {
let first_borrow = first.as_borrow();
let rst = match opr {
InnerOpr::Logical(logical) => {
if logical == &common_pb::Logical::Not {
apply_logical(logical, first_borrow, None)
apply_logical(logical, first?.as_borrow(), None)
} else if logical == &common_pb::Logical::Isnull {
let first_obj = match first {
Ok(obj) => obj,
Err(err) => match err {
ExprEvalError::GetNoneFromContext => Object::None,
_ => return Err(err),
},
};
apply_logical(logical, first_obj.as_borrow(), None)
} else {
if let Some(second) = stack.pop() {
apply_logical(logical, second.as_borrow(), Some(first_borrow))
apply_logical(logical, second?.as_borrow(), Some(first?.as_borrow()))
} else {
Err(ExprEvalError::OtherErr("invalid expression".to_string()))
}
}
}

InnerOpr::Arith(arith) => {
if let Some(second) = stack.pop() {
apply_arith(arith, second.as_borrow(), first_borrow)
apply_arith(arith, second?.as_borrow(), first?.as_borrow())
} else {
Err(ExprEvalError::OtherErr("invalid expression".to_string()))
}
}
_ => unreachable!(),
};
stack.push((rst?).into());
stack.push(rst);
}
}
}

if stack.len() == 1 {
Ok(stack.pop().unwrap())
Ok(stack.pop().unwrap()?)
} else {
Err("invalid expression".into())
}
Expand Down Expand Up @@ -662,6 +700,7 @@ mod tests {
"1 << 2", // 4
"4 >> 2", // 1
"232 & 64 != 0", // true
"!(!true)", // true
];

let expected: Vec<Object> = vec![
Expand Down Expand Up @@ -697,6 +736,7 @@ mod tests {
object!(4),
object!(1),
object!(true),
object!(true),
];

for (case, expected) in cases.into_iter().zip(expected.into_iter()) {
Expand Down Expand Up @@ -923,4 +963,36 @@ mod tests {
is_context = true;
}
}

#[test]
fn test_eval_is_null() {
// [v0: id = 1, label = 9, age = 31, name = John, birthday = 19900416, hobbies = [football, guitar]]
// [v1: id = 2, label = 11, age = 26, name = Jimmy, birthday = 19950816]
let ctxt = prepare_context();
let cases: Vec<&str> = vec![
"@0.hobbies isNull", // false
"!(@0.hobbies isNull)", // true
"@1.hobbies isNull", // true
"!(@1.hobbies isNull)", // false
"true isNull", // false
"false isNull", // false
"!true isNull", // i.e., !(true isNull), false
"@1.hobbies isNull && @1.age == 26", // true
];
let expected: Vec<Object> = vec![
object!(false),
object!(true),
object!(true),
object!(false),
object!(false),
object!(false),
object!(false),
object!(true),
];

for (case, expected) in cases.into_iter().zip(expected.into_iter()) {
let eval = Evaluator::try_from(str_to_expr_pb(case.to_string()).unwrap()).unwrap();
assert_eq!(eval.eval::<_, Vertices>(Some(&ctxt)).unwrap(), expected);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ impl From<Partial> for Option<Predicates> {
cmp: cmp.unwrap(),
right: right.unwrap(),
}))
} else if right.is_none() {
if cmp.unwrap() == common_pb::Logical::Isnull {
Some(Predicates::Predicate(Predicate {
left: left.unwrap(),
cmp: cmp.unwrap(),
right: Operand::Const(Object::None),
}))
} else {
None
}
} else {
None
}
Expand Down Expand Up @@ -323,6 +333,18 @@ impl EvalPred for Predicate {
)?
.as_bool()
.unwrap_or(false)),
Logical::Isnull => {
let left = match self.left.eval(context) {
Ok(left) => Ok(left),
Err(err) => match err {
ExprEvalError::GetNoneFromContext => Ok(Object::None),
_ => Err(err),
},
};
Ok(apply_logical(&self.cmp, left?.as_borrow_object(), None)?
.as_bool()
.unwrap_or(false))
}
_ => Err(ExprEvalError::OtherErr(format!(
"invalid logical operator: {:?} in a predicate",
self.cmp
Expand Down Expand Up @@ -418,7 +440,8 @@ fn process_predicates(
| Logical::Within
| Logical::Without
| Logical::Startswith
| Logical::Endswith => partial.cmp(logical)?,
| Logical::Endswith
| Logical::Isnull => partial.cmp(logical)?,
Logical::Not => is_not = true,
Logical::And | Logical::Or => {
predicates = predicates.merge_partial(curr_cmp, partial, is_not)?;
Expand Down Expand Up @@ -895,4 +918,30 @@ mod tests {
.eval_bool::<_, Vertices>(Some(&context))
.unwrap());
}

#[test]
fn test_eval_predicates_is_null() {
// [v0: id = 1, label = 9, age = 31, name = John, birthday = 19900416, hobbies = [football, guitar]]
// [v1: id = 2, label = 11, age = 26, name = Jimmy, birthday = 19950816]
let ctxt = prepare_context();
let cases: Vec<&str> = vec![
"@0.hobbies isNull", // false
"!(@0.hobbies isNull)", // true
"@1.hobbies isNull", // true
"!(@1.hobbies isNull)", // false
"true isNull", // false
"false isNull", // false
"@1.hobbies isNull && @1.age == 26", // true
];
let expected: Vec<bool> = vec![false, true, true, false, false, false, true];

for (case, expected) in cases.into_iter().zip(expected.into_iter()) {
let eval = PEvaluator::try_from(str_to_expr_pb(case.to_string()).unwrap()).unwrap();
assert_eq!(
eval.eval_bool::<_, Vertices>(Some(&ctxt))
.unwrap(),
expected
);
}
}
}
Loading