From 7dad8474215945c3815327194318bfca7e9412b1 Mon Sep 17 00:00:00 2001
From: Kaitmazian Maksim <m.kaitmazian@picodata.io>
Date: Tue, 10 Sep 2024 12:40:20 +0300
Subject: [PATCH] feat: support empty queries

---
 .../test/integration/empty_queries.lua        | 33 +++++++++++++++++++
 sbroad-core/src/executor.rs                   | 19 +++++++++++
 sbroad-core/src/executor/engine/helpers.rs    | 17 ++++++++++
 sbroad-core/src/frontend/sql.rs               | 15 +++++++++
 sbroad-core/src/frontend/sql/query.pest       |  4 ++-
 sbroad-core/src/ir.rs                         |  7 +++-
 6 files changed, 93 insertions(+), 2 deletions(-)
 create mode 100644 sbroad-cartridge/test_app/test/integration/empty_queries.lua

diff --git a/sbroad-cartridge/test_app/test/integration/empty_queries.lua b/sbroad-cartridge/test_app/test/integration/empty_queries.lua
new file mode 100644
index 000000000..b97c9b267
--- /dev/null
+++ b/sbroad-cartridge/test_app/test/integration/empty_queries.lua
@@ -0,0 +1,33 @@
+local t = require('luatest')
+local g = t.group('integration_api.empty_queries')
+
+local helper = require('test.helper.cluster_no_replication')
+
+g.after_all(function()
+    helper.stop_test_cluster()
+end)
+
+g.test_empty_queries = function()
+    local r, err
+    local api = helper.cluster:server("api-1").net_box
+
+    r, err = api:call("sbroad.execute", { [[]] })
+    t.assert_equals(err, nil)
+    t.assert_equals(r["row_count"], 0)
+
+    r, err = api:call("sbroad.execute", { [[ ]] })
+    t.assert_equals(err, nil)
+    t.assert_equals(r["row_count"], 0)
+
+    r, err = api:call("sbroad.execute", { [[;]] })
+    t.assert_equals(err, nil)
+    t.assert_equals(r["row_count"], 0)
+
+    r, err = api:call("sbroad.execute", { [[ ; ]] })
+    t.assert_equals(err, nil)
+    t.assert_equals(r["row_count"], 0)
+
+    r, err = api:call("sbroad.execute", { [[; ; ;]] })
+    t.assert_equals(err, nil)
+    t.assert_equals(r["row_count"], 0)
+end
diff --git a/sbroad-core/src/executor.rs b/sbroad-core/src/executor.rs
index 7b944a796..94dc17ccd 100644
--- a/sbroad-core/src/executor.rs
+++ b/sbroad-core/src/executor.rs
@@ -126,6 +126,11 @@ where
         if plan.is_empty() {
             let metadata = coordinator.metadata().lock();
             plan = C::ParseTree::transform_into_plan(sql, &*metadata)?;
+            // Empty query.
+            if plan.is_empty() {
+                return Ok(Query::empty(coordinator));
+            }
+
             if coordinator.provides_versions() {
                 let mut table_version_map =
                     TableVersionMap::with_capacity(plan.relations.tables.len());
@@ -159,6 +164,15 @@ where
         Ok(query)
     }
 
+    fn empty(coordinator: &'a C) -> Self {
+        Self {
+            is_explain: false,
+            exec_plan: Plan::empty().into(),
+            coordinator,
+            bucket_map: Default::default(),
+        }
+    }
+
     /// Get the execution plan of the query.
     #[must_use]
     pub fn get_exec_plan(&self) -> &ExecutionPlan {
@@ -326,6 +340,11 @@ where
     pub fn is_acl(&self) -> Result<bool, SbroadError> {
         self.exec_plan.get_ir_plan().is_acl()
     }
+
+    /// Checks that query is an empty query.
+    pub fn is_empty(&self) -> bool {
+        self.exec_plan.get_ir_plan().is_empty()
+    }
 }
 
 #[cfg(test)]
diff --git a/sbroad-core/src/executor/engine/helpers.rs b/sbroad-core/src/executor/engine/helpers.rs
index 1d1e2a0b4..934367ed9 100644
--- a/sbroad-core/src/executor/engine/helpers.rs
+++ b/sbroad-core/src/executor/engine/helpers.rs
@@ -826,6 +826,18 @@ fn has_zero_limit_clause(plan: &ExecutionPlan) -> Result<bool, SbroadError> {
     Ok(false)
 }
 
+fn empty_query_response() -> Result<Box<dyn Any>, SbroadError> {
+    let res = ConsumerResult { row_count: 0 };
+    match Tuple::new(&[res]) {
+        Ok(t) => Ok(Box::new(t)),
+        Err(e) => Err(SbroadError::FailedTo(
+            Action::Create,
+            Some(Entity::Tuple),
+            format_smolstr!("{e}"),
+        )),
+    }
+}
+
 /// A helper function to dispatch the execution plan from the router to the storages.
 ///
 /// # Errors
@@ -841,6 +853,11 @@ pub fn dispatch_impl(
         Option::from("dispatch"),
         &format!("dispatching plan: {plan:?}")
     );
+
+    if plan.get_ir_plan().is_empty() {
+        return empty_query_response();
+    }
+
     let sub_plan = plan.take_subtree(top_id)?;
 
     let tier = {
diff --git a/sbroad-core/src/frontend/sql.rs b/sbroad-core/src/frontend/sql.rs
index 8bcbb6b74..c2690a53a 100644
--- a/sbroad-core/src/frontend/sql.rs
+++ b/sbroad-core/src/frontend/sql.rs
@@ -2638,6 +2638,10 @@ impl AbstractSyntaxTree {
         }
     }
 
+    fn is_empty(&self) -> bool {
+        self == &Self::empty()
+    }
+
     /// Constructor.
     /// Builds a tree (nodes are in postorder reverse).
     /// Builds abstract syntax tree (AST) from SQL query.
@@ -2663,6 +2667,12 @@ impl AbstractSyntaxTree {
         let top_pair = command_pair
             .next()
             .expect("Query expected as a first parsing tree child.");
+
+        if let Rule::EmptyQuery = top_pair.as_rule() {
+            *self = Self::empty();
+            return Ok(());
+        }
+
         let top = StackParseNode::new(top_pair, None);
 
         let mut stack: Vec<StackParseNode> = vec![top];
@@ -3848,6 +3858,11 @@ impl Ast for AbstractSyntaxTree {
         let mut pos_to_ast_id: SelectChildPairTranslation = HashMap::new();
         let mut ast = AbstractSyntaxTree::empty();
         ast.fill(query, &mut ast_id_to_pairs_map, &mut pos_to_ast_id)?;
+        // Empty query.
+        if ast.is_empty() {
+            return Ok(Plan::empty());
+        }
+
         ast.resolve_metadata(metadata, &mut ast_id_to_pairs_map, &mut pos_to_ast_id)
     }
 }
diff --git a/sbroad-core/src/frontend/sql/query.pest b/sbroad-core/src/frontend/sql/query.pest
index 9cfe1cb97..2c397badd 100644
--- a/sbroad-core/src/frontend/sql/query.pest
+++ b/sbroad-core/src/frontend/sql/query.pest
@@ -1,4 +1,4 @@
-Command = _{ SOI ~ (Query | ExplainQuery | Block | DDL | ACL) ~ EOF }
+Command = _{ SOI ~ (Query | ExplainQuery | Block | DDL | ACL | EmptyQuery) ~ EOF }
 
 // Helper rule to denote we have to update plan relations from metadata
 // (with Table which name corresponds to current node).
@@ -261,6 +261,8 @@ Identifier = @{ DelimitedIdentifier | RegularIdentifier  }
                         | ^"limit"
                         }
 
+EmptyQuery = { WHITESPACE* }
+
 Expr = { ExprAtomValue ~ (ExprInfixOp ~ ExprAtomValue)* }
     ExprInfixOp = _{ Between | ArithInfixOp | CmpInfixOp | ConcatInfixOp | And | Or }
         Between       = { NotFlag? ~ ^"between" }
diff --git a/sbroad-core/src/ir.rs b/sbroad-core/src/ir.rs
index 765399f43..743e97d76 100644
--- a/sbroad-core/src/ir.rs
+++ b/sbroad-core/src/ir.rs
@@ -854,7 +854,12 @@ impl Plan {
     /// Constructor for an empty plan structure.
     #[must_use]
     pub fn new() -> Self {
-        Plan {
+        Self::empty()
+    }
+
+    /// Construct an empty plan.
+    pub fn empty() -> Self {
+        Self {
             nodes: Nodes {
                 arena32: Vec::new(),
                 arena64: Vec::new(),
-- 
GitLab