[NO ISSUE][API] Introduce 'readonly' request parameter

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

Details:
- Introduce 'readonly' request parameter which allows user
  to specify whether DDL / DML statements must be rejected
  (if set to 'true') or allowed ('false' - default)
- Add test cases and update documentation
- Fix category of WRITE and INSERT statements

Change-Id: Ia2555483f431f97c10d922d2a8832bace6a97610
Reviewed-on: https://asterix-gerrit.ics.uci.edu/3543
Reviewed-by: Murtadha Hubail <mhubail@apache.org>
Contrib: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
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 84c53c9..e242258 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
@@ -53,4 +53,11 @@
      * @return Statement parameters
      */
     Map<String, IAObject> getStatementParameters();
+
+    /**
+     * @return a bitmask that restricts which statement
+     *   {@link org.apache.asterix.lang.common.base.Statement.Category categories} are permitted for this request,
+     *   {@code 0} if all categories are allowed
+     */
+    int getStatementCategoryRestrictionMask();
 }
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 b1361d9..27e685c 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
@@ -86,10 +86,13 @@
             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.getClientContextID(), handleUrl,
-                    optionalParameters, statementParameters, param.isMultiStatement(), requestReference);
+            int stmtCategoryRestrictionMask = org.apache.asterix.app.translator.RequestParameters
+                    .getStatementCategoryRestrictionMask(param.isReadOnly());
+            ExecuteStatementRequestMessage requestMsg =
+                    new ExecuteStatementRequestMessage(ncCtx.getNodeId(), responseFuture.getFutureId(), queryLanguage,
+                            statementsText, sessionOutput.config(), resultProperties.getNcToCcResultProperties(),
+                            param.getClientContextID(), handleUrl, optionalParameters, statementParameters,
+                            param.isMultiStatement(), stmtCategoryRestrictionMask, requestReference);
             execution.start();
             ncMb.sendMessageToPrimaryCC(requestMsg);
             try {
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
index df321bc..566133d 100644
--- 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
@@ -45,6 +45,7 @@
     private Map<String, JsonNode> statementParams;
     private boolean expressionTree;
     private boolean parseOnly; //don't execute; simply check for syntax correctness and named parameters.
+    private boolean readOnly; // only allow statements that belong to QUERY category, fail for all other categories.
     private boolean rewrittenExpressionTree;
     private boolean logicalPlan;
     private boolean optimizedLogicalPlan;
@@ -180,6 +181,14 @@
         return parseOnly;
     }
 
+    public void setReadOnly(boolean readOnly) {
+        this.readOnly = readOnly;
+    }
+
+    public boolean isReadOnly() {
+        return readOnly;
+    }
+
     public boolean isJob() {
         return job;
     }
@@ -223,6 +232,8 @@
         object.put("job", job);
         object.put("signature", signature);
         object.put("multiStatement", multiStatement);
+        object.put("parseOnly", parseOnly);
+        object.put("readOnly", readOnly);
         if (statementParams != null) {
             for (Map.Entry<String, JsonNode> statementParam : statementParams.entrySet()) {
                 object.set('$' + statementParam.getKey(), statementParam.getValue());
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 db7075d..31f0f2b 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
@@ -58,6 +58,7 @@
 import org.apache.asterix.app.result.fields.TypePrinter;
 import org.apache.asterix.app.result.fields.WarningsPrinter;
 import org.apache.asterix.app.translator.QueryTranslator;
+import org.apache.asterix.app.translator.RequestParameters;
 import org.apache.asterix.common.api.IApplicationContext;
 import org.apache.asterix.common.api.IClusterManagementWork;
 import org.apache.asterix.common.api.ICodedMessage;
@@ -168,6 +169,7 @@
         LOGICAL_PLAN("logical-plan"),
         OPTIMIZED_LOGICAL_PLAN("optimized-logical-plan"),
         PARSE_ONLY("parse-only"),
+        READ_ONLY("readonly"),
         JOB("job"),
         SIGNATURE("signature"),
         MULTI_STATEMENT("multi-statement");
@@ -378,6 +380,7 @@
         param.setRewrittenExpressionTree(getOptBoolean(jsonRequest, Parameter.REWRITTEN_EXPRESSION_TREE, false));
         param.setLogicalPlan(getOptBoolean(jsonRequest, Parameter.LOGICAL_PLAN, false));
         param.setParseOnly(getOptBoolean(jsonRequest, Parameter.PARSE_ONLY, false));
+        param.setReadOnly(getOptBoolean(jsonRequest, Parameter.READ_ONLY, false));
         param.setOptimizedLogicalPlan(getOptBoolean(jsonRequest, Parameter.OPTIMIZED_LOGICAL_PLAN, false));
         param.setJob(getOptBoolean(jsonRequest, Parameter.JOB, false));
         param.setSignature(getOptBoolean(jsonRequest, Parameter.SIGNATURE, true));
@@ -403,6 +406,7 @@
         param.setMaxResultReads(getParameter(request, Parameter.MAX_RESULT_READS));
         param.setPlanFormat(getParameter(request, Parameter.PLAN_FORMAT));
         param.setParseOnly(getOptBoolean(request, Parameter.PARSE_ONLY, false));
+        param.setReadOnly(getOptBoolean(request, Parameter.READ_ONLY, false));
         param.setMultiStatement(getOptBoolean(request, Parameter.MULTI_STATEMENT, true));
         try {
             param.setStatementParams(getOptStatementParameters(request, request.getParameterNames().iterator(),
@@ -573,7 +577,7 @@
         IParserFactory factory = compilationProvider.getParserFactory();
         IParser parser = factory.createParser(statementsText);
         List<Statement> stmts = parser.parse();
-        QueryTranslator.validateStatements(stmts);
+        QueryTranslator.validateStatements(stmts, true, RequestParameters.NO_CATEGORY_RESTRICTION_MASK);
         Query query = (Query) stmts.get(stmts.size() - 1);
         Set<VariableExpr> extVars =
                 compilationProvider.getRewriterFactory().createQueryRewriter().getExternalVariables(query.getBody());
@@ -601,9 +605,11 @@
         execution.start();
         Map<String, IAObject> stmtParams =
                 org.apache.asterix.app.translator.RequestParameters.deserializeParameterValues(statementParameters);
+        int stmtCategoryRestriction = org.apache.asterix.app.translator.RequestParameters
+                .getStatementCategoryRestrictionMask(param.isReadOnly());
         IRequestParameters requestParameters = new org.apache.asterix.app.translator.RequestParameters(requestReference,
                 statementsText, getResultSet(), resultProperties, stats, null, param.getClientContextID(),
-                optionalParameters, stmtParams, param.isMultiStatement());
+                optionalParameters, stmtParams, param.isMultiStatement(), stmtCategoryRestriction);
         translator.compileAndExecute(getHyracksClientConnection(), requestParameters);
         execution.end();
         translator.getWarnings(warnings);
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 e394c7a..b666085 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
@@ -84,12 +84,14 @@
     private final Map<String, String> optionalParameters;
     private final Map<String, byte[]> statementParameters;
     private final boolean multiStatement;
+    private final int statementCategoryRestrictionMask;
     private final IRequestReference requestReference;
 
     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, boolean multiStatement, IRequestReference requestReference) {
+            Map<String, byte[]> statementParameters, boolean multiStatement, int statementCategoryRestrictionMask,
+            IRequestReference requestReference) {
         this.requestNodeId = requestNodeId;
         this.requestMessageId = requestMessageId;
         this.lang = lang;
@@ -101,6 +103,7 @@
         this.optionalParameters = optionalParameters;
         this.statementParameters = statementParameters;
         this.multiStatement = multiStatement;
+        this.statementCategoryRestrictionMask = statementCategoryRestrictionMask;
         this.requestReference = requestReference;
     }
 
@@ -139,9 +142,9 @@
                     compilationProvider, storageComponentProvider, new ResponsePrinter(sessionOutput));
             final IStatementExecutor.Stats stats = new IStatementExecutor.Stats();
             Map<String, IAObject> stmtParams = RequestParameters.deserializeParameterValues(statementParameters);
-            final IRequestParameters requestParameters =
-                    new RequestParameters(requestReference, statementsText, null, resultProperties, stats, outMetadata,
-                            clientContextID, optionalParameters, stmtParams, multiStatement);
+            final IRequestParameters requestParameters = new RequestParameters(requestReference, statementsText, null,
+                    resultProperties, stats, outMetadata, clientContextID, optionalParameters, stmtParams,
+                    multiStatement, statementCategoryRestrictionMask);
             translator.compileAndExecute(ccApp.getHcc(), requestParameters);
             translator.getWarnings(warnings);
             outPrinter.close();
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 e1247bd..a59adb7 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
@@ -272,9 +272,8 @@
 
     @Override
     public void compileAndExecute(IHyracksClientConnection hcc, IRequestParameters requestParameters) throws Exception {
-        if (!requestParameters.isMultiStatement()) {
-            validateStatements(statements);
-        }
+        validateStatements(statements, requestParameters.isMultiStatement(),
+                requestParameters.getStatementCategoryRestrictionMask());
         trackRequest(requestParameters);
         int resultSetIdCounter = 0;
         FileSplit outputFile = null;
@@ -2977,9 +2976,20 @@
         appCtx.getRequestTracker().track(clientRequest);
     }
 
-    public static void validateStatements(List<Statement> statements) throws CompilationException {
-        if (statements.stream().filter(QueryTranslator::isNotAllowedMultiStatement).count() > 1) {
-            throw new CompilationException(ErrorCode.UNSUPPORTED_MULTIPLE_STATEMENTS);
+    public static void validateStatements(List<Statement> statements, boolean allowMultiStatement,
+            int stmtCategoryRestrictionMask) throws CompilationException {
+        if (!allowMultiStatement) {
+            if (statements.stream().filter(QueryTranslator::isNotAllowedMultiStatement).count() > 1) {
+                throw new CompilationException(ErrorCode.UNSUPPORTED_MULTIPLE_STATEMENTS);
+            }
+        }
+        if (stmtCategoryRestrictionMask != RequestParameters.NO_CATEGORY_RESTRICTION_MASK) {
+            for (Statement stmt : statements) {
+                if (isNotAllowedStatementCategory(stmt, stmtCategoryRestrictionMask)) {
+                    throw new CompilationException(ErrorCode.PROHIBITED_STATEMENT_CATEGORY, stmt.getSourceLocation(),
+                            stmt.getKind());
+                }
+            }
         }
     }
 
@@ -2995,6 +3005,15 @@
         }
     }
 
+    private static boolean isNotAllowedStatementCategory(Statement statement, int categoryRestrictionMask) {
+        int category = statement.getCategory();
+        if (category <= 0) {
+            throw new IllegalArgumentException(String.valueOf(category));
+        }
+        int i = category & categoryRestrictionMask;
+        return i == 0;
+    }
+
     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 eda8a4a..90602e7 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
@@ -26,6 +26,7 @@
 import org.apache.asterix.common.api.IRequestReference;
 import org.apache.asterix.external.parser.JSONDataParser;
 import org.apache.asterix.formats.nontagged.SerializerDeserializerProvider;
+import org.apache.asterix.lang.common.base.Statement;
 import org.apache.asterix.om.base.IAObject;
 import org.apache.asterix.om.types.BuiltinType;
 import org.apache.asterix.translator.IRequestParameters;
@@ -42,6 +43,8 @@
 
 public class RequestParameters implements IRequestParameters {
 
+    public static final int NO_CATEGORY_RESTRICTION_MASK = 0;
+
     private final IRequestReference requestReference;
     private final IResultSet resultSet;
     private final ResultProperties resultProperties;
@@ -51,12 +54,21 @@
     private final String clientContextId;
     private final Map<String, IAObject> statementParameters;
     private final boolean multiStatement;
+    private final int statementCategoryRestrictionMask;
     private final String statement;
 
     public RequestParameters(IRequestReference requestReference, String statement, IResultSet resultSet,
             ResultProperties resultProperties, Stats stats, IStatementExecutor.ResultMetadata outMetadata,
             String clientContextId, Map<String, String> optionalParameters, Map<String, IAObject> statementParameters,
             boolean multiStatement) {
+        this(requestReference, statement, resultSet, resultProperties, stats, outMetadata, clientContextId,
+                optionalParameters, statementParameters, multiStatement, NO_CATEGORY_RESTRICTION_MASK);
+    }
+
+    public RequestParameters(IRequestReference requestReference, String statement, IResultSet resultSet,
+            ResultProperties resultProperties, Stats stats, IStatementExecutor.ResultMetadata outMetadata,
+            String clientContextId, Map<String, String> optionalParameters, Map<String, IAObject> statementParameters,
+            boolean multiStatement, int statementCategoryRestrictionMask) {
         this.requestReference = requestReference;
         this.statement = statement;
         this.resultSet = resultSet;
@@ -67,6 +79,7 @@
         this.optionalParameters = optionalParameters;
         this.statementParameters = statementParameters;
         this.multiStatement = multiStatement;
+        this.statementCategoryRestrictionMask = statementCategoryRestrictionMask;
     }
 
     @Override
@@ -105,6 +118,11 @@
     }
 
     @Override
+    public int getStatementCategoryRestrictionMask() {
+        return statementCategoryRestrictionMask;
+    }
+
+    @Override
     public Map<String, IAObject> getStatementParameters() {
         return statementParameters;
     }
@@ -159,4 +177,8 @@
         }
         return m;
     }
+
+    public static int getStatementCategoryRestrictionMask(boolean readOnly) {
+        return readOnly ? Statement.Category.QUERY : NO_CATEGORY_RESTRICTION_MASK;
+    }
 }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.1.ddl.sqlpp
new file mode 100644
index 0000000..b469669
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.1.ddl.sqlpp
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+drop dataverse test if exists;
+create dataverse test;
+
+drop dataverse test2 if exists;
+create dataverse test2;
+
+use test;
+
+create type testType as {
+    id: bigint,
+    c1: bigint,
+    c2: bigint
+};
+
+create dataset t1(testType) primary key id;
+
+create dataset t2(testType) primary key id;
+
+create index idx_c1 on t2(c1);
+
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.10.update.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.10.update.sqlpp
new file mode 100644
index 0000000..cbc61f4
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.10.update.sqlpp
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statement is prohibited in readonly request mode:
+ *                 : "insert"
+ * Expected Result : Failure
+ */
+
+// requesttype=application/json
+// param readonly:json=true
+
+use test;
+
+insert into t2
+([
+  {"id":1, "c1":100},
+  {"id":2, "c1":200},
+  {"id":3, "c1":300}
+]);
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.11.update.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.11.update.sqlpp
new file mode 100644
index 0000000..000b5a9
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.11.update.sqlpp
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statement is prohibited in readonly request mode:
+ *                 : "upsert"
+ * Expected Result : Failure
+ */
+
+-- param readonly:string=true
+
+use test;
+
+upsert into t2
+([
+  {"id":1, "c1":100},
+  {"id":2, "c1":200},
+  {"id":3, "c1":300}
+]);
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.12.update.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.12.update.sqlpp
new file mode 100644
index 0000000..c530572
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.12.update.sqlpp
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statement is prohibited in readonly request mode:
+ *                 : "delete"
+ * Expected Result : Failure
+ */
+
+// requesttype=application/json
+// param readonly:json=true
+
+use test;
+
+delete from t2
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.2.update.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.2.update.sqlpp
new file mode 100644
index 0000000..7306d85
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.2.update.sqlpp
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+use test;
+
+insert into t1
+([
+  {"id":1, "c1":100, "c2": 1000},
+  {"id":2, "c1":200, "c2": 2000},
+  {"id":3, "c1":300, "c2": 3000}
+]);
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.3.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.3.query.sqlpp
new file mode 100644
index 0000000..da67eff
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.3.query.sqlpp
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statements are allowed in readonly request mode:
+ *                 : "set", "use", "select", "declare function"
+ * Expected Result : Success
+ */
+
+// requesttype=application/json
+// param readonly:json=true
+
+use test;
+
+declare function f1(x) {
+  x + 1
+};
+
+select t1.id, t1.c1, f1(t1.c1) f1
+from t1
+order by t1.c1
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.4.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.4.ddl.sqlpp
new file mode 100644
index 0000000..5426e6c
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.4.ddl.sqlpp
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statement is prohibited in readonly request mode:
+ *                 : "create dataverse"
+ * Expected Result : Failure
+ */
+
+// requesttype=application/json
+// param readonly:json=true
+
+create dataverse test3;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.5.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.5.ddl.sqlpp
new file mode 100644
index 0000000..b6b3779
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.5.ddl.sqlpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statement is prohibited in readonly request mode:
+ *                 : "drop dataverse"
+ * Expected Result : Failure
+ */
+
+-- param readonly:string=true
+
+drop dataverse test2;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.6.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.6.ddl.sqlpp
new file mode 100644
index 0000000..1d96f33
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.6.ddl.sqlpp
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statement is prohibited in readonly request mode:
+ *                 : "create dataset"
+ * Expected Result : Failure
+ */
+
+// requesttype=application/json
+// param readonly:json=true
+
+use test;
+
+create dataset t3(testType) primary key id;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.7.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.7.ddl.sqlpp
new file mode 100644
index 0000000..5dea1d6
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.7.ddl.sqlpp
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statement is prohibited in readonly request mode:
+ *                 : "drop dataset"
+ * Expected Result : Failure
+ */
+
+-- param readonly:string=true
+
+use test;
+
+drop dataset t2;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.8.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.8.ddl.sqlpp
new file mode 100644
index 0000000..a3f699b
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.8.ddl.sqlpp
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statement is prohibited in readonly request mode:
+ *                 : "create index"
+ * Expected Result : Failure
+ */
+
+// requesttype=application/json
+// param readonly:json=true
+
+use test;
+
+create index idx_c2 on t2(c2);
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.9.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.9.ddl.sqlpp
new file mode 100644
index 0000000..b3b09c9
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/api/readonly-request/readonly-request.9.ddl.sqlpp
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+/*
+ * Description     : Test that the following statement is prohibited in readonly request mode:
+ *                 : "drop index"
+ * Expected Result : Failure
+ */
+
+-- param readonly:string=true
+
+use test;
+
+drop index t2.idx_c1;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/readonly-request/readonly-request.3.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/readonly-request/readonly-request.3.adm
new file mode 100644
index 0000000..7f2b70a
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/readonly-request/readonly-request.3.adm
@@ -0,0 +1,3 @@
+{ "id": 1, "c1": 100, "f1": 101 }
+{ "id": 2, "c1": 200, "f1": 201 }
+{ "id": 3, "c1": 300, "f1": 301 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
index a3c9508..6227109 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
@@ -28,6 +28,22 @@
   &GeoQueries;
   &TemporalQueries;
   <test-group name="flwor">
+    <test-case FilePath="api">
+      <compilation-unit name="readonly-request">
+        <output-dir compare="Text">readonly-request</output-dir>
+        <expected-error>ASX0044: CREATE_DATAVERSE statement is prohibited by this request</expected-error>
+        <expected-error>ASX0044: DATAVERSE_DROP statement is prohibited by this request</expected-error>
+        <expected-error>ASX0044: DATASET_DECL statement is prohibited by this request</expected-error>
+        <expected-error>ASX0044: DATASET_DROP statement is prohibited by this request</expected-error>
+        <expected-error>ASX0044: CREATE_INDEX statement is prohibited by this request</expected-error>
+        <expected-error>ASX0044: INDEX_DROP statement is prohibited by this request</expected-error>
+        <expected-error>ASX0044: INSERT statement is prohibited by this request</expected-error>
+        <expected-error>ASX0044: UPSERT statement is prohibited by this request</expected-error>
+        <expected-error>ASX0044: DELETE statement is prohibited by this request</expected-error>
+      </compilation-unit>
+    </test-case>
+  </test-group>
+  <test-group name="flwor">
     <test-case FilePath="flwor">
       <compilation-unit name="at00">
         <output-dir compare="Text">at00</output-dir>
@@ -5867,8 +5883,8 @@
     <test-case FilePath="misc">
       <compilation-unit name="ensure_result_numeric_type">
         <output-dir compare="Text">ensure_result_numeric_type</output-dir>
-        <source-location>false</source-location>
         <expected-error>expected &lt; 3.0</expected-error>
+        <source-location>false</source-location>
       </compilation-unit>
     </test-case>
     <test-case FilePath="misc">
@@ -10744,8 +10760,8 @@
     </test-case>
     <test-case FilePath="user-defined-functions">
       <compilation-unit name="query-ASTERIXDB-1652">
-        <expected-error>In function call "test.length(...)", the dataverse "test" cannot be found!</expected-error>
         <output-dir compare="Text">query-ASTERIXDB-1652-2</output-dir>
+        <expected-error>In function call "test.length(...)", the dataverse "test" cannot be found!</expected-error>
       </compilation-unit>
     </test-case>
     <test-case FilePath="user-defined-functions">
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 ca8ee38..8803214 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
@@ -76,6 +76,7 @@
     public static final int REQUEST_CANCELLED = 41;
     public static final int TPCDS_INVALID_TABLE_NAME = 42;
     public static final int VALUE_OUT_OF_RANGE = 43;
+    public static final int PROHIBITED_STATEMENT_CATEGORY = 44;
 
     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 362e506..73f87df 100644
--- a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
+++ b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
@@ -78,6 +78,7 @@
 41 = Request %1$s has been cancelled
 42 = %1$s: \"%2$s\" is not a TPC-DS table name
 43 = Value out of range, function %1$s expects its %2$s input parameter value to be between %3$s and %4$s, received %5$s
+44 = %1$s statement is prohibited by this request
 
 100 = Unsupported JRE: %1$s
 
diff --git a/asterixdb/asterix-doc/src/site/markdown/api.md b/asterixdb/asterix-doc/src/site/markdown/api.md
index 6853938..d39919f 100644
--- a/asterixdb/asterix-doc/src/site/markdown/api.md
+++ b/asterixdb/asterix-doc/src/site/markdown/api.md
@@ -43,6 +43,9 @@
   If the delivery mode is `immediate` the query result is returned with the response.
   If the delivery mode is `deferred` the response contains a handle to the <a href="#queryresult">result</a>.
   If the delivery mode is `async` the response contains a handle to the query's <a href="#querystatus">status</a>.
+* `readonly` - Reject DDL and DML statements, only accept the following kinds:
+  [SELECT](sqlpp/manual.html#SELECT_statements), [USE](sqlpp/manual.html#Declarations),
+  [DECLARE FUNCTION](sqlpp/manual.html#Declarations), and [SET](sqlpp/manual.html#Performance_tuning)
 * `args` - (SQL++ only) A JSON array where each item is a value of a [positional query parameter](sqlpp/manual.html#Parameter_references)
 * `$parameter_name` - (SQL++ only) a JSON value of a [named query parameter](sqlpp/manual.html#Parameter_references).
 
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/InsertStatement.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/InsertStatement.java
index 7bb2cbe..efa58fc 100644
--- a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/InsertStatement.java
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/InsertStatement.java
@@ -141,10 +141,6 @@
 
     @Override
     public byte getCategory() {
-        if (var == null) {
-            return Category.UPDATE;
-        }
-        return Category.QUERY;
+        return Category.UPDATE;
     }
-
 }
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/WriteStatement.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/WriteStatement.java
index aabd999..d4c11e4 100644
--- a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/WriteStatement.java
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/WriteStatement.java
@@ -60,7 +60,7 @@
 
     @Override
     public byte getCategory() {
-        return Category.QUERY;
+        return Category.PROCEDURE;
     }
 
 }