From 79126fb63163e6e985f8b79bbc42d8b0324d6f47 Mon Sep 17 00:00:00 2001
From: Denis Smirnov <sd@picodata.io>
Date: Wed, 10 Aug 2022 16:32:54 +0700
Subject: [PATCH] feat: implement "not in" operator

---
 src/executor/tests.rs                   |  3 ++
 src/executor/tests/not_in.rs            | 69 +++++++++++++++++++++++++
 src/frontend/sql.rs                     |  3 +-
 src/frontend/sql/ast.rs                 |  2 +
 src/frontend/sql/ir.rs                  |  1 +
 src/frontend/sql/query.pest             | 17 +++---
 src/ir/operator.rs                      |  4 ++
 src/ir/transformation/redistribution.rs |  9 ++--
 8 files changed, 97 insertions(+), 11 deletions(-)
 create mode 100644 src/executor/tests/not_in.rs

diff --git a/src/executor/tests.rs b/src/executor/tests.rs
index 7d18caa9ea..ce0e6c9939 100644
--- a/src/executor/tests.rs
+++ b/src/executor/tests.rs
@@ -1371,3 +1371,6 @@ fn get_motion_policy(plan: &Plan, motion_id: usize) -> &MotionPolicy {
 
 #[cfg(test)]
 mod between;
+
+#[cfg(test)]
+mod not_in;
diff --git a/src/executor/tests/not_in.rs b/src/executor/tests/not_in.rs
new file mode 100644
index 0000000000..bdfded6dfc
--- /dev/null
+++ b/src/executor/tests/not_in.rs
@@ -0,0 +1,69 @@
+use pretty_assertions::assert_eq;
+
+use crate::executor::engine::cartridge::backend::sql::ir::PatternWithParams;
+use crate::executor::engine::mock::RouterRuntimeMock;
+use crate::executor::result::ProducerResult;
+use crate::executor::vtable::VirtualTable;
+use crate::ir::relation::{Column, ColumnRole, Type};
+use crate::ir::transformation::redistribution::tests::get_motion_id;
+use crate::ir::transformation::redistribution::MotionPolicy;
+use crate::ir::value::Value;
+
+use super::*;
+
+#[test]
+fn not_in1_test() {
+    let sql = r#"
+        SELECT "identification_number" FROM "hash_testing" AS "t"
+        WHERE "identification_number" NOT IN (
+            SELECT "identification_number" as "id" FROM "hash_testing_hist"
+            WHERE "identification_number" = 3
+        )
+        "#;
+
+    // Initialize the query.
+    let coordinator = RouterRuntimeMock::new();
+    let mut query = Query::new(&coordinator, sql, &[]).unwrap();
+    let plan = query.exec_plan.get_ir_plan();
+
+    // Validate the motion type.
+    let motion_id = *get_motion_id(&plan, 0, 0).unwrap();
+    assert_eq!(&MotionPolicy::Full, get_motion_policy(plan, motion_id));
+    assert_eq!(true, get_motion_id(plan, 0, 1).is_none());
+
+    // Mock a virtual table.
+    let mut virtual_table = VirtualTable::new();
+    virtual_table.add_column(Column {
+        name: "id".into(),
+        r#type: Type::Integer,
+        role: ColumnRole::User,
+    });
+    virtual_table.add_tuple(vec![Value::from(3_u64)]);
+    query
+        .coordinator
+        .add_virtual_table(motion_id, virtual_table);
+
+    // Execute the query.
+    let result = *query
+        .dispatch()
+        .unwrap()
+        .downcast::<ProducerResult>()
+        .unwrap();
+
+    // Validate the result.
+    let mut expected = ProducerResult::new();
+    expected.rows.extend(vec![vec![
+        Value::String(format!("Execute query on all buckets")),
+        Value::String(String::from(PatternWithParams::new(
+            format!(
+                "{} {}",
+                r#"SELECT "t"."identification_number" FROM "hash_testing" as "t""#,
+                r#"WHERE ("t"."identification_number") not in (SELECT COLUMN_1 as "id" FROM (VALUES (?)))"#,
+            ),
+            vec![
+                Value::from(3_u64),
+            ],
+        ))),
+    ]]);
+    assert_eq!(expected, result);
+}
diff --git a/src/frontend/sql.rs b/src/frontend/sql.rs
index 8d8c1c049b..3a6a857d33 100644
--- a/src/frontend/sql.rs
+++ b/src/frontend/sql.rs
@@ -460,7 +460,8 @@ impl Ast for AbstractSyntaxTree {
                 | Type::GtEq
                 | Type::Lt
                 | Type::LtEq
-                | Type::NotEq => {
+                | Type::NotEq
+                | Type::NotIn => {
                     let ast_left_id = node.children.get(0).ok_or_else(|| {
                         QueryPlannerError::CustomError(
                             "Left node id is not found among comparison children.".into(),
diff --git a/src/frontend/sql/ast.rs b/src/frontend/sql/ast.rs
index 5f8a1729bc..b1a76f3660 100644
--- a/src/frontend/sql/ast.rs
+++ b/src/frontend/sql/ast.rs
@@ -48,6 +48,7 @@ pub enum Type {
     LtEq,
     Name,
     NotEq,
+    NotIn,
     Null,
     Or,
     Parameter,
@@ -101,6 +102,7 @@ impl Type {
             Rule::LtEq => Ok(Type::LtEq),
             Rule::Name => Ok(Type::Name),
             Rule::NotEq => Ok(Type::NotEq),
+            Rule::NotIn => Ok(Type::NotIn),
             Rule::Null => Ok(Type::Null),
             Rule::Or => Ok(Type::Or),
             Rule::Parameter => Ok(Type::Parameter),
diff --git a/src/frontend/sql/ir.rs b/src/frontend/sql/ir.rs
index 39acdff593..f60284307c 100644
--- a/src/frontend/sql/ir.rs
+++ b/src/frontend/sql/ir.rs
@@ -29,6 +29,7 @@ impl Bool {
             Type::Lt => Ok(Bool::Lt),
             Type::LtEq => Ok(Bool::LtEq),
             Type::NotEq => Ok(Bool::NotEq),
+            Type::NotIn => Ok(Bool::NotIn),
             _ => Err(QueryPlannerError::InvalidBool),
         }
     }
diff --git a/src/frontend/sql/query.pest b/src/frontend/sql/query.pest
index f8bf690c45..4692358b17 100644
--- a/src/frontend/sql/query.pest
+++ b/src/frontend/sql/query.pest
@@ -33,28 +33,31 @@ Expr = _{  Or | And | Unary | Between | Cmp | Primary | Parentheses }
     Primary = _{ SubQuery | Value | Reference }
     Unary = _{ IsNull }
         IsNull = { Primary ~ ^"is" ~ ^"null" } 
-    Cmp = _{ Eq | In | Gt | GtEq | Lt | LtEq | NotEq }
+    Cmp = _{ Eq | In | Gt | GtEq | Lt | LtEq | NotEq | NotIn }
     Eq = { EqLeft ~ "=" ~ EqRight }
         EqLeft = _{ Primary }
         EqRight = _{ Eq | EqLeft }
     In = { InLeft ~ ^"in" ~ InRight }
-        InLeft = _{ EqRight }
+        InLeft = _{ Primary }
         InRight = _{ In | InLeft }
     Gt = { GtLeft ~ ">" ~ GtRight }
-        GtLeft = _{ InRight }
+        GtLeft = _{ Primary }
         GtRight = _{ Gt | GtLeft }
     GtEq = { GtEqLeft ~ ">=" ~ GtEqRight }
-        GtEqLeft = _{ GtRight }
+        GtEqLeft = _{ Primary }
         GtEqRight = _{ GtEq | GtEqLeft }
     Lt = { LtLeft ~ "<" ~ LtRight }
-        LtLeft = _{ GtEqRight }
+        LtLeft = _{ Primary }
         LtRight = _{ Lt | LtLeft }
     LtEq = { LtEqLeft ~ "<=" ~ LtEqRight }
-        LtEqLeft = _{ LtRight }
+        LtEqLeft = _{ Primary }
         LtEqRight = _{ LtEq | LtEqLeft }
     NotEq = { NotEqLeft ~ ("<>" | "!=") ~ NotEqRight }
-        NotEqLeft = _{ LtEqRight }
+        NotEqLeft = _{ Primary }
         NotEqRight = _{ NotEq | NotEqLeft }
+    NotIn = { NotInLeft ~ ^"not" ~ ^"in" ~ NotInRight }
+        NotInLeft = _{ Primary }
+        NotInRight = _{ NotIn | NotInLeft }
     Between = { BetweenLeft ~ ^"between" ~ BetweenCenter ~ ^"and" ~ BetweenRight }
         BetweenLeft = _{ Primary }
         BetweenCenter = _{ Primary }
diff --git a/src/ir/operator.rs b/src/ir/operator.rs
index fcf5610239..2860407d89 100644
--- a/src/ir/operator.rs
+++ b/src/ir/operator.rs
@@ -37,6 +37,8 @@ pub enum Bool {
     LtEq,
     /// `!=`
     NotEq,
+    /// `not in`
+    NotIn,
     /// `||`
     Or,
 }
@@ -57,6 +59,7 @@ impl Bool {
             "<" => Ok(Bool::Lt),
             "<=" => Ok(Bool::LtEq),
             "!=" | "<>" => Ok(Bool::NotEq),
+            "not in" => Ok(Bool::NotIn),
             _ => Err(QueryPlannerError::InvalidBool),
         }
     }
@@ -74,6 +77,7 @@ impl Display for Bool {
             Bool::Lt => "<",
             Bool::LtEq => "<=",
             Bool::NotEq => "<>",
+            Bool::NotIn => "not in",
         };
 
         write!(f, "{}", op)
diff --git a/src/ir/transformation/redistribution.rs b/src/ir/transformation/redistribution.rs
index 603990898c..f4d2bf08d6 100644
--- a/src/ir/transformation/redistribution.rs
+++ b/src/ir/transformation/redistribution.rs
@@ -718,9 +718,12 @@ impl Plan {
                         Bool::Eq | Bool::In => {
                             self.join_policy_for_eq(rel_id, bool_op.left, bool_op.right)?
                         }
-                        Bool::NotEq | Bool::Gt | Bool::GtEq | Bool::Lt | Bool::LtEq => {
-                            MotionPolicy::Full
-                        }
+                        Bool::NotEq
+                        | Bool::NotIn
+                        | Bool::Gt
+                        | Bool::GtEq
+                        | Bool::Lt
+                        | Bool::LtEq => MotionPolicy::Full,
                         Bool::And | Bool::Or => {
                             // "a and 1" or "a or 1" expressions make no sense.
                             return Err(QueryPlannerError::CustomError(
-- 
GitLab