From 44c75b919bd75622790095a0ad4d6d5c6f83dcda Mon Sep 17 00:00:00 2001
From: Dima Koltsov <dkoltsov@picodata.io>
Date: Mon, 15 Aug 2022 16:42:50 +0300
Subject: [PATCH] feat: implement "is not null" operator

Closes #226
---
 src/frontend/sql.rs                    |  2 +-
 src/frontend/sql/ast.rs                |  2 ++
 src/frontend/sql/ir.rs                 |  1 +
 src/frontend/sql/ir/tests.rs           | 16 +++++++++
 src/frontend/sql/query.pest            |  5 +--
 src/ir/explain.rs                      |  1 +
 src/ir/operator.rs                     |  4 +++
 test_app/test/integration/api_test.lua | 48 ++++++++++++++++++++++++++
 8 files changed, 76 insertions(+), 3 deletions(-)

diff --git a/src/frontend/sql.rs b/src/frontend/sql.rs
index 3a6a857d33..2d31b68c01 100644
--- a/src/frontend/sql.rs
+++ b/src/frontend/sql.rs
@@ -478,7 +478,7 @@ impl Ast for AbstractSyntaxTree {
                     let cond_id = plan.add_cond(plan_left_id, op, plan_right_id)?;
                     map.add(*id, cond_id);
                 }
-                Type::IsNull => {
+                Type::IsNull | Type::IsNotNull => {
                     let ast_child_id = node.children.get(0).ok_or_else(|| {
                         QueryPlannerError::CustomError(format!("{:?} has no children.", &node.rule))
                     })?;
diff --git a/src/frontend/sql/ast.rs b/src/frontend/sql/ast.rs
index b1a76f3660..7f54017ad8 100644
--- a/src/frontend/sql/ast.rs
+++ b/src/frontend/sql/ast.rs
@@ -44,6 +44,7 @@ pub enum Type {
     Insert,
     Integer,
     IsNull,
+    IsNotNull,
     Lt,
     LtEq,
     Name,
@@ -98,6 +99,7 @@ impl Type {
             Rule::Integer => Ok(Type::Integer),
             Rule::Insert => Ok(Type::Insert),
             Rule::IsNull => Ok(Type::IsNull),
+            Rule::IsNotNull => Ok(Type::IsNotNull),
             Rule::Lt => Ok(Type::Lt),
             Rule::LtEq => Ok(Type::LtEq),
             Rule::Name => Ok(Type::Name),
diff --git a/src/frontend/sql/ir.rs b/src/frontend/sql/ir.rs
index f60284307c..0de79a86d3 100644
--- a/src/frontend/sql/ir.rs
+++ b/src/frontend/sql/ir.rs
@@ -44,6 +44,7 @@ impl Unary {
     pub(super) fn from_node_type(s: &Type) -> Result<Self, QueryPlannerError> {
         match s {
             Type::IsNull => Ok(Unary::IsNull),
+            Type::IsNotNull => Ok(Unary::IsNotNull),
             _ => Err(QueryPlannerError::CustomError(format!(
                 "Invalid unary operator: {:?}",
                 s
diff --git a/src/frontend/sql/ir/tests.rs b/src/frontend/sql/ir/tests.rs
index 72f6b06322..1cb1932039 100644
--- a/src/frontend/sql/ir/tests.rs
+++ b/src/frontend/sql/ir/tests.rs
@@ -404,6 +404,22 @@ fn front_sql18() {
     assert_eq!(sql_to_sql(input, &[], &no_transform), expected);
 }
 
+#[test]
+fn front_sql19() {
+    let input = r#"SELECT "identification_number" FROM "hash_testing"
+        WHERE "product_code" IS NOT NULL"#;
+    let expected = PatternWithParams::new(
+        format!(
+            "{} {}",
+            r#"SELECT "hash_testing"."identification_number""#,
+            r#"FROM "hash_testing" WHERE ("hash_testing"."product_code") is not null"#,
+        ),
+        vec![],
+    );
+
+    assert_eq!(sql_to_sql(input, &[], &no_transform), expected);
+}
+
 #[test]
 fn front_params1() {
     let pattern = r#"SELECT "id", "FIRST_NAME" FROM "test_space"
diff --git a/src/frontend/sql/query.pest b/src/frontend/sql/query.pest
index 4692358b17..6eb4c9b0d9 100644
--- a/src/frontend/sql/query.pest
+++ b/src/frontend/sql/query.pest
@@ -31,8 +31,9 @@ Query = _{ Except | UnionAll | Select | Values | Insert }
 Expr = _{  Or | And | Unary | Between | Cmp | Primary | Parentheses }
     Parentheses = _{ "(" ~ Expr ~ ")" }
     Primary = _{ SubQuery | Value | Reference }
-    Unary = _{ IsNull }
-        IsNull = { Primary ~ ^"is" ~ ^"null" } 
+    Unary = _{ IsNull | IsNotNull}
+        IsNull = { Primary ~ ^"is" ~ ^"null" }
+        IsNotNull = { Primary ~ ^"is" ~ ^"not" ~ ^"null" }
     Cmp = _{ Eq | In | Gt | GtEq | Lt | LtEq | NotEq | NotIn }
     Eq = { EqLeft ~ "=" ~ EqRight }
         EqLeft = _{ Primary }
diff --git a/src/ir/explain.rs b/src/ir/explain.rs
index a60a9d2e94..0c109a2a20 100644
--- a/src/ir/explain.rs
+++ b/src/ir/explain.rs
@@ -344,6 +344,7 @@ impl Display for Selection {
             }
             Selection::UnaryOp { op, child } => match op {
                 Unary::IsNull => format!("{} {}", child, op),
+                Unary::IsNotNull => format!("{} {}", child, op),
             },
         };
 
diff --git a/src/ir/operator.rs b/src/ir/operator.rs
index 2860407d89..fec393e2e6 100644
--- a/src/ir/operator.rs
+++ b/src/ir/operator.rs
@@ -89,6 +89,8 @@ impl Display for Bool {
 pub enum Unary {
     /// `is null`
     IsNull,
+    /// `is not null`
+    IsNotNull,
 }
 
 impl Unary {
@@ -99,6 +101,7 @@ impl Unary {
     pub fn from(s: &str) -> Result<Self, QueryPlannerError> {
         match s.to_lowercase().as_str() {
             "is null" => Ok(Unary::IsNull),
+            "is not null" => Ok(Unary::IsNotNull),
             _ => Err(QueryPlannerError::CustomError(format!(
                 "Invalid unary operator: {}",
                 s
@@ -111,6 +114,7 @@ impl Display for Unary {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         let op = match &self {
             Unary::IsNull => "is null",
+            Unary::IsNotNull => "is not null",
         };
 
         write!(f, "{}", op)
diff --git a/test_app/test/integration/api_test.lua b/test_app/test/integration/api_test.lua
index 0a1dc1a062..2ce70b79ec 100644
--- a/test_app/test/integration/api_test.lua
+++ b/test_app/test/integration/api_test.lua
@@ -8,6 +8,8 @@ g.before_each(
     function()
         local api = cluster:server("api-1").net_box
 
+        -- "testing_space" contains:
+        -- [1, "123", 1]
         local r, err = api:call("sbroad.execute", {
             [[insert into "testing_space" ("id", "name", "product_units") values (?, ?, ?)]],
             {1, "123", 1}
@@ -15,6 +17,8 @@ g.before_each(
         t.assert_equals(err, nil)
         t.assert_equals(r, {row_count = 1})
 
+        -- "testing_space_hist" contains:
+        -- [1, "123", 5]
         r, err = api:call("sbroad.execute", {
             [[insert into "testing_space_hist" ("id", "name", "product_units") values (?, ?, ?)]],
             {1, "123", 5}
@@ -22,6 +26,9 @@ g.before_each(
         t.assert_equals(err, nil)
         t.assert_equals(r, {row_count = 1})
 
+        -- "space_simple_shard_key" contains:
+        -- [1, "ok", 1]
+        -- [10, NULL, 0]
         r, err = api:call("sbroad.execute", {
             [[insert into "space_simple_shard_key" ("id", "name", "sysOp") values (?, ?, ?), (?, ?, ?)]],
             {1, "ok", 1, 10, box.NULL, 0}
@@ -29,6 +36,9 @@ g.before_each(
         t.assert_equals(err, nil)
         t.assert_equals(r, {row_count = 2})
 
+        -- "space_simple_shard_key_hist" contains:
+        -- [1, "ok_hist", 3]
+        -- [2, "ok_hist_2", 1]
         r, err = api:call("sbroad.execute", {
             [[insert into "space_simple_shard_key_hist" ("id", "name", "sysOp") values (?, ?, ?), (?, ?, ?)]],
             {1, "ok_hist", 3, 2, "ok_hist_2", 1}
@@ -36,6 +46,9 @@ g.before_each(
         t.assert_equals(err, nil)
         t.assert_equals(r, {row_count = 2})
 
+        -- "t" contains:
+        -- [1, 4.2]
+        -- [2, decimal(6.66)]
         r, err = api:call("sbroad.execute", {
             [[insert into "t" ("id", "a") values (?, ?), (?, ?)]],
             {1, 4.2, 2, require('decimal').new(6.66)}
@@ -824,6 +837,41 @@ g.test_is_null = function()
     })
 end
 
+g.test_is_not_null_1 = function()
+    local api = cluster:server("api-1").net_box
+
+    local r, err = api:call("sbroad.execute", { [[
+        SELECT "id" FROM "space_simple_shard_key" WHERE "name" IS NOT NULL and "id" = 10
+    ]], {} })
+
+    t.assert_equals(err, nil)
+    t.assert_equals(r, {
+        metadata = {
+            {name = "id", type = "integer"},
+        },
+        rows = {
+        },
+    })
+end
+
+g.test_is_not_null_2 = function()
+    local api = cluster:server("api-1").net_box
+
+    local r, err = api:call("sbroad.execute", { [[
+        SELECT "id" FROM "space_simple_shard_key" WHERE "name" IS NOT NULL
+    ]], {} })
+
+    t.assert_equals(err, nil)
+    t.assert_equals(r, {
+        metadata = {
+            {name = "id", type = "integer"},
+        },
+        rows = {
+            {1}
+        },
+    })
+end
+
 g.test_between1 = function()
     local api = cluster:server("api-1").net_box
 
-- 
GitLab