[NO ISSUE][API] Add Multi-Statement Parameter to HTTP API

- user model changes: no
- storage format changes: no
- interface changes: no

Details:
- Add new query service http API parameter to
  enable or disble multiple statements and default
  it to enabled.
- When multi-statement is disabled, only the following
  statements are allowed to appear multiple times:
  USE, SET, DECLARE, and WRITE.
- Extract RequestParameters class out of
  QueryServiceServlet.
- Clean test cases left dataverses one at a time
  in test framework.

Change-Id: I5bf2d9fc5fec351565b09cfef504539d5a938af5
Reviewed-on: https://asterix-gerrit.ics.uci.edu/2736
Reviewed-by: Murtadha Hubail <mhubail@apache.org>
Sonar-Qube: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Contrib: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: Till Westmann <tillw@apache.org>
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/IRequestParameters.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/IRequestParameters.java
index d58d761..0dbd3aa 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/IRequestParameters.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/IRequestParameters.java
@@ -62,4 +62,9 @@
      * @return Statement parameters
      */
     Map<String, IAObject> getStatementParameters();
+
+    /**
+     * @return true if the request accepts multiple statements. Otherwise, false.
+     */
+    boolean isMultiStatement();
 }
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/ApiServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/ApiServlet.java
index 86cac25..466757e 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/ApiServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/ApiServlet.java
@@ -164,7 +164,7 @@
             long startTime = System.currentTimeMillis();
             final IRequestParameters requestParameters =
                     new RequestParameters(hds, new ResultProperties(IStatementExecutor.ResultDelivery.IMMEDIATE),
-                            new IStatementExecutor.Stats(), null, null, null, null);
+                            new IStatementExecutor.Stats(), null, null, null, null, true);
             translator.compileAndExecute(hcc, null, requestParameters);
             long endTime = System.currentTimeMillis();
             duration = (endTime - startTime) / 1000.00;
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
index 9655f57..3150cb2 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
@@ -70,7 +70,7 @@
 
     @Override
     protected void executeStatement(String statementsText, SessionOutput sessionOutput,
-            ResultProperties resultProperties, IStatementExecutor.Stats stats, RequestParameters param,
+            ResultProperties resultProperties, IStatementExecutor.Stats stats, QueryServiceRequestParameters param,
             RequestExecutionState execution, Map<String, String> optionalParameters,
             Map<String, byte[]> statementParameters) throws Exception {
         // Running on NC -> send 'execute' message to CC
@@ -79,31 +79,31 @@
         final IStatementExecutor.ResultDelivery delivery = resultProperties.getDelivery();
         ExecuteStatementResponseMessage responseMsg;
         MessageFuture responseFuture = ncMb.registerMessageFuture();
-        final String handleUrl = getHandleUrl(param.host, param.path, delivery);
+        final String handleUrl = getHandleUrl(param.getHost(), param.getPath(), delivery);
         try {
-            if (param.clientContextID == null) {
-                param.clientContextID = UUID.randomUUID().toString();
+            if (param.getClientContextID() == null) {
+                param.setClientContextID(UUID.randomUUID().toString());
             }
             long timeout = ExecuteStatementRequestMessage.DEFAULT_NC_TIMEOUT_MILLIS;
-            if (param.timeout != null && !param.timeout.trim().isEmpty()) {
-                timeout = TimeUnit.NANOSECONDS.toMillis(Duration.parseDurationStringToNanos(param.timeout));
+            if (param.getTimeout() != null && !param.getTimeout().trim().isEmpty()) {
+                timeout = TimeUnit.NANOSECONDS.toMillis(Duration.parseDurationStringToNanos(param.getTimeout()));
             }
-            ExecuteStatementRequestMessage requestMsg =
-                    new ExecuteStatementRequestMessage(ncCtx.getNodeId(), responseFuture.getFutureId(), queryLanguage,
-                            statementsText, sessionOutput.config(), resultProperties.getNcToCcResultProperties(),
-                            param.clientContextID, handleUrl, optionalParameters, statementParameters);
+            ExecuteStatementRequestMessage requestMsg = new ExecuteStatementRequestMessage(ncCtx.getNodeId(),
+                    responseFuture.getFutureId(), queryLanguage, statementsText, sessionOutput.config(),
+                    resultProperties.getNcToCcResultProperties(), param.getClientContextID(), handleUrl,
+                    optionalParameters, statementParameters, param.isMultiStatement());
             execution.start();
             ncMb.sendMessageToPrimaryCC(requestMsg);
             try {
                 responseMsg = (ExecuteStatementResponseMessage) responseFuture.get(timeout, TimeUnit.MILLISECONDS);
             } catch (InterruptedException e) {
-                cancelQuery(ncMb, ncCtx.getNodeId(), param.clientContextID, e, false);
+                cancelQuery(ncMb, ncCtx.getNodeId(), param.getClientContextID(), e, false);
                 throw e;
             } catch (TimeoutException exception) {
                 RuntimeDataException hde = new RuntimeDataException(ErrorCode.QUERY_TIMEOUT);
                 hde.addSuppressed(exception);
                 // cancel query
-                cancelQuery(ncMb, ncCtx.getNodeId(), param.clientContextID, hde, true);
+                cancelQuery(ncMb, ncCtx.getNodeId(), param.getClientContextID(), hde, true);
                 throw hde;
             }
             execution.end();
@@ -157,7 +157,8 @@
     }
 
     @Override
-    protected void handleExecuteStatementException(Throwable t, RequestExecutionState state, RequestParameters param) {
+    protected void handleExecuteStatementException(Throwable t, RequestExecutionState state,
+            QueryServiceRequestParameters param) {
         if (t instanceof TimeoutException // TODO(mblow): I don't think t can ever been an instance of TimeoutException
                 || ExceptionUtils.matchingCause(t, candidate -> candidate instanceof IPCException)) {
             GlobalConfig.ASTERIX_LOGGER.log(Level.WARN, t.toString(), t);
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceRequestParameters.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceRequestParameters.java
new file mode 100644
index 0000000..d0c59b5
--- /dev/null
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceRequestParameters.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.asterix.api.http.server;
+
+import java.util.Map;
+
+import org.apache.hyracks.util.JSONUtil;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public class QueryServiceRequestParameters {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private String host;
+    private String path;
+    private String statement;
+    private String format;
+    private String timeout;
+    private boolean pretty;
+    private String clientContextID;
+    private String mode;
+    private String maxResultReads;
+    private String planFormat;
+    private Map<String, JsonNode> statementParams;
+    private boolean expressionTree;
+    private boolean rewrittenExpressionTree;
+    private boolean logicalPlan;
+    private boolean optimizedLogicalPlan;
+    private boolean job;
+    private boolean signature;
+    private boolean multiStatement;
+
+    public String getHost() {
+        return host;
+    }
+
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    public String getStatement() {
+        return statement;
+    }
+
+    public void setStatement(String statement) {
+        this.statement = statement;
+    }
+
+    public String getFormat() {
+        return format;
+    }
+
+    public void setFormat(String format) {
+        this.format = format;
+    }
+
+    public String getTimeout() {
+        return timeout;
+    }
+
+    public void setTimeout(String timeout) {
+        this.timeout = timeout;
+    }
+
+    public boolean isPretty() {
+        return pretty;
+    }
+
+    public void setPretty(boolean pretty) {
+        this.pretty = pretty;
+    }
+
+    public String getClientContextID() {
+        return clientContextID;
+    }
+
+    public void setClientContextID(String clientContextID) {
+        this.clientContextID = clientContextID;
+    }
+
+    public String getMode() {
+        return mode;
+    }
+
+    public void setMode(String mode) {
+        this.mode = mode;
+    }
+
+    public String getMaxResultReads() {
+        return maxResultReads;
+    }
+
+    public void setMaxResultReads(String maxResultReads) {
+        this.maxResultReads = maxResultReads;
+    }
+
+    public String getPlanFormat() {
+        return planFormat;
+    }
+
+    public void setPlanFormat(String planFormat) {
+        this.planFormat = planFormat;
+    }
+
+    public Map<String, JsonNode> getStatementParams() {
+        return statementParams;
+    }
+
+    public void setStatementParams(Map<String, JsonNode> statementParams) {
+        this.statementParams = statementParams;
+    }
+
+    public boolean isExpressionTree() {
+        return expressionTree;
+    }
+
+    public void setExpressionTree(boolean expressionTree) {
+        this.expressionTree = expressionTree;
+    }
+
+    public boolean isRewrittenExpressionTree() {
+        return rewrittenExpressionTree;
+    }
+
+    public void setRewrittenExpressionTree(boolean rewrittenExpressionTree) {
+        this.rewrittenExpressionTree = rewrittenExpressionTree;
+    }
+
+    public boolean isLogicalPlan() {
+        return logicalPlan;
+    }
+
+    public void setLogicalPlan(boolean logicalPlan) {
+        this.logicalPlan = logicalPlan;
+    }
+
+    public boolean isOptimizedLogicalPlan() {
+        return optimizedLogicalPlan;
+    }
+
+    public void setOptimizedLogicalPlan(boolean optimizedLogicalPlan) {
+        this.optimizedLogicalPlan = optimizedLogicalPlan;
+    }
+
+    public boolean isJob() {
+        return job;
+    }
+
+    public void setJob(boolean job) {
+        this.job = job;
+    }
+
+    public boolean isSignature() {
+        return signature;
+    }
+
+    public void setSignature(boolean signature) {
+        this.signature = signature;
+    }
+
+    public boolean isMultiStatement() {
+        return multiStatement;
+    }
+
+    public void setMultiStatement(boolean multiStatement) {
+        this.multiStatement = multiStatement;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            ObjectNode on = OBJECT_MAPPER.createObjectNode();
+            on.put("host", host);
+            on.put("path", path);
+            on.put("statement", JSONUtil.escape(new StringBuilder(), statement).toString());
+            on.put("pretty", pretty);
+            on.put("mode", mode);
+            on.put("clientContextID", clientContextID);
+            on.put("format", format);
+            on.put("timeout", timeout);
+            on.put("maxResultReads", maxResultReads);
+            on.put("planFormat", planFormat);
+            on.put("expressionTree", expressionTree);
+            on.put("rewrittenExpressionTree", rewrittenExpressionTree);
+            on.put("logicalPlan", logicalPlan);
+            on.put("optimizedLogicalPlan", optimizedLogicalPlan);
+            on.put("job", job);
+            on.put("signature", signature);
+            on.put("multiStatement", multiStatement);
+            if (statementParams != null) {
+                for (Map.Entry<String, JsonNode> statementParam : statementParams.entrySet()) {
+                    on.set('$' + statementParam.getKey(), statementParam.getValue());
+                }
+            }
+            return OBJECT_MAPPER.writeValueAsString(on);
+        } catch (JsonProcessingException e) {
+            QueryServiceServlet.LOGGER.debug("unexpected exception marshalling {} instance to json", getClass(), e);
+            return e.toString();
+        }
+    }
+}
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
index a52973c..3d1175c 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
@@ -67,18 +67,14 @@
 import org.apache.hyracks.http.api.IServletRequest;
 import org.apache.hyracks.http.api.IServletResponse;
 import org.apache.hyracks.http.server.utils.HttpUtil;
-import org.apache.hyracks.util.JSONUtil;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
 import com.fasterxml.jackson.databind.JsonMappingException;
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
+
 import io.netty.handler.codec.http.HttpResponseStatus;
 
 public class QueryServiceServlet extends AbstractQueryApiServlet {
@@ -154,7 +150,8 @@
         LOGICAL_PLAN("logical-plan"),
         OPTIMIZED_LOGICAL_PLAN("optimized-logical-plan"),
         JOB("job"),
-        SIGNATURE("signature");
+        SIGNATURE("signature"),
+        MULTI_STATEMENT("multi-statement");
 
         private final String str;
 
@@ -201,59 +198,6 @@
         }
     }
 
-    protected static class RequestParameters {
-        String host;
-        String path;
-        String statement;
-        String format;
-        String timeout;
-        boolean pretty;
-        String clientContextID;
-        String mode;
-        String maxResultReads;
-        String planFormat;
-        Map<String, JsonNode> statementParams;
-        boolean expressionTree;
-        boolean rewrittenExpressionTree;
-        boolean logicalPlan;
-        boolean optimizedLogicalPlan;
-        boolean job;
-        boolean signature;
-
-        @Override
-        public String toString() {
-            try {
-                ObjectMapper om = new ObjectMapper();
-                ObjectNode on = om.createObjectNode();
-                on.put("host", host);
-                on.put("path", path);
-                on.put("statement", JSONUtil.escape(new StringBuilder(), statement).toString());
-                on.put("pretty", pretty);
-                on.put("mode", mode);
-                on.put("clientContextID", clientContextID);
-                on.put("format", format);
-                on.put("timeout", timeout);
-                on.put("maxResultReads", maxResultReads);
-                on.put("planFormat", planFormat);
-                on.put("expressionTree", expressionTree);
-                on.put("rewrittenExpressionTree", rewrittenExpressionTree);
-                on.put("logicalPlan", logicalPlan);
-                on.put("optimizedLogicalPlan", optimizedLogicalPlan);
-                on.put("job", job);
-                on.put("signature", signature);
-                if (statementParams != null) {
-                    for (Map.Entry<String, JsonNode> statementParam : statementParams.entrySet()) {
-                        on.set('$' + statementParam.getKey(), statementParam.getValue());
-                    }
-                }
-                return om.writer(new MinimalPrettyPrinter()).writeValueAsString(on);
-            } catch (JsonProcessingException e) { // NOSONAR
-                LOGGER.debug("unexpected exception marshalling {} instance to json", getClass(), e);
-                return e.toString();
-            }
-        }
-    }
-
     protected static final class RequestExecutionState {
         private long execStart = -1;
         private long execEnd = -1;
@@ -332,39 +276,39 @@
         return SessionConfig.OutputFormat.CLEAN_JSON;
     }
 
-    private static SessionOutput createSessionOutput(RequestParameters param, String handleUrl,
+    private static SessionOutput createSessionOutput(QueryServiceRequestParameters param, String handleUrl,
             PrintWriter resultWriter) {
         SessionOutput.ResultDecorator resultPrefix = ResultUtil.createPreResultDecorator();
         SessionOutput.ResultDecorator resultPostfix = ResultUtil.createPostResultDecorator();
         SessionOutput.ResultAppender appendHandle = ResultUtil.createResultHandleAppender(handleUrl);
         SessionOutput.ResultAppender appendStatus = ResultUtil.createResultStatusAppender();
 
-        SessionConfig.OutputFormat format = getFormat(param.format);
-        final SessionConfig.PlanFormat planFormat =
-                SessionConfig.PlanFormat.get(param.planFormat, param.planFormat, SessionConfig.PlanFormat.JSON, LOGGER);
+        SessionConfig.OutputFormat format = getFormat(param.getFormat());
+        final SessionConfig.PlanFormat planFormat = SessionConfig.PlanFormat.get(param.getPlanFormat(),
+                param.getPlanFormat(), SessionConfig.PlanFormat.JSON, LOGGER);
         SessionConfig sessionConfig = new SessionConfig(format, planFormat);
         sessionConfig.set(SessionConfig.FORMAT_WRAPPER_ARRAY, true);
-        sessionConfig.set(SessionConfig.OOB_EXPR_TREE, param.expressionTree);
-        sessionConfig.set(SessionConfig.OOB_REWRITTEN_EXPR_TREE, param.rewrittenExpressionTree);
-        sessionConfig.set(SessionConfig.OOB_LOGICAL_PLAN, param.logicalPlan);
-        sessionConfig.set(SessionConfig.OOB_OPTIMIZED_LOGICAL_PLAN, param.optimizedLogicalPlan);
-        sessionConfig.set(SessionConfig.OOB_HYRACKS_JOB, param.job);
-        sessionConfig.set(SessionConfig.FORMAT_INDENT_JSON, param.pretty);
+        sessionConfig.set(SessionConfig.OOB_EXPR_TREE, param.isExpressionTree());
+        sessionConfig.set(SessionConfig.OOB_REWRITTEN_EXPR_TREE, param.isRewrittenExpressionTree());
+        sessionConfig.set(SessionConfig.OOB_LOGICAL_PLAN, param.isLogicalPlan());
+        sessionConfig.set(SessionConfig.OOB_OPTIMIZED_LOGICAL_PLAN, param.isOptimizedLogicalPlan());
+        sessionConfig.set(SessionConfig.OOB_HYRACKS_JOB, param.isJob());
+        sessionConfig.set(SessionConfig.FORMAT_INDENT_JSON, param.isPretty());
         sessionConfig.set(SessionConfig.FORMAT_QUOTE_RECORD,
                 format != SessionConfig.OutputFormat.CLEAN_JSON && format != SessionConfig.OutputFormat.LOSSLESS_JSON);
         sessionConfig.set(SessionConfig.FORMAT_CSV_HEADER, format == SessionConfig.OutputFormat.CSV
-                && "present".equals(getParameterValue(param.format, Attribute.HEADER.str())));
+                && "present".equals(getParameterValue(param.getFormat(), Attribute.HEADER.str())));
         return new SessionOutput(sessionConfig, resultWriter, resultPrefix, resultPostfix, appendHandle, appendStatus);
     }
 
-    private static void printClientContextID(PrintWriter pw, RequestParameters params) {
-        if (params.clientContextID != null && !params.clientContextID.isEmpty()) {
-            ResultUtil.printField(pw, ResultFields.CLIENT_ID.str(), params.clientContextID);
+    private static void printClientContextID(PrintWriter pw, QueryServiceRequestParameters params) {
+        if (params.getClientContextID() != null && !params.getClientContextID().isEmpty()) {
+            ResultUtil.printField(pw, ResultFields.CLIENT_ID.str(), params.getClientContextID());
         }
     }
 
-    private static void printSignature(PrintWriter pw, RequestParameters param) {
-        if (param.signature) {
+    private static void printSignature(PrintWriter pw, QueryServiceRequestParameters param) {
+        if (param.isSignature()) {
             pw.print("\t\"");
             pw.print(ResultFields.SIGNATURE.str());
             pw.print("\": {\n");
@@ -457,50 +401,54 @@
         return result;
     }
 
-    private RequestParameters getRequestParameters(IServletRequest request) throws IOException {
+    private QueryServiceRequestParameters getRequestParameters(IServletRequest request) throws IOException {
         final String contentType = HttpUtil.getContentTypeOnly(request);
-        RequestParameters param = new RequestParameters();
-        param.host = host(request);
-        param.path = servletPath(request);
+        QueryServiceRequestParameters param = new QueryServiceRequestParameters();
+        param.setHost(host(request));
+        param.setPath(servletPath(request));
         if (HttpUtil.ContentType.APPLICATION_JSON.equals(contentType)) {
             try {
                 JsonNode jsonRequest = OBJECT_MAPPER.readTree(HttpUtil.getRequestBody(request));
-                param.statement = jsonRequest.get(Parameter.STATEMENT.str()).asText();
-                param.format = toLower(getOptText(jsonRequest, Parameter.FORMAT.str()));
-                param.pretty = getOptBoolean(jsonRequest, Parameter.PRETTY.str(), false);
-                param.mode = toLower(getOptText(jsonRequest, Parameter.MODE.str()));
-                param.clientContextID = getOptText(jsonRequest, Parameter.CLIENT_ID.str());
-                param.timeout = getOptText(jsonRequest, Parameter.TIMEOUT.str());
-                param.maxResultReads = getOptText(jsonRequest, Parameter.MAX_RESULT_READS.str());
-                param.planFormat = getOptText(jsonRequest, Parameter.PLAN_FORMAT.str());
-                param.expressionTree = getOptBoolean(jsonRequest, Parameter.EXPRESSION_TREE.str(), false);
-                param.rewrittenExpressionTree =
-                        getOptBoolean(jsonRequest, Parameter.REWRITTEN_EXPRESSION_TREE.str(), false);
-                param.logicalPlan = getOptBoolean(jsonRequest, Parameter.LOGICAL_PLAN.str(), false);
-                param.optimizedLogicalPlan = getOptBoolean(jsonRequest, Parameter.OPTIMIZED_LOGICAL_PLAN.str(), false);
-                param.job = getOptBoolean(jsonRequest, Parameter.JOB.str(), false);
-                param.signature = getOptBoolean(jsonRequest, Parameter.SIGNATURE.str(), true);
-                param.statementParams =
-                        getOptStatementParameters(jsonRequest, jsonRequest.fieldNames(), JsonNode::get, v -> v);
+                param.setStatement(jsonRequest.get(Parameter.STATEMENT.str()).asText());
+                param.setFormat(toLower(getOptText(jsonRequest, Parameter.FORMAT.str())));
+                param.setPretty(getOptBoolean(jsonRequest, Parameter.PRETTY.str(), false));
+                param.setMode(toLower(getOptText(jsonRequest, Parameter.MODE.str())));
+                param.setClientContextID(getOptText(jsonRequest, Parameter.CLIENT_ID.str()));
+                param.setTimeout(getOptText(jsonRequest, Parameter.TIMEOUT.str()));
+                param.setMaxResultReads(getOptText(jsonRequest, Parameter.MAX_RESULT_READS.str()));
+                param.setPlanFormat(getOptText(jsonRequest, Parameter.PLAN_FORMAT.str()));
+                param.setExpressionTree(getOptBoolean(jsonRequest, Parameter.EXPRESSION_TREE.str(), false));
+                param.setRewrittenExpressionTree(
+                        getOptBoolean(jsonRequest, Parameter.REWRITTEN_EXPRESSION_TREE.str(), false));
+                param.setLogicalPlan(getOptBoolean(jsonRequest, Parameter.LOGICAL_PLAN.str(), false));
+                param.setOptimizedLogicalPlan(
+                        getOptBoolean(jsonRequest, Parameter.OPTIMIZED_LOGICAL_PLAN.str(), false));
+                param.setJob(getOptBoolean(jsonRequest, Parameter.JOB.str(), false));
+                param.setSignature(getOptBoolean(jsonRequest, Parameter.SIGNATURE.str(), true));
+                param.setStatementParams(
+                        getOptStatementParameters(jsonRequest, jsonRequest.fieldNames(), JsonNode::get, v -> v));
+                param.setMultiStatement(getOptBoolean(jsonRequest, Parameter.MULTI_STATEMENT.str(), true));
             } catch (JsonParseException | JsonMappingException e) {
                 // if the JSON parsing fails, the statement is empty and we get an empty statement error
                 GlobalConfig.ASTERIX_LOGGER.log(Level.ERROR, e.getMessage(), e);
             }
         } else {
-            param.statement = request.getParameter(Parameter.STATEMENT.str());
-            if (param.statement == null) {
-                param.statement = HttpUtil.getRequestBody(request);
+            param.setStatement(request.getParameter(Parameter.STATEMENT.str()));
+            if (param.getStatement() == null) {
+                param.setStatement(HttpUtil.getRequestBody(request));
             }
-            param.format = toLower(request.getParameter(Parameter.FORMAT.str()));
-            param.pretty = Boolean.parseBoolean(request.getParameter(Parameter.PRETTY.str()));
-            param.mode = toLower(request.getParameter(Parameter.MODE.str()));
-            param.clientContextID = request.getParameter(Parameter.CLIENT_ID.str());
-            param.timeout = request.getParameter(Parameter.TIMEOUT.str());
-            param.maxResultReads = request.getParameter(Parameter.MAX_RESULT_READS.str());
-            param.planFormat = request.getParameter(Parameter.PLAN_FORMAT.str());
+            param.setFormat(toLower(request.getParameter(Parameter.FORMAT.str())));
+            param.setPretty(Boolean.parseBoolean(request.getParameter(Parameter.PRETTY.str())));
+            param.setMode(toLower(request.getParameter(Parameter.MODE.str())));
+            param.setClientContextID(request.getParameter(Parameter.CLIENT_ID.str()));
+            param.setTimeout(request.getParameter(Parameter.TIMEOUT.str()));
+            param.setMaxResultReads(request.getParameter(Parameter.MAX_RESULT_READS.str()));
+            param.setPlanFormat(request.getParameter(Parameter.PLAN_FORMAT.str()));
+            final String multiStatementParam = request.getParameter(Parameter.MULTI_STATEMENT.str());
+            param.setMultiStatement(multiStatementParam == null || Boolean.parseBoolean(multiStatementParam));
             try {
-                param.statementParams = getOptStatementParameters(request, request.getParameterNames().iterator(),
-                        IServletRequest::getParameter, OBJECT_MAPPER::readTree);
+                param.setStatementParams(getOptStatementParameters(request, request.getParameterNames().iterator(),
+                        IServletRequest::getParameter, OBJECT_MAPPER::readTree));
             } catch (JsonParseException | JsonMappingException e) {
                 GlobalConfig.ASTERIX_LOGGER.log(Level.ERROR, e.getMessage(), e);
             }
@@ -548,17 +496,17 @@
     }
 
     private void handleRequest(IServletRequest request, IServletResponse response) throws IOException {
-        RequestParameters param = getRequestParameters(request);
+        QueryServiceRequestParameters param = getRequestParameters(request);
         LOGGER.info("handleRequest: {}", param);
         long elapsedStart = System.nanoTime();
         final PrintWriter httpWriter = response.writer();
 
-        ResultDelivery delivery = parseResultDelivery(param.mode);
+        ResultDelivery delivery = parseResultDelivery(param.getMode());
 
-        final ResultProperties resultProperties = param.maxResultReads == null ? new ResultProperties(delivery)
-                : new ResultProperties(delivery, Long.parseLong(param.maxResultReads));
+        final ResultProperties resultProperties = param.getMaxResultReads() == null ? new ResultProperties(delivery)
+                : new ResultProperties(delivery, Long.parseLong(param.getMaxResultReads()));
 
-        String handleUrl = getHandleUrl(param.host, param.path, delivery);
+        String handleUrl = getHandleUrl(param.getHost(), param.getPath(), delivery);
         SessionOutput sessionOutput = createSessionOutput(param, handleUrl, httpWriter);
         SessionConfig sessionConfig = sessionOutput.config();
         HttpUtil.setContentType(response, HttpUtil.ContentType.APPLICATION_JSON, HttpUtil.Encoding.UTF8);
@@ -575,16 +523,16 @@
         printType(sessionOutput.out(), sessionConfig);
         long errorCount = 1; // so far we just return 1 error
         try {
-            if (param.statement == null || param.statement.isEmpty()) {
+            if (param.getStatement() == null || param.getStatement().isEmpty()) {
                 throw new AsterixException("Empty request, no statement provided");
             }
-            String statementsText = param.statement + ";";
+            String statementsText = param.getStatement() + ";";
             Map<String, String> optionalParams = null;
             if (optionalParamProvider != null) {
                 optionalParams = optionalParamProvider.apply(request);
             }
-            Map<String, byte[]> statementParams =
-                    org.apache.asterix.app.translator.RequestParameters.serializeParameterValues(param.statementParams);
+            Map<String, byte[]> statementParams = org.apache.asterix.app.translator.RequestParameters
+                    .serializeParameterValues(param.getStatementParams());
             // CORS
             response.setHeader("Access-Control-Allow-Origin",
                     "http://" + hostName + ":" + appCtx.getExternalProperties().getQueryWebInterfacePort());
@@ -616,8 +564,9 @@
     }
 
     protected void executeStatement(String statementsText, SessionOutput sessionOutput,
-            ResultProperties resultProperties, Stats stats, RequestParameters param, RequestExecutionState execution,
-            Map<String, String> optionalParameters, Map<String, byte[]> statementParameters) throws Exception {
+            ResultProperties resultProperties, Stats stats, QueryServiceRequestParameters param,
+            RequestExecutionState execution, Map<String, String> optionalParameters,
+            Map<String, byte[]> statementParameters) throws Exception {
         IClusterManagementWork.ClusterState clusterState =
                 ((ICcApplicationContext) appCtx).getClusterStateManager().getState();
         if (clusterState != IClusterManagementWork.ClusterState.ACTIVE) {
@@ -634,13 +583,14 @@
                 org.apache.asterix.app.translator.RequestParameters.deserializeParameterValues(statementParameters);
         IRequestParameters requestParameters =
                 new org.apache.asterix.app.translator.RequestParameters(getHyracksDataset(), resultProperties, stats,
-                        null, param.clientContextID, optionalParameters, stmtParams);
+                        null, param.getClientContextID(), optionalParameters, stmtParams, param.isMultiStatement());
         translator.compileAndExecute(getHyracksClientConnection(), queryCtx, requestParameters);
         execution.end();
         printExecutionPlans(sessionOutput, translator.getExecutionPlans());
     }
 
-    protected void handleExecuteStatementException(Throwable t, RequestExecutionState state, RequestParameters param) {
+    protected void handleExecuteStatementException(Throwable t, RequestExecutionState state,
+            QueryServiceRequestParameters param) {
         if (t instanceof org.apache.asterix.aqlplus.parser.TokenMgrError || t instanceof TokenMgrError
                 || t instanceof AlgebricksException) {
             if (LOGGER.isDebugEnabled()) {
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/RestApiServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/RestApiServlet.java
index 40095d7..3c58bc6 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/RestApiServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/RestApiServlet.java
@@ -210,7 +210,7 @@
             IStatementExecutor translator = statementExecutorFactory.create(appCtx, aqlStatements, sessionOutput,
                     compilationProvider, componentProvider);
             final IRequestParameters requestParameters = new RequestParameters(hds,
-                    new ResultProperties(resultDelivery), new IStatementExecutor.Stats(), null, null, null, null);
+                    new ResultProperties(resultDelivery), new IStatementExecutor.Stats(), null, null, null, null, true);
             translator.compileAndExecute(hcc, null, requestParameters);
         } catch (AsterixException | TokenMgrError | org.apache.asterix.aqlplus.parser.TokenMgrError pe) {
             response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/java/AsterixJavaClient.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/java/AsterixJavaClient.java
index 2d3a2f6..71b4b81 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/java/AsterixJavaClient.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/java/AsterixJavaClient.java
@@ -128,7 +128,7 @@
                 storageComponentProvider);
         final IRequestParameters requestParameters =
                 new RequestParameters(null, new ResultProperties(IStatementExecutor.ResultDelivery.IMMEDIATE),
-                        new IStatementExecutor.Stats(), null, null, null, statementParams);
+                        new IStatementExecutor.Stats(), null, null, null, statementParams, true);
         translator.compileAndExecute(hcc, null, requestParameters);
         writer.flush();
     }
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/message/ExecuteStatementRequestMessage.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/message/ExecuteStatementRequestMessage.java
index ce259a2..88b5da8 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/message/ExecuteStatementRequestMessage.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/message/ExecuteStatementRequestMessage.java
@@ -78,11 +78,12 @@
     private final String handleUrl;
     private final Map<String, String> optionalParameters;
     private final Map<String, byte[]> statementParameters;
+    private final boolean multiStatement;
 
     public ExecuteStatementRequestMessage(String requestNodeId, long requestMessageId, ILangExtension.Language lang,
             String statementsText, SessionConfig sessionConfig, ResultProperties resultProperties,
             String clientContextID, String handleUrl, Map<String, String> optionalParameters,
-            Map<String, byte[]> statementParameters) {
+            Map<String, byte[]> statementParameters, boolean multiStatement) {
         this.requestNodeId = requestNodeId;
         this.requestMessageId = requestMessageId;
         this.lang = lang;
@@ -93,6 +94,7 @@
         this.handleUrl = handleUrl;
         this.optionalParameters = optionalParameters;
         this.statementParameters = statementParameters;
+        this.multiStatement = multiStatement;
     }
 
     @Override
@@ -130,7 +132,7 @@
             final IStatementExecutor.Stats stats = new IStatementExecutor.Stats();
             Map<String, IAObject> stmtParams = RequestParameters.deserializeParameterValues(statementParameters);
             final IRequestParameters requestParameters = new RequestParameters(null, resultProperties, stats,
-                    outMetadata, clientContextID, optionalParameters, stmtParams);
+                    outMetadata, clientContextID, optionalParameters, stmtParams, multiStatement);
             translator.compileAndExecute(ccApp.getHcc(), statementExecutorContext, requestParameters);
             outPrinter.close();
             responseMsg.setResult(outWriter.toString());
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
index 4d71715..469a2dd 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
@@ -65,6 +65,7 @@
 import org.apache.asterix.common.exceptions.CompilationException;
 import org.apache.asterix.common.exceptions.ErrorCode;
 import org.apache.asterix.common.exceptions.MetadataException;
+import org.apache.asterix.common.exceptions.RuntimeDataException;
 import org.apache.asterix.common.functions.FunctionSignature;
 import org.apache.asterix.common.utils.JobUtils;
 import org.apache.asterix.common.utils.JobUtils.ProgressState;
@@ -263,6 +264,9 @@
     @Override
     public void compileAndExecute(IHyracksClientConnection hcc, IStatementExecutorContext ctx,
             IRequestParameters requestParameters) throws Exception {
+        if (!requestParameters.isMultiStatement()) {
+            validateStatements(statements);
+        }
         int resultSetIdCounter = 0;
         FileSplit outputFile = null;
         IAWriterFactory writerFactory = PrinterBasedWriterFactory.INSTANCE;
@@ -2940,6 +2944,24 @@
         }
     }
 
+    protected void validateStatements(List<Statement> statements) throws RuntimeDataException {
+        if (statements.stream().filter(this::isNotAllowedMultiStatement).count() > 1) {
+            throw new RuntimeDataException(ErrorCode.UNSUPPORTED_MULTIPLE_STATEMENTS);
+        }
+    }
+
+    protected boolean isNotAllowedMultiStatement(Statement statement) {
+        switch (statement.getKind()) {
+            case DATAVERSE_DECL:
+            case FUNCTION_DECL:
+            case SET:
+            case WRITE:
+                return false;
+            default:
+                return true;
+        }
+    }
+
     private Map<VarIdentifier, IAObject> createExternalVariables(Map<String, IAObject> stmtParams,
             IStatementRewriter stmtRewriter) {
         if (stmtParams == null || stmtParams.isEmpty()) {
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/RequestParameters.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/RequestParameters.java
index 0655285..ad12125 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/RequestParameters.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/RequestParameters.java
@@ -48,10 +48,11 @@
     private final IStatementExecutor.ResultMetadata outMetadata;
     private final String clientContextId;
     private final Map<String, IAObject> statementParameters;
+    private final boolean multiStatement;
 
     public RequestParameters(IHyracksDataset hdc, ResultProperties resultProperties, Stats stats,
             IStatementExecutor.ResultMetadata outMetadata, String clientContextId,
-            Map<String, String> optionalParameters, Map<String, IAObject> statementParameters) {
+            Map<String, String> optionalParameters, Map<String, IAObject> statementParameters, boolean multiStatement) {
         this.hdc = hdc;
         this.resultProperties = resultProperties;
         this.stats = stats;
@@ -59,6 +60,7 @@
         this.clientContextId = clientContextId;
         this.optionalParameters = optionalParameters;
         this.statementParameters = statementParameters;
+        this.multiStatement = multiStatement;
     }
 
     @Override
@@ -92,6 +94,11 @@
     }
 
     @Override
+    public boolean isMultiStatement() {
+        return multiStatement;
+    }
+
+    @Override
     public Map<String, IAObject> getStatementParameters() {
         return statementParameters;
     }
diff --git a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java
index 2281238..3c51775 100644
--- a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java
+++ b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java
@@ -1786,13 +1786,14 @@
                 LOGGER.info("Last test left some garbage. Dropping dataverses: " + StringUtils.join(toBeDropped, ','));
                 StringBuilder dropStatement = new StringBuilder();
                 for (String dv : toBeDropped) {
+                    dropStatement.setLength(0);
                     dropStatement.append("drop dataverse ");
                     dropStatement.append(dv);
                     dropStatement.append(";\n");
+                    resultStream = executeQueryService(dropStatement.toString(), getEndpoint(Servlets.QUERY_SERVICE),
+                            OutputFormat.CLEAN_JSON);
+                    ResultExtractor.extract(resultStream);
                 }
-                resultStream = executeQueryService(dropStatement.toString(), getEndpoint(Servlets.QUERY_SERVICE),
-                        OutputFormat.CLEAN_JSON);
-                ResultExtractor.extract(resultStream);
             }
         } catch (Throwable th) {
             th.printStackTrace();
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
index d919d0e..e0c0d78 100644
--- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
@@ -75,6 +75,7 @@
     public static final int REJECT_BAD_CLUSTER_STATE = 32;
     public static final int REJECT_NODE_UNREGISTERED = 33;
     public static final int DIVISION_BY_ZERO = 34;
+    public static final int UNSUPPORTED_MULTIPLE_STATEMENTS = 35;
 
     public static final int UNSUPPORTED_JRE = 100;
 
diff --git a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
index ac51570..d600762 100644
--- a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
+++ b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
@@ -68,6 +68,7 @@
 32 = Cannot execute request, cluster is %1$s
 33 = Node is not registered with the CC
 34 = Division by Zero.
+35 = Unsupported multiple statements.
 
 100 = Unsupported JRE: %1$s