Repository: alibaba/GraphScope (language: python)

Issue:
[BUG] unexpected results to evaluate whether the value of a variable is None in GIE
**Describe the bug**

Currently, if a variable "a" refers to a None value, then to evaluate whether it is None, the result is false. This may not be unexpected, if it is `is_null()` checking.

**To Reproduce**
Run the following test:

```
 #[test]
    fn left_join_is_none_test() {
        initialize();
        // g.V().as("a")
        let source_opr_1 = algebra_pb::Scan {
            scan_opt: 0,
            alias: Some(0.into()),
            params: Some(query_params(vec![], vec![], None)),
            idx_predicate: None,
            meta_data: None,
        };

        // g.V().hasLabel("person").as("b")
        let source_opr_2 = algebra_pb::Scan {
            scan_opt: 0,
            alias: Some(1.into()),
            params: Some(query_params(vec![PERSON_LABEL.into()], vec![], None)),
            idx_predicate: None,
            meta_data: None,
        };

        // .where(expr("@b==None"))
        let select_opr = algebra_pb::Select {
            predicate: Some(common_pb::Expression {
                operators: vec![
                    common_pb::ExprOpr {
                        node_type: None,
                        item: Some(common_pb::expr_opr::Item::Var(common_pb::Variable {
                            tag: Some(1.into()),
                            property: None,
                            node_type: None,
                        })),
                    },
                    common_pb::ExprOpr {
                        node_type: None,
                        item: Some(common_pb::expr_opr::Item::Logical(common_pb::Logical::Eq as i32)),
                    },
                    common_pb::ExprOpr {
                        node_type: None,
                        item: Some(common_pb::expr_opr::Item::Const(common_pb::Value {
                            item: Some(common_pb::value::Item::None(common_pb::None {})),
                        })),
                    },
                ],
            }),
        };

        let mut job_builder = JobBuilder::default();
        job_builder.add_dummy_source();
        job_builder.join_func(
            unsafe { std::mem::transmute(1) },
            move |left| {
                left.add_scan_source(source_opr_1.clone());
            },
            move |right| {
                right.add_scan_source(source_opr_2.clone());
            },
            // @a.~id
            vec![common_pb::Variable::from("@0.~id".to_string())],
            // @b.~id
            vec![common_pb::Variable::from("@1.~id".to_string())],
        );
        job_builder.select(select_opr);
        job_builder.sink(algebra_pb::Sink {
            tags: vec![
                common_pb::NameOrIdKey { key: Some(0.into()) },
                common_pb::NameOrIdKey { key: Some(1.into()) },
            ],
            sink_target: default_sink_target(),
        });

        let request = job_builder.build().unwrap();
        let mut results = submit_query(request, 1);
        let mut result_collection = vec![];
        let v3: DefaultId = LDBCVertexParser::to_global_id(3, 1);
        let v5: DefaultId = LDBCVertexParser::to_global_id(5, 1);
        let mut expected_result_ids = vec![v3, v5];
        while let Some(result) = results.next() {
            match result {
                Ok(res) => {
                    let record = parse_result(res).unwrap();
                    println!("result {:?}", record);
                    if let Some(vertex) = record.get(Some(TAG_A)).unwrap().as_vertex() {
                        result_collection.push(vertex.id() as DefaultId);
                    }
                }
                Err(e) => {
                    panic!("err result {:?}", e);
                }
            }
        }
        expected_result_ids.sort();
        result_collection.sort();
        assert_eq!(result_collection, expected_result_ids)
    }
```



Discussion / hints:

Fix patch:
diff --git a/docs/interactive_engine/tinkerpop/supported_gremlin_steps.md b/docs/interactive_engine/tinkerpop/supported_gremlin_steps.md
index 32bd7c00ed69..481938d473e3 100644
--- a/docs/interactive_engine/tinkerpop/supported_gremlin_steps.md
+++ b/docs/interactive_engine/tinkerpop/supported_gremlin_steps.md
@@ -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
@@ -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
diff --git a/interactive_engine/executor/ir/common/src/expr_parse/mod.rs b/interactive_engine/executor/ir/common/src/expr_parse/mod.rs
index 80f95f63f021..1d559d0b5301 100644
--- a/interactive_engine/executor/ir/common/src/expr_parse/mod.rs
+++ b/interactive_engine/executor/ir/common/src/expr_parse/mod.rs
@@ -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()),
         }
     }
 }
@@ -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,
diff --git a/interactive_engine/executor/ir/common/src/expr_parse/token.rs b/interactive_engine/executor/ir/common/src/expr_parse/token.rs
index 13cd7f9416c5..7bf3caa303e8 100644
--- a/interactive_engine/executor/ir/common/src/expr_parse/token.rs
+++ b/interactive_engine/executor/ir/common/src/expr_parse/token.rs
@@ -46,7 +46,7 @@ pub enum Token {
     Without,    // Without
     StartsWith, // String StartsWith
     EndsWith,   // String EndsWith
-
+    IsNull,     // IsNull
     // Precedence
     LBrace, // (
     RBrace, // )
@@ -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,
@@ -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().
@@ -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]
diff --git a/interactive_engine/executor/ir/graph_proxy/src/utils/expr/eval.rs b/interactive_engine/executor/ir/graph_proxy/src/utils/expr/eval.rs
index 69b4a8f45b54..631e07016745 100644
--- a/interactive_engine/executor/ir/graph_proxy/src/utils/expr/eval.rs
+++ b/interactive_engine/executor/ir/graph_proxy/src/utils/expr/eval.rs
@@ -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 {}
@@ -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();
@@ -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()))
@@ -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()))
@@ -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()))?)
@@ -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())
         }
@@ -662,6 +700,7 @@ mod tests {
             "1 << 2",         // 4
             "4 >> 2",         // 1
             "232 & 64 != 0",  // true
+            "!(!true)",       // true
         ];
 
         let expected: Vec<Object> = vec![
@@ -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()) {
@@ -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);
+        }
+    }
 }
diff --git a/interactive_engine/executor/ir/graph_proxy/src/utils/expr/eval_pred.rs b/interactive_engine/executor/ir/graph_proxy/src/utils/expr/eval_pred.rs
index a3595358f969..abcaff88b771 100644
--- a/interactive_engine/executor/ir/graph_proxy/src/utils/expr/eval_pred.rs
+++ b/interactive_engine/executor/ir/graph_proxy/src/utils/expr/eval_pred.rs
@@ -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
                 }
@@ -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
@@ -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)?;
@@ -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
+            );
+        }
+    }
 }
diff --git a/interactive_engine/executor/ir/proto/expr.proto b/interactive_engine/executor/ir/proto/expr.proto
index 2ebe5478ac34..c65250912967 100644
--- a/interactive_engine/executor/ir/proto/expr.proto
+++ b/interactive_engine/executor/ir/proto/expr.proto
@@ -77,6 +77,8 @@ enum Logical {
   OR = 11;
   // A unary logical not operator.
   NOT = 12;
+  // A unary logical isnull operator
+  ISNULL = 13;
 }
 
 enum Arithmetic {


Test patch:
diff --git a/interactive_engine/executor/ir/integrated/tests/join_test.rs b/interactive_engine/executor/ir/integrated/tests/join_test.rs
index e850851030f4..38eb3d3288e0 100644
--- a/interactive_engine/executor/ir/integrated/tests/join_test.rs
+++ b/interactive_engine/executor/ir/integrated/tests/join_test.rs
@@ -22,6 +22,7 @@ mod test {
     use graph_proxy::apis::GraphElement;
     use graph_store::common::DefaultId;
     use graph_store::ldbc::LDBCVertexParser;
+    use ir_common::expr_parse::str_to_expr_pb;
     use ir_common::generated::algebra as algebra_pb;
     use ir_common::generated::common as common_pb;
     use ir_physical_client::physical_builder::*;
@@ -310,4 +311,151 @@ mod test {
     fn anti_join_w2_test() {
         anti_join(2)
     }
+
+    #[test]
+    fn left_join_is_none_test() {
+        initialize();
+        // g.V().as("a")
+        let source_opr_1 = algebra_pb::Scan {
+            scan_opt: 0,
+            alias: Some(TAG_A.into()),
+            params: Some(query_params(vec![], vec![], None)),
+            idx_predicate: None,
+            meta_data: None,
+        };
+
+        // g.V().hasLabel("person").as("b")
+        let source_opr_2 = algebra_pb::Scan {
+            scan_opt: 0,
+            alias: Some(TAG_B.into()),
+            params: Some(query_params(vec![PERSON_LABEL.into()], vec![], None)),
+            idx_predicate: None,
+            meta_data: None,
+        };
+
+        // where b.is(null)
+        let select_opr = algebra_pb::Select { predicate: str_to_expr_pb("@1 isnull".to_string()).ok() };
+
+        let mut job_builder = JobBuilder::default();
+        job_builder.add_dummy_source();
+        job_builder.join_func(
+            unsafe { std::mem::transmute(1) },
+            move |left| {
+                left.add_scan_source(source_opr_1.clone());
+            },
+            move |right| {
+                right.add_scan_source(source_opr_2.clone());
+            },
+            // @a.~id
+            vec![common_pb::Variable::from("@0.~id".to_string())],
+            // @b.~id
+            vec![common_pb::Variable::from("@1.~id".to_string())],
+        );
+        job_builder.select(select_opr);
+        job_builder.sink(algebra_pb::Sink {
+            tags: vec![
+                common_pb::NameOrIdKey { key: Some(TAG_A.into()) },
+                common_pb::NameOrIdKey { key: Some(TAG_B.into()) },
+            ],
+            sink_target: default_sink_target(),
+        });
+
+        let request = job_builder.build().unwrap();
+        let mut results = submit_query(request, 1);
+        let mut result_collection = vec![];
+        let v3: DefaultId = LDBCVertexParser::to_global_id(3, 1);
+        let v5: DefaultId = LDBCVertexParser::to_global_id(5, 1);
+        let mut expected_result_ids = vec![v3, v5];
+        while let Some(result) = results.next() {
+            match result {
+                Ok(res) => {
+                    let record = parse_result(res).unwrap();
+                    println!("result {:?}", record);
+                    if let Some(vertex) = record.get(Some(TAG_A)).unwrap().as_vertex() {
+                        result_collection.push(vertex.id() as DefaultId);
+                    }
+                }
+                Err(e) => {
+                    panic!("err result {:?}", e);
+                }
+            }
+        }
+        expected_result_ids.sort();
+        result_collection.sort();
+        assert_eq!(result_collection, expected_result_ids)
+    }
+
+    #[test]
+    fn left_join_is_not_none_test() {
+        initialize();
+        // g.V().as("a")
+        let source_opr_1 = algebra_pb::Scan {
+            scan_opt: 0,
+            alias: Some(TAG_A.into()),
+            params: Some(query_params(vec![], vec![], None)),
+            idx_predicate: None,
+            meta_data: None,
+        };
+
+        // g.V().hasLabel("person").as("b")
+        let source_opr_2 = algebra_pb::Scan {
+            scan_opt: 0,
+            alias: Some(TAG_B.into()),
+            params: Some(query_params(vec![PERSON_LABEL.into()], vec![], None)),
+            idx_predicate: None,
+            meta_data: None,
+        };
+
+        let select_opr = algebra_pb::Select { predicate: str_to_expr_pb("!(@1 isnull)".to_string()).ok() };
+
+        let mut job_builder = JobBuilder::default();
+        job_builder.add_dummy_source();
+        job_builder.join_func(
+            unsafe { std::mem::transmute(1) },
+            move |left| {
+                left.add_scan_source(source_opr_1.clone());
+            },
+            move |right| {
+                right.add_scan_source(source_opr_2.clone());
+            },
+            // @a.~id
+            vec![common_pb::Variable::from("@0.~id".to_string())],
+            // @b.~id
+            vec![common_pb::Variable::from("@1.~id".to_string())],
+        );
+        job_builder.select(select_opr);
+        job_builder.sink(algebra_pb::Sink {
+            tags: vec![
+                common_pb::NameOrIdKey { key: Some(TAG_A.into()) },
+                common_pb::NameOrIdKey { key: Some(TAG_B.into()) },
+            ],
+            sink_target: default_sink_target(),
+        });
+
+        let request = job_builder.build().unwrap();
+        let mut results = submit_query(request, 1);
+        let mut result_collection = vec![];
+        let v1: DefaultId = LDBCVertexParser::to_global_id(1, 0);
+        let v2: DefaultId = LDBCVertexParser::to_global_id(2, 0);
+        let v4: DefaultId = LDBCVertexParser::to_global_id(4, 0);
+        let v6: DefaultId = LDBCVertexParser::to_global_id(6, 0);
+        let mut expected_result_ids = vec![v1, v2, v4, v6];
+        while let Some(result) = results.next() {
+            match result {
+                Ok(res) => {
+                    let record = parse_result(res).unwrap();
+                    println!("result {:?}", record);
+                    if let Some(vertex) = record.get(Some(TAG_A)).unwrap().as_vertex() {
+                        result_collection.push(vertex.id() as DefaultId);
+                    }
+                }
+                Err(e) => {
+                    panic!("err result {:?}", e);
+                }
+            }
+        }
+        expected_result_ids.sort();
+        result_collection.sort();
+        assert_eq!(result_collection, expected_result_ids)
+    }
 }