You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@asterixdb.apache.org by ti...@apache.org on 2016/07/29 19:03:21 UTC

asterixdb git commit: Implement EXPLAIN for SQL++

Repository: asterixdb
Updated Branches:
  refs/heads/master 018ec4a1d -> ef3f02761


Implement EXPLAIN for SQL++

- move some code from static methods in ResultUtils to a stateful
  ResultPrinter to facilitate reuse (we create one ResultWriter per request)
- tiny cleanup in LogicalOperatorPrettyPrintVisitor

Change-Id: I7b7028fb243d494150cac525c73b2d77b0068646
Reviewed-on: https://asterix-gerrit.ics.uci.edu/1020
Tested-by: Jenkins <je...@fulliautomatix.ics.uci.edu>
Integration-Tests: Jenkins <je...@fulliautomatix.ics.uci.edu>
Reviewed-by: Yingyi Bu <bu...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/asterixdb/repo
Commit: http://git-wip-us.apache.org/repos/asf/asterixdb/commit/ef3f0276
Tree: http://git-wip-us.apache.org/repos/asf/asterixdb/tree/ef3f0276
Diff: http://git-wip-us.apache.org/repos/asf/asterixdb/diff/ef3f0276

Branch: refs/heads/master
Commit: ef3f02761f7e0e5953f3939302690ee2c54f35d3
Parents: 018ec4a
Author: Till Westmann <ti...@apache.org>
Authored: Wed Jul 27 18:34:01 2016 -0700
Committer: Till Westmann <ti...@apache.org>
Committed: Fri Jul 29 12:02:33 2016 -0700

----------------------------------------------------------------------
 .../asterix-algebra/src/main/javacc/AQLPlus.jj  |   2 +-
 .../apache/asterix/api/common/APIFramework.java |  12 ++
 .../asterix/aql/translator/QueryTranslator.java |   8 +-
 .../apache/asterix/result/ResultPrinter.java    | 188 +++++++++++++++++++
 .../org/apache/asterix/result/ResultUtils.java  | 130 +------------
 .../explain_simple/explain_simple.1.query.sqlpp |  24 +++
 .../explain/explain_simple/explain_simple.1.adm |   9 +
 .../resources/runtimets/testsuite_sqlpp.xml     |   7 +
 .../aql/statement/SubscribeFeedStatement.java   |   2 +-
 .../aql/visitor/AqlDeleteRewriteVisitor.java    |   2 +-
 .../asterix-lang-aql/src/main/javacc/AQL.jj     |   2 +-
 .../asterix/lang/common/statement/Query.java    |  20 +-
 .../visitor/AbstractInlineUdfsVisitor.java      |   2 +-
 .../CloneAndSubstituteVariablesVisitor.java     |   2 +-
 .../lang/sqlpp/visitor/DeepCopyVisitor.java     |   2 +-
 .../visitor/SqlppDeleteRewriteVisitor.java      |   2 +-
 .../asterix-lang-sqlpp/src/main/javacc/SQLPP.jj |  19 +-
 .../LogicalOperatorPrettyPrintVisitor.java      |   9 +-
 18 files changed, 291 insertions(+), 151 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-algebra/src/main/javacc/AQLPlus.jj
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-algebra/src/main/javacc/AQLPlus.jj b/asterixdb/asterix-algebra/src/main/javacc/AQLPlus.jj
index 0e1e7a1..98cae63 100644
--- a/asterixdb/asterix-algebra/src/main/javacc/AQLPlus.jj
+++ b/asterixdb/asterix-algebra/src/main/javacc/AQLPlus.jj
@@ -487,7 +487,7 @@ FunctionDecl FunctionDeclaration() throws ParseException:
 
 Query Query()throws ParseException:
 {
-  Query query = new Query();
+  Query query = new Query(false);
   Expression expr;
 }
 {

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java
index af17c05..d6864c1 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java
@@ -18,6 +18,7 @@
  */
 package org.apache.asterix.api.common;
 
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.rmi.RemoteException;
 import java.util.ArrayList;
@@ -48,6 +49,7 @@ import org.apache.asterix.lang.common.statement.Query;
 import org.apache.asterix.metadata.declared.AqlMetadataProvider;
 import org.apache.asterix.om.util.AsterixAppContextInfo;
 import org.apache.asterix.optimizer.base.RuleCollections;
+import org.apache.asterix.result.ResultUtils;
 import org.apache.asterix.runtime.job.listener.JobEventListenerFactory;
 import org.apache.asterix.transaction.management.service.transaction.JobIdFactory;
 import org.apache.asterix.translator.CompiledStatements.ICompiledDmlStatement;
@@ -279,6 +281,16 @@ public class APIFramework {
                 }
             }
         }
+        if (rwQ != null && rwQ.isExplain()) {
+            try {
+                LogicalOperatorPrettyPrintVisitor pvisitor = new LogicalOperatorPrettyPrintVisitor();
+                PlanPrettyPrinter.printPlan(plan, pvisitor, 0);
+                ResultUtils.displayResults(pvisitor.get().toString(), conf, new ResultUtils.Stats(), null);
+                return null;
+            } catch (IOException e) {
+                throw new AlgebricksException(e);
+            }
+        }
 
         if (!conf.isGenerateJobSpec()) {
             return null;

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-app/src/main/java/org/apache/asterix/aql/translator/QueryTranslator.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/aql/translator/QueryTranslator.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/aql/translator/QueryTranslator.java
index 05d9b3d..d6065fb 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/aql/translator/QueryTranslator.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/aql/translator/QueryTranslator.java
@@ -2529,14 +2529,16 @@ public class QueryTranslator extends AbstractLangTranslator {
         boolean bActiveTxn = true;
         metadataProvider.setMetadataTxnContext(mdTxnCtx);
         MetadataLockManager.INSTANCE.queryBegin(activeDefaultDataverse, query.getDataverses(), query.getDatasets());
-        JobSpecification compiled = null;
         try {
-            compiled = rewriteCompileQuery(metadataProvider, query, null);
+            JobSpecification compiled = rewriteCompileQuery(metadataProvider, query, null);
 
             MetadataManager.INSTANCE.commitTransaction(mdTxnCtx);
             bActiveTxn = false;
 
-            if (sessionConfig.isExecuteQuery() && compiled != null) {
+            if (query.isExplain()) {
+                sessionConfig.out().flush();
+                return;
+            } else if (sessionConfig.isExecuteQuery() && compiled != null) {
                 GlobalConfig.ASTERIX_LOGGER.info(compiled.toJSON().toString(1));
                 JobId jobId = JobUtils.runJob(hcc, compiled, false);
 

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-app/src/main/java/org/apache/asterix/result/ResultPrinter.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/result/ResultPrinter.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/result/ResultPrinter.java
new file mode 100644
index 0000000..8e16ef7
--- /dev/null
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/result/ResultPrinter.java
@@ -0,0 +1,188 @@
+/*
+ * 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.result;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.ByteBuffer;
+
+import org.apache.asterix.api.common.SessionConfig;
+import org.apache.asterix.common.utils.JSONUtil;
+import org.apache.asterix.om.types.ARecordType;
+import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
+import org.apache.hyracks.algebricks.core.algebra.prettyprint.AlgebricksAppendable;
+import org.apache.hyracks.api.comm.IFrame;
+import org.apache.hyracks.api.comm.IFrameTupleAccessor;
+import org.apache.hyracks.api.comm.VSizeFrame;
+import org.apache.hyracks.api.exceptions.HyracksDataException;
+import org.apache.hyracks.control.nc.resources.memory.FrameManager;
+
+public class ResultPrinter {
+
+    // TODO(tillw): Should this be static?
+    private static FrameManager resultDisplayFrameMgr = new FrameManager(ResultReader.FRAME_SIZE);
+
+    private final SessionConfig conf;
+    private final ResultUtils.Stats stats;
+    private final ARecordType recordType;
+
+    private boolean indentJSON;
+    private boolean quoteRecord;
+
+    // Whether we are wrapping the output sequence in an array
+    private boolean wrapArray = false;
+    // Whether this is the first instance being output
+    private boolean notFirst = false;
+
+    public ResultPrinter(SessionConfig conf, ResultUtils.Stats stats, ARecordType recordType) {
+        this.conf = conf;
+        this.stats = stats;
+        this.recordType = recordType;
+        this.indentJSON = conf.is(SessionConfig.FORMAT_INDENT_JSON);
+        this.quoteRecord = conf.is(SessionConfig.FORMAT_QUOTE_RECORD);
+    }
+
+    private static void appendCSVHeader(Appendable app, ARecordType recordType) throws HyracksDataException {
+        try {
+            String[] fieldNames = recordType.getFieldNames();
+            boolean notfirst = false;
+            for (String name : fieldNames) {
+                if (notfirst) {
+                    app.append(',');
+                }
+                notfirst = true;
+                app.append('"').append(name.replace("\"", "\"\"")).append('"');
+            }
+            app.append("\r\n");
+        } catch (IOException e) {
+            throw new HyracksDataException(e);
+        }
+    }
+
+    private void printPrefix() throws HyracksDataException {
+        // If we're outputting CSV with a header, the HTML header was already
+        // output by displayCSVHeader(), so skip it here
+        if (conf.is(SessionConfig.FORMAT_HTML)) {
+            conf.out().println("<h4>Results:</h4>");
+            conf.out().println("<pre>");
+        }
+
+        try {
+            conf.resultPrefix(new AlgebricksAppendable(conf.out()));
+        } catch (AlgebricksException e) {
+            throw new HyracksDataException(e);
+        }
+
+        if (conf.is(SessionConfig.FORMAT_WRAPPER_ARRAY)) {
+            conf.out().print("[ ");
+            wrapArray = true;
+        }
+
+        if (conf.fmt() == SessionConfig.OutputFormat.CSV && conf.is(SessionConfig.FORMAT_CSV_HEADER)) {
+            if (recordType == null) {
+                throw new HyracksDataException("Cannot print CSV with header without specifying output-record-type");
+            }
+            if (quoteRecord) {
+                StringWriter sw = new StringWriter();
+                appendCSVHeader(sw, recordType);
+                conf.out().print(JSONUtil.quoteAndEscape(sw.toString()));
+                conf.out().print("\n");
+                notFirst = true;
+            } else {
+                appendCSVHeader(conf.out(), recordType);
+            }
+        }
+    }
+
+    private void printPostfix() throws HyracksDataException {
+        conf.out().flush();
+        if (wrapArray) {
+            conf.out().println(" ]");
+        }
+        try {
+            conf.resultPostfix(new AlgebricksAppendable(conf.out()));
+        } catch (AlgebricksException e) {
+            throw new HyracksDataException(e);
+        }
+        if (conf.is(SessionConfig.FORMAT_HTML)) {
+            conf.out().println("</pre>");
+        }
+    }
+
+    private void displayRecord(String result) {
+        String record = result;
+        if (indentJSON) {
+            // TODO(tillw): this is inefficient - do this during record generation
+            record = JSONUtil.indent(record, 2);
+        }
+        if (conf.fmt() == SessionConfig.OutputFormat.CSV) {
+            // TODO(tillw): this is inefficient as well
+            record = record + "\r\n";
+        }
+        if (quoteRecord) {
+            // TODO(tillw): this is inefficient as well
+            record = JSONUtil.quoteAndEscape(record);
+        }
+        conf.out().print(record);
+        ++stats.count;
+        // TODO(tillw) fix this approximation
+        stats.size += record.length();
+    }
+
+    public void print(String record) throws HyracksDataException {
+        printPrefix();
+        // TODO(tillw) evil hack
+        quoteRecord = true;
+        displayRecord(record);
+        printPostfix();
+    }
+
+    public void print(ResultReader resultReader) throws HyracksDataException {
+        printPrefix();
+
+        final IFrameTupleAccessor fta = resultReader.getFrameTupleAccessor();
+        final IFrame frame = new VSizeFrame(resultDisplayFrameMgr);
+
+        while (resultReader.read(frame) > 0) {
+            final ByteBuffer frameBuffer = frame.getBuffer();
+            final byte[] frameBytes = frameBuffer.array();
+            fta.reset(frameBuffer);
+            final int last = fta.getTupleCount();
+            for (int tIndex = 0; tIndex < last; tIndex++) {
+                final int start = fta.getTupleStartOffset(tIndex);
+                int length = fta.getTupleEndOffset(tIndex) - start;
+                if (conf.fmt() == SessionConfig.OutputFormat.CSV
+                        && ((length > 0) && (frameBytes[start + length - 1] == '\n'))) {
+                    length--;
+                }
+                String result = new String(frameBytes, start, length, UTF_8);
+                if (wrapArray && notFirst) {
+                    conf.out().print(", ");
+                }
+                notFirst = true;
+                displayRecord(result);
+            }
+            frameBuffer.clear();
+        }
+
+        printPostfix();
+    }
+}

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-app/src/main/java/org/apache/asterix/result/ResultUtils.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/result/ResultUtils.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/result/ResultUtils.java
index 8c3ccfc..3503549 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/result/ResultUtils.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/result/ResultUtils.java
@@ -24,7 +24,6 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
 import java.util.HashMap;
 import java.util.Map;
@@ -32,25 +31,16 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.asterix.api.common.SessionConfig;
-import org.apache.asterix.api.common.SessionConfig.OutputFormat;
 import org.apache.asterix.api.http.servlet.APIServlet;
-import org.apache.asterix.common.utils.JSONUtil;
 import org.apache.asterix.om.types.ARecordType;
 import org.apache.http.ParseException;
 import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
-import org.apache.hyracks.algebricks.core.algebra.prettyprint.AlgebricksAppendable;
-import org.apache.hyracks.api.comm.IFrame;
-import org.apache.hyracks.api.comm.IFrameTupleAccessor;
-import org.apache.hyracks.api.comm.VSizeFrame;
 import org.apache.hyracks.api.exceptions.HyracksDataException;
-import org.apache.hyracks.control.nc.resources.memory.FrameManager;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 public class ResultUtils {
-    private static final Charset UTF_8 = Charset.forName("UTF-8");
-
     static Map<Character, String> HTML_ENTITIES = new HashMap<Character, String>();
 
     static {
@@ -74,124 +64,14 @@ public class ResultUtils {
         return s;
     }
 
-    private static void printCSVHeader(ARecordType recordType, PrintWriter out) {
-        String[] fieldNames = recordType.getFieldNames();
-        boolean notfirst = false;
-        for (String name : fieldNames) {
-            if (notfirst) {
-                out.print(',');
-            }
-            notfirst = true;
-            out.print('"');
-            out.print(name.replace("\"", "\"\""));
-            out.print('"');
-        }
-        out.print("\r\n");
-    }
-
-    public static FrameManager resultDisplayFrameMgr = new FrameManager(ResultReader.FRAME_SIZE);
-
     public static void displayResults(ResultReader resultReader, SessionConfig conf, Stats stats,
             ARecordType recordType) throws HyracksDataException {
-        // Whether we are wrapping the output sequence in an array
-        boolean wrap_array = false;
-        // Whether this is the first instance being output
-        boolean notfirst = false;
-
-        // If we're outputting CSV with a header, the HTML header was already
-        // output by displayCSVHeader(), so skip it here
-        if (conf.is(SessionConfig.FORMAT_HTML)) {
-            conf.out().println("<h4>Results:</h4>");
-            conf.out().println("<pre>");
-        }
-
-        try {
-            conf.resultPrefix(new AlgebricksAppendable(conf.out()));
-        } catch (AlgebricksException e) {
-            throw new HyracksDataException(e);
-        }
-
-        if (conf.is(SessionConfig.FORMAT_WRAPPER_ARRAY)) {
-            conf.out().print("[ ");
-            wrap_array = true;
-        }
-
-        final boolean indentJSON = conf.is(SessionConfig.FORMAT_INDENT_JSON);
-        final boolean quoteRecord = conf.is(SessionConfig.FORMAT_QUOTE_RECORD);
-
-        if (conf.fmt() == OutputFormat.CSV && conf.is(SessionConfig.FORMAT_CSV_HEADER)) {
-            if (recordType == null) {
-                throw new HyracksDataException("Cannot print CSV with header without specifying output-record-type");
-            }
-            if (quoteRecord) {
-                StringWriter sw = new StringWriter();
-                PrintWriter pw = new PrintWriter(sw);
-                printCSVHeader(recordType, pw);
-                pw.close();
-                conf.out().print(JSONUtil.quoteAndEscape(sw.toString()));
-                conf.out().print("\n");
-                notfirst = true;
-            } else {
-                printCSVHeader(recordType, conf.out());
-            }
-        }
-
-        final IFrameTupleAccessor fta = resultReader.getFrameTupleAccessor();
-        final IFrame frame = new VSizeFrame(resultDisplayFrameMgr);
-
-        while (resultReader.read(frame) > 0) {
-            final ByteBuffer frameBuffer = frame.getBuffer();
-            final byte[] frameBytes = frameBuffer.array();
-            fta.reset(frameBuffer);
-            final int last = fta.getTupleCount();
-            for (int tIndex = 0; tIndex < last; tIndex++) {
-                final int start = fta.getTupleStartOffset(tIndex);
-                int length = fta.getTupleEndOffset(tIndex) - start;
-                if (conf.fmt() == OutputFormat.CSV) {
-                    if ((length > 0) && (frameBytes[start + length - 1] == '\n')) {
-                        length--;
-                    }
-                }
-                String result = new String(frameBytes, start, length, UTF_8);
-                if (wrap_array && notfirst) {
-                    conf.out().print(", ");
-                }
-                notfirst = true;
-                if (indentJSON) {
-                    // TODO(tillw): this is inefficient - do this during result generation
-                    result = JSONUtil.indent(result, 2);
-                }
-                if (conf.fmt() == OutputFormat.CSV) {
-                    // TODO(tillw): this is inefficient as well
-                    result = result + "\r\n";
-                }
-                if (quoteRecord) {
-                    // TODO(tillw): this is inefficient as well
-                    result = JSONUtil.quoteAndEscape(result);
-                }
-                conf.out().print(result);
-                ++stats.count;
-                // TODO(tillw) fix this approximation
-                stats.size += result.length();
-            }
-            frameBuffer.clear();
-        }
-
-        conf.out().flush();
-
-        if (wrap_array) {
-            conf.out().println(" ]");
-        }
-
-        try {
-            conf.resultPostfix(new AlgebricksAppendable(conf.out()));
-        } catch (AlgebricksException e) {
-            throw new HyracksDataException(e);
-        }
+        new ResultPrinter(conf, stats, recordType).print(resultReader);
+    }
 
-        if (conf.is(SessionConfig.FORMAT_HTML)) {
-            conf.out().println("</pre>");
-        }
+    public static void displayResults(String record, SessionConfig conf, Stats stats, ARecordType recordType)
+            throws HyracksDataException {
+        new ResultPrinter(conf, stats, recordType).print(record);
     }
 
     public static JSONObject getErrorResponse(int errorCode, String errorMessage, String errorSummary,

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/explain/explain_simple/explain_simple.1.query.sqlpp
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/explain/explain_simple/explain_simple.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/explain/explain_simple/explain_simple.1.query.sqlpp
new file mode 100644
index 0000000..6860087
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/explain/explain_simple/explain_simple.1.query.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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  : EXPLAIN a plan for a very simple query
+* Expected Res : Success
+* Date         : Jul 25, 2016
+*/
+explain select value 1+1;

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-app/src/test/resources/runtimets/results/explain/explain_simple/explain_simple.1.adm
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/explain/explain_simple/explain_simple.1.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/explain/explain_simple/explain_simple.1.adm
new file mode 100644
index 0000000..d3b66a5
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/explain/explain_simple/explain_simple.1.adm
@@ -0,0 +1,9 @@
+distribute result [%0->$$2]
+-- DISTRIBUTE_RESULT  |UNPARTITIONED|
+  exchange
+  -- ONE_TO_ONE_EXCHANGE  |UNPARTITIONED|
+    assign [$$2] <- [AInt64: {2}]
+    -- ASSIGN  |UNPARTITIONED|
+      empty-tuple-source
+      -- EMPTY_TUPLE_SOURCE  |UNPARTITIONED|
+

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
----------------------------------------------------------------------
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 99e4935..fa29a42 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
@@ -63,6 +63,13 @@
       </compilation-unit>
     </test-case>
   </test-group>
+  <test-group name="explain">
+    <test-case FilePath="explain">
+      <compilation-unit name="explain_simple">
+        <output-dir compare="Text">explain_simple</output-dir>
+      </compilation-unit>
+    </test-case>
+  </test-group>
   <!--
     <test-group name="union">
         <test-case FilePath="union">

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/statement/SubscribeFeedStatement.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/statement/SubscribeFeedStatement.java b/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/statement/SubscribeFeedStatement.java
index 9e6f857..836de6a 100644
--- a/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/statement/SubscribeFeedStatement.java
+++ b/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/statement/SubscribeFeedStatement.java
@@ -70,7 +70,7 @@ public class SubscribeFeedStatement implements Statement {
     }
 
     public void initialize(MetadataTransactionContext mdTxnCtx) throws MetadataException {
-        this.query = new Query();
+        this.query = new Query(false);
         EntityId sourceFeedId = connectionRequest.getFeedJointKey().getFeedId();
         Feed subscriberFeed =
                 MetadataManager.INSTANCE.getFeed(mdTxnCtx, connectionRequest.getReceivingFeedId().getDataverse(),

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/visitor/AqlDeleteRewriteVisitor.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/visitor/AqlDeleteRewriteVisitor.java b/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/visitor/AqlDeleteRewriteVisitor.java
index cedeb77..9268422 100644
--- a/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/visitor/AqlDeleteRewriteVisitor.java
+++ b/asterixdb/asterix-lang-aql/src/main/java/org/apache/asterix/lang/aql/visitor/AqlDeleteRewriteVisitor.java
@@ -64,7 +64,7 @@ public class AqlDeleteRewriteVisitor extends AbstractAqlAstVisitor<Void, Void> {
         VariableExpr returnExpr = new VariableExpr(var.getVar());
         returnExpr.setIsNewVar(false);
         FLWOGRExpression flowgr = new FLWOGRExpression(clauseList, returnExpr);
-        Query query = new Query();
+        Query query = new Query(false);
         query.setBody(flowgr);
         deleteStmt.setQuery(query);
         return null;

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-lang-aql/src/main/javacc/AQL.jj
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-lang-aql/src/main/javacc/AQL.jj b/asterixdb/asterix-lang-aql/src/main/javacc/AQL.jj
index 172e534..b3f6200 100644
--- a/asterixdb/asterix-lang-aql/src/main/javacc/AQL.jj
+++ b/asterixdb/asterix-lang-aql/src/main/javacc/AQL.jj
@@ -1528,7 +1528,7 @@ FunctionDecl FunctionDeclaration() throws ParseException:
 
 Query Query() throws ParseException:
 {
-  Query query = new Query();
+  Query query = new Query(false);
   // we set the pointers to the dataverses and datasets lists to fill them with entities to be locked
   setDataverses(query.getDataverses());
   setDatasets(query.getDatasets());

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/Query.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/Query.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/Query.java
index 80853c8..9be4830 100644
--- a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/Query.java
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/Query.java
@@ -28,17 +28,20 @@ import org.apache.asterix.lang.common.visitor.base.ILangVisitor;
 import org.apache.commons.lang3.ObjectUtils;
 
 public class Query implements Statement {
+    private final boolean explain;
     private boolean topLevel = true;
     private Expression body;
     private int varCounter;
     private List<String> dataverses = new ArrayList<>();
     private List<String> datasets = new ArrayList<>();
 
-    public Query() {
-        // Default constructor.
+    public Query(boolean explain) {
+        this.explain = explain;
     }
 
-    public Query(boolean topLevel, Expression body, int varCounter, List<String> dataverses, List<String> datasets) {
+    public Query(boolean explain, boolean topLevel, Expression body, int varCounter, List<String> dataverses,
+            List<String> datasets) {
+        this.explain = explain;
         this.topLevel = topLevel;
         this.body = body;
         this.varCounter = varCounter;
@@ -70,6 +73,10 @@ public class Query implements Statement {
         return topLevel;
     }
 
+    public boolean isExplain() {
+        return explain;
+    }
+
     @Override
     public byte getKind() {
         return Statement.Kind.QUERY;
@@ -98,7 +105,7 @@ public class Query implements Statement {
 
     @Override
     public int hashCode() {
-        return ObjectUtils.hashCodeMulti(body, datasets, dataverses, topLevel);
+        return ObjectUtils.hashCodeMulti(body, datasets, dataverses, topLevel, explain);
     }
 
     @Override
@@ -110,7 +117,8 @@ public class Query implements Statement {
             return false;
         }
         Query target = (Query) object;
-        return ObjectUtils.equals(body, target.body) && ObjectUtils.equals(datasets, target.datasets)
-                && ObjectUtils.equals(dataverses, target.dataverses) && topLevel == target.topLevel;
+        return explain == target.explain && ObjectUtils.equals(body, target.body)
+                && ObjectUtils.equals(datasets, target.datasets) && ObjectUtils.equals(dataverses, target.dataverses)
+                && topLevel == target.topLevel;
     }
 }

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/AbstractInlineUdfsVisitor.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/AbstractInlineUdfsVisitor.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/AbstractInlineUdfsVisitor.java
index 159af9f..f79f811 100644
--- a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/AbstractInlineUdfsVisitor.java
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/AbstractInlineUdfsVisitor.java
@@ -307,7 +307,7 @@ public abstract class AbstractInlineUdfsVisitor extends AbstractQueryExpressionV
     }
 
     protected Expression rewriteFunctionBody(Expression expr) throws AsterixException {
-        Query wrappedQuery = new Query();
+        Query wrappedQuery = new Query(false);
         wrappedQuery.setBody(expr);
         wrappedQuery.setTopLevel(false);
         IQueryRewriter queryRewriter = rewriterFactory.createQueryRewriter();

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/CloneAndSubstituteVariablesVisitor.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/CloneAndSubstituteVariablesVisitor.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/CloneAndSubstituteVariablesVisitor.java
index 667878c..9ffcf8d 100644
--- a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/CloneAndSubstituteVariablesVisitor.java
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/visitor/CloneAndSubstituteVariablesVisitor.java
@@ -235,7 +235,7 @@ public class CloneAndSubstituteVariablesVisitor extends
     @Override
     public Pair<ILangExpression, VariableSubstitutionEnvironment> visit(Query q, VariableSubstitutionEnvironment env)
             throws AsterixException {
-        Query newQ = new Query();
+        Query newQ = new Query(q.isExplain());
         Pair<ILangExpression, VariableSubstitutionEnvironment> p1 = q.getBody().accept(this, env);
         newQ.setBody((Expression) p1.first);
         return new Pair<>(newQ, p1.second);

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/DeepCopyVisitor.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/DeepCopyVisitor.java b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/DeepCopyVisitor.java
index 01f3524..d752396 100644
--- a/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/DeepCopyVisitor.java
+++ b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/DeepCopyVisitor.java
@@ -229,7 +229,7 @@ public class DeepCopyVisitor extends AbstractSqlppQueryExpressionVisitor<ILangEx
 
     @Override
     public Query visit(Query q, Void arg) throws AsterixException {
-        return new Query(q.isTopLevel(), (Expression) q.getBody().accept(this, arg), q.getVarCounter(),
+        return new Query(q.isExplain(), q.isTopLevel(), (Expression) q.getBody().accept(this, arg), q.getVarCounter(),
                 q.getDataverses(), q.getDatasets());
     }
 

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/SqlppDeleteRewriteVisitor.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/SqlppDeleteRewriteVisitor.java b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/SqlppDeleteRewriteVisitor.java
index bfb1e44..a71cc47 100644
--- a/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/SqlppDeleteRewriteVisitor.java
+++ b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/visitor/SqlppDeleteRewriteVisitor.java
@@ -84,7 +84,7 @@ public class SqlppDeleteRewriteVisitor extends AbstractSqlppAstVisitor<Void, Voi
         SelectBlock selectBlock = new SelectBlock(selectClause, fromClause, null, whereClause, null, null, null);
         SelectSetOperation selectSetOperation = new SelectSetOperation(new SetOperationInput(selectBlock, null), null);
         SelectExpression selectExpression = new SelectExpression(null, selectSetOperation, null, null, false);
-        Query query = new Query(false, selectExpression, 0, new ArrayList<>(), new ArrayList<>());
+        Query query = new Query(false, false, selectExpression, 0, new ArrayList<>(), new ArrayList<>());
         query.setBody(selectExpression);
 
         // return the delete statement.

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
index afc970a..8f9a201 100644
--- a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
+++ b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
@@ -316,7 +316,8 @@ Statement SingleStatement() throws ParseException:
     | stmt = UpdateStatement()
     | stmt = FeedStatement()
     | stmt = CompactStatement()
-    | stmt = Query() <SEMICOLON>
+    | stmt = ExplainStatement()
+    | stmt = Query(false) <SEMICOLON>
     | stmt = RefreshExternalDatasetStatement()
     | stmt = RunStatement()
   )
@@ -925,7 +926,7 @@ InsertStatement InsertStatement() throws ParseException:
   Query query;
 }
 {
-  <INSERT> <INTO> nameComponents = QualifiedName() query = Query()
+  <INSERT> <INTO> nameComponents = QualifiedName() query = Query(false)
     {
       query.setTopLevel(true);
       return new InsertStatement(nameComponents.first, nameComponents.second, query, getVarCounter());
@@ -1562,10 +1563,20 @@ FunctionDecl FunctionDeclaration() throws ParseException:
     }
 }
 
+Query ExplainStatement() throws ParseException:
+{
+  Query query;
+}
+{
+  "explain" query = Query(true)
+  {
+    return query;
+  }
+}
 
-Query Query() throws ParseException:
+Query Query(boolean explain) throws ParseException:
 {
-  Query query = new Query();
+  Query query = new Query(explain);
   // we set the pointers to the dataverses and datasets lists to fill them with entities to be locked
   setDataverses(query.getDataverses());
   setDatasets(query.getDatasets());

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/ef3f0276/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/prettyprint/LogicalOperatorPrettyPrintVisitor.java
----------------------------------------------------------------------
diff --git a/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/prettyprint/LogicalOperatorPrettyPrintVisitor.java b/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/prettyprint/LogicalOperatorPrettyPrintVisitor.java
index 7c749ed..fe8e044 100644
--- a/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/prettyprint/LogicalOperatorPrettyPrintVisitor.java
+++ b/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/prettyprint/LogicalOperatorPrettyPrintVisitor.java
@@ -142,7 +142,7 @@ public class LogicalOperatorPrettyPrintVisitor implements ILogicalOperatorVisito
 
     @Override
     public Void visitDistinctOperator(DistinctOperator op, Integer indent) throws AlgebricksException {
-        addIndent(indent).append("distinct " + "(");
+        addIndent(indent).append("distinct (");
         pprintExprList(op.getExpressions(), indent);
         buffer.append(")");
         return null;
@@ -177,7 +177,6 @@ public class LogicalOperatorPrettyPrintVisitor implements ILogicalOperatorVisito
             }
             String fst = getOrderString(p.first);
             buffer.append("(" + fst + ", " + p.second.getValue().accept(exprVisitor, indent) + ") ");
-
         }
         return null;
     }
@@ -346,7 +345,7 @@ public class LogicalOperatorPrettyPrintVisitor implements ILogicalOperatorVisito
 
     @Override
     public Void visitExchangeOperator(ExchangeOperator op, Integer indent) throws AlgebricksException {
-        addIndent(indent).append("exchange ");
+        addIndent(indent).append("exchange");
         return null;
     }
 
@@ -358,13 +357,13 @@ public class LogicalOperatorPrettyPrintVisitor implements ILogicalOperatorVisito
 
     @Override
     public Void visitReplicateOperator(ReplicateOperator op, Integer indent) throws AlgebricksException {
-        addIndent(indent).append("replicate ");
+        addIndent(indent).append("replicate");
         return null;
     }
 
     @Override
     public Void visitMaterializeOperator(MaterializeOperator op, Integer indent) throws AlgebricksException {
-        addIndent(indent).append("materialize ");
+        addIndent(indent).append("materialize");
         return null;
     }