From 50c75c56a46dd47b04503abd1ecd17adfa20da5f Mon Sep 17 00:00:00 2001
From: Teon Banek <teon.banek@memgraph.io>
Date: Thu, 23 Aug 2018 13:15:15 +0200
Subject: [PATCH] Add EXPLAIN to openCypher

Summary:
  * Move PlanPrinter from test to memgraph
  * Add explainQuery to MemgraphCypher.g4
  * Add Explain operator
  * Update changelog

Reviewers: mtomic, buda, ipaljak

Reviewed By: mtomic

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D1555
---
 CHANGELOG.md                                  |   1 +
 .../reference_guide/other_features.md         |  38 +++
 src/CMakeLists.txt                            |   1 +
 src/query/frontend/ast/ast.capnp              |   3 +-
 src/query/frontend/ast/ast.cpp                |   2 +
 src/query/frontend/ast/ast.hpp                |   5 +
 .../frontend/ast/cypher_main_visitor.cpp      |   8 +
 .../frontend/ast/cypher_main_visitor.hpp      |   5 +
 .../opencypher/grammar/MemgraphCypher.g4      |   3 +
 .../opencypher/grammar/MemgraphCypherLexer.g4 |   3 +-
 src/query/interpreter.hpp                     |   2 +-
 src/query/plan/distributed.cpp                |  37 ++-
 src/query/plan/operator.cpp                   |  49 +++
 src/query/plan/operator.lcp                   |  29 +-
 src/query/plan/planner.hpp                    |   6 +
 src/query/plan/preprocess.cpp                 |  29 +-
 src/query/plan/pretty_print.cpp               | 311 ++++++++++++++++++
 src/query/plan/pretty_print.hpp               |  27 ++
 tests/manual/query_planner.cpp                | 292 +---------------
 tests/unit/cypher_main_visitor.cpp            |  29 ++
 tests/unit/query_planner.cpp                  |   4 +
 21 files changed, 575 insertions(+), 309 deletions(-)
 create mode 100644 src/query/plan/pretty_print.cpp
 create mode 100644 src/query/plan/pretty_print.hpp

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4251082ce..166d36be7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 
 * [Enterprise Ed.] Authentication and authorization support.
 * [Enterprise Ed.] Kafka integration.
+* Add `EXPLAIN` clause to openCypher
 
 ## v0.12.0
 
diff --git a/docs/user_technical/reference_guide/other_features.md b/docs/user_technical/reference_guide/other_features.md
index 71ef618d7..dd2e237e6 100644
--- a/docs/user_technical/reference_guide/other_features.md
+++ b/docs/user_technical/reference_guide/other_features.md
@@ -158,3 +158,41 @@ to true is matched:
 MATCH (n)
 RETURN CASE WHEN n.height < 30 THEN "short" WHEN n.height > 300 THEN "tall" END
 ```
+
+### EXPLAIN
+
+The `EXPLAIN` clause is used to get a visual representation of operations a
+given query will perform. This can be a useful tool for understanding the
+query execution and fine tuning the behaviour.
+
+NOTE: Query will not be executed. `EXPLAIN` only shows the operations that
+would need to be taken.
+
+For example:
+
+```opencypher
+EXPLAIN MATCH (n :Person) WHERE n.height < 30 RETURN n;
+```
+
+Will display:
+
+```plaintext
+* Produce {n}
+* Filter
+* ScanAllByLabel (n :Person)
+```
+
+This makes it evident that indexing on a property value `height` is not used.
+
+We create an index with:
+
+```opencypher
+CREATE INDEX ON :Person(height)
+```
+
+Running the same `EXPLAIN` will now display:
+
+```plaintext
+* Produce {n}
+* ScanAllByLabelPropertyRange (n :Person {height})
+```
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 1356cb1e1..8ca057c68 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -56,6 +56,7 @@ set(memgraph_src_files
     query/plan/distributed.cpp
     query/plan/operator.cpp
     query/plan/preprocess.cpp
+    query/plan/pretty_print.cpp
     query/plan/rule_based_planner.cpp
     query/plan/variable_start_planner.cpp
     query/repl.cpp
diff --git a/src/query/frontend/ast/ast.capnp b/src/query/frontend/ast/ast.capnp
index 2368fc9fb..70b76ea28 100644
--- a/src/query/frontend/ast/ast.capnp
+++ b/src/query/frontend/ast/ast.capnp
@@ -155,6 +155,7 @@ struct CypherUnion {
 struct Query {
   singleQuery @0 :Tree;
   cypherUnions @1 :List(Tree);
+  explain @2 :Bool;
 }
 
 struct BinaryOperator {
@@ -431,7 +432,7 @@ struct AuthQuery {
   role @2 :Text;
   userOrRole @3 :Text;
   password @4 :Tree;
-  privileges @5 :List(Privilege);  
+  privileges @5 :List(Privilege);
 }
 
 struct CreateStream {
diff --git a/src/query/frontend/ast/ast.cpp b/src/query/frontend/ast/ast.cpp
index b1a6c0d7b..6158f35da 100644
--- a/src/query/frontend/ast/ast.cpp
+++ b/src/query/frontend/ast/ast.cpp
@@ -2700,6 +2700,7 @@ void Query::Save(capnp::Tree::Builder *tree_builder,
 }
 
 void Query::Save(capnp::Query::Builder *builder, std::vector<int> *saved_uids) {
+  builder->setExplain(explain_);
   if (single_query_) {
     auto sq_builder = builder->initSingleQuery();
     single_query_->Save(&sq_builder, saved_uids);
@@ -2716,6 +2717,7 @@ void Query::Load(const capnp::Tree::Reader &reader, AstStorage *storage,
                  std::vector<int> *loaded_uids) {
   Tree::Load(reader, storage, loaded_uids);
   auto query_reader = reader.getQuery();
+  explain_ = query_reader.getExplain();
   if (query_reader.hasSingleQuery()) {
     const auto sq_reader = query_reader.getSingleQuery();
     single_query_ =
diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp
index 96da6b840..c3883e844 100644
--- a/src/query/frontend/ast/ast.hpp
+++ b/src/query/frontend/ast/ast.hpp
@@ -1728,6 +1728,7 @@ class Query : public Tree {
     for (auto *cypher_union : cypher_unions_) {
       query->cypher_unions_.push_back(cypher_union->Clone(storage));
     }
+    query->explain_ = explain_;
     return query;
   }
 
@@ -1736,7 +1737,11 @@ class Query : public Tree {
   void Save(capnp::Tree::Builder *builder,
             std::vector<int> *saved_uids) override;
 
+  /// True if this is an EXPLAIN query
+  bool explain_ = false;
+  /// First and potentially only query
   SingleQuery *single_query_ = nullptr;
+  /// Contains remaining queries that should form a union with `single_query_`
   std::vector<CypherUnion *> cypher_unions_;
 
  protected:
diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp
index 62f6dd957..cee8fd84a 100644
--- a/src/query/frontend/ast/cypher_main_visitor.cpp
+++ b/src/query/frontend/ast/cypher_main_visitor.cpp
@@ -25,6 +25,14 @@ namespace query::frontend {
 
 const std::string CypherMainVisitor::kAnonPrefix = "anon";
 
+antlrcpp::Any CypherMainVisitor::visitExplainQuery(
+    MemgraphCypher::ExplainQueryContext *ctx) {
+  visitChildren(ctx);
+  CHECK(query_);
+  query_->explain_ = true;
+  return query_;
+}
+
 antlrcpp::Any CypherMainVisitor::visitAuthQuery(
     MemgraphCypher::AuthQueryContext *ctx) {
   query_ = storage_.query();
diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp
index d84b166b0..3b10922a9 100644
--- a/src/query/frontend/ast/cypher_main_visitor.hpp
+++ b/src/query/frontend/ast/cypher_main_visitor.hpp
@@ -132,6 +132,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
     return expression;
   }
 
+  /**
+   * @return Query*
+   */
+  antlrcpp::Any visitExplainQuery(MemgraphCypher::ExplainQueryContext *ctx) override;
+
   /**
    * @return Query*
    */
diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4
index 9c8f2dc7c..65dd88533 100644
--- a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4
+++ b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4
@@ -48,8 +48,11 @@ symbolicName : UnescapedSymbolicName
 query : regularQuery
       | authQuery
       | streamQuery
+      | explainQuery
       ;
 
+explainQuery : EXPLAIN regularQuery ;
+
 authQuery : createRole
           | dropRole
           | showRoles
diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4
index 0ec3a28a7..7154141ca 100644
--- a/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4
+++ b/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4
@@ -6,7 +6,7 @@
  * and bitsets) if needed.
  */
 
-lexer grammar MemgraphCypherLexer ; 
+lexer grammar MemgraphCypherLexer ;
 
 import CypherLexer ;
 
@@ -17,6 +17,7 @@ BATCHES        : B A T C H E S ;
 DATA           : D A T A ;
 DENY           : D E N Y ;
 DROP           : D R O P ;
+EXPLAIN        : E X P L A I N ;
 FOR            : F O R ;
 FROM           : F R O M ;
 GRANT          : G R A N T ;
diff --git a/src/query/interpreter.hpp b/src/query/interpreter.hpp
index 771f44ec6..9dbbb9a7f 100644
--- a/src/query/interpreter.hpp
+++ b/src/query/interpreter.hpp
@@ -168,7 +168,7 @@ class Interpreter {
 
   /**
    * Generates an Results object for the parameters. The resulting object
-   * can the be Pulled with it's results written to an arbitrary stream.
+   * can be Pulled with its results written to an arbitrary stream.
    */
   Results operator()(const std::string &query,
                      database::GraphDbAccessor &db_accessor,
diff --git a/src/query/plan/distributed.cpp b/src/query/plan/distributed.cpp
index 96d513afa..f91b979f0 100644
--- a/src/query/plan/distributed.cpp
+++ b/src/query/plan/distributed.cpp
@@ -95,6 +95,16 @@ class IndependentSubtreeFinder : public HierarchicalLogicalOperatorVisitor {
   bool Visit(StartStopAllStreams &) override { return true; }
   bool Visit(TestStream &) override { return true; }
 
+  // Treat Explain as if the query is planned without it
+  bool PreVisit(Explain &explain) override {
+    prev_ops_.push_back(&explain);
+    return true;
+  }
+  bool PostVisit(Explain &explain) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
   bool PreVisit(ScanAll &scan) override {
     prev_ops_.push_back(&scan);
     return true;
@@ -1041,6 +1051,26 @@ class DistributedPlanner : public HierarchicalLogicalOperatorVisitor {
     return true;
   }
 
+  // Treat Explain as if the query is planned without it
+  bool PreVisit(Explain &explain) override {
+    CHECK(prev_ops_.empty());
+    prev_ops_.push_back(&explain);
+    return true;
+  }
+
+  bool PostVisit(Explain &explain) override {
+    // Set Explain as the final operator on master.
+    if (ShouldSplit()) {
+      auto input = explain.input();
+      auto pull_id = AddWorkerPlan(input);
+      Split(explain, std::make_shared<PullRemote>(
+                         input, pull_id,
+                         input->OutputSymbols(distributed_plan_.symbol_table)));
+    }
+    prev_ops_.pop_back();
+    return false;
+  }
+
   // Skip needs to skip only the first N results from *all* of the results.
   // Therefore, the earliest (deepest in the plan tree) encountered Skip will
   // break the plan in 2 parts.
@@ -1140,9 +1170,10 @@ class DistributedPlanner : public HierarchicalLogicalOperatorVisitor {
       // Shallow copy Distinct
       auto pull_id = AddWorkerPlan(std::make_shared<Distinct>(distinct));
       auto input = distinct.input();
-      Split(distinct, std::make_shared<PullRemote>(
-                          input, pull_id, input->OutputSymbols(
-                                              distributed_plan_.symbol_table)));
+      Split(distinct,
+            std::make_shared<PullRemote>(
+                input, pull_id,
+                input->OutputSymbols(distributed_plan_.symbol_table)));
     }
     return true;
   }
diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp
index da98353d6..3ba628763 100644
--- a/src/query/plan/operator.cpp
+++ b/src/query/plan/operator.cpp
@@ -31,6 +31,7 @@
 #include "query/frontend/semantic/symbol_table.hpp"
 #include "query/interpret/eval.hpp"
 #include "query/path.hpp"
+#include "query/plan/pretty_print.hpp"
 #include "utils/algorithm.hpp"
 #include "utils/exceptions.hpp"
 #include "utils/hashing/fnv.hpp"
@@ -4613,4 +4614,52 @@ std::unique_ptr<Cursor> TestStream::MakeCursor(
   return std::make_unique<TestStreamCursor>(*this, db);
 }
 
+Explain::Explain(const std::shared_ptr<LogicalOperator> &input,
+                 const Symbol &output_symbol)
+    : input_(input), output_symbol_(output_symbol) {}
+
+ACCEPT_WITH_INPUT(Explain);
+
+std::vector<Symbol> Explain::OutputSymbols(const SymbolTable &) const {
+  return {output_symbol_};
+}
+
+std::vector<Symbol> Explain::ModifiedSymbols(const SymbolTable &table) const {
+  return OutputSymbols(table);
+}
+
+class ExplainCursor : public Cursor {
+ public:
+  ExplainCursor(const Explain &self, const database::GraphDbAccessor &dba,
+                const Symbol &output_symbol)
+      : printed_plan_rows_([&dba, &self]() {
+          std::stringstream stream;
+          PrettyPrint(dba, self.input().get(), &stream);
+          return utils::Split(stream.str(), "\n");
+        }()),
+        print_it_(printed_plan_rows_.begin()),
+        output_symbol_(output_symbol) {}
+
+  bool Pull(Frame &frame, Context &ctx) override {
+    if (print_it_ != printed_plan_rows_.end()) {
+      frame[output_symbol_] = *print_it_;
+      print_it_++;
+      return true;
+    }
+    return false;
+  }
+
+  void Reset() override { print_it_ = printed_plan_rows_.begin(); }
+
+ private:
+  std::vector<std::string> printed_plan_rows_;
+  std::vector<std::string>::iterator print_it_;
+  Symbol output_symbol_;
+};
+
+std::unique_ptr<Cursor> Explain::MakeCursor(
+    database::GraphDbAccessor &dba) const {
+  return std::make_unique<ExplainCursor>(*this, dba, output_symbol_);
+}
+
 }  // namespace query::plan
diff --git a/src/query/plan/operator.lcp b/src/query/plan/operator.lcp
index 04c20a69d..bbea378e4 100644
--- a/src/query/plan/operator.lcp
+++ b/src/query/plan/operator.lcp
@@ -109,6 +109,7 @@ class ShowStreams;
 class StartStopStream;
 class StartStopAllStreams;
 class TestStream;
+class Explain;
 
 using LogicalOperatorCompositeVisitor = ::utils::CompositeVisitor<
     Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel,
@@ -118,7 +119,7 @@ using LogicalOperatorCompositeVisitor = ::utils::CompositeVisitor<
     ExpandUniquenessFilter<VertexAccessor>,
     ExpandUniquenessFilter<EdgeAccessor>, Accumulate, Aggregate, Skip, Limit,
     OrderBy, Merge, Optional, Unwind, Distinct, Union, PullRemote, Synchronize,
-    Cartesian, PullRemoteOrderBy>;
+    Cartesian, PullRemoteOrderBy, Explain>;
 
 using LogicalOperatorLeafVisitor =
     ::utils::LeafVisitor<Once, CreateIndex, AuthHandler, CreateStream,
@@ -2661,6 +2662,32 @@ in the db.")
     #>cpp TestStream() {} cpp<#)
   (:serialize :capnp))
 
+(lcp:define-class explain (logical-operator)
+  ((input "std::shared_ptr<LogicalOperator>"
+          :capnp-save #'save-operator-pointer
+          :capnp-load #'load-operator-pointer)
+   (output-symbol "Symbol" :reader t))
+  (:documentation "Pretty print a LogicalOperator plan")
+  (:public
+   #>cpp
+   Explain(const std::shared_ptr<LogicalOperator> &input,
+           const Symbol &output_symbol);
+
+   bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
+   std::unique_ptr<Cursor> MakeCursor(
+       database::GraphDbAccessor & db) const override;
+   std::vector<Symbol> OutputSymbols(const SymbolTable &) const override;
+   std::vector<Symbol> ModifiedSymbols(const SymbolTable &) const override;
+
+   bool HasSingleInput() const override { return true; }
+   std::shared_ptr<LogicalOperator> input() const override { return input_; }
+   void set_input(std::shared_ptr<LogicalOperator> input) override {
+     input_ = input;
+   }
+   cpp<#)
+  (:private
+   #>cpp Explain () {} cpp<#)
+  (:serialize :capnp))
 
 (lcp:pop-namespace) ;; plan
 (lcp:pop-namespace) ;; query
diff --git a/src/query/plan/planner.hpp b/src/query/plan/planner.hpp
index a3d9f651e..7e6d34bce 100644
--- a/src/query/plan/planner.hpp
+++ b/src/query/plan/planner.hpp
@@ -98,6 +98,12 @@ auto MakeLogicalPlan(TPlanningContext &context, const Parameters &parameters,
         prev_op, prev_op->OutputSymbols(context.symbol_table));
   }
 
+  if (context.ast_storage.query()->explain_) {
+    last_op = std::make_unique<Explain>(
+        std::move(last_op),
+        context.symbol_table.CreateSymbol("QUERY PLAN", false));
+  }
+
   return std::make_pair(std::move(last_op), total_cost);
 }
 
diff --git a/src/query/plan/preprocess.cpp b/src/query/plan/preprocess.cpp
index 79156f7bb..7e19d58e3 100644
--- a/src/query/plan/preprocess.cpp
+++ b/src/query/plan/preprocess.cpp
@@ -477,29 +477,28 @@ std::vector<SingleQueryPart> CollectSingleQueryParts(
   return query_parts;
 }
 
-QueryParts CollectQueryParts(SymbolTable &symbol_table,
-                             AstStorage &storage) {
-  auto query = storage.query();
+QueryParts CollectQueryParts(SymbolTable &symbol_table, AstStorage &storage) {
+  auto *query = storage.query();
   std::vector<QueryPart> query_parts;
+
+  auto *single_query = query->single_query_;
+  CHECK(single_query) << "Expected at least a single query";
+  query_parts.push_back(
+      QueryPart{CollectSingleQueryParts(symbol_table, storage, single_query)});
+
   bool distinct = false;
-
-  if (auto *single_query = query->single_query_) {
-    query_parts.push_back(QueryPart{
-        CollectSingleQueryParts(symbol_table, storage, single_query)});
-  }
-
   for (auto *cypher_union : query->cypher_unions_) {
     if (cypher_union->distinct_) {
       distinct = true;
     }
 
-    if (auto *single_query = cypher_union->single_query_) {
-      query_parts.push_back(QueryPart{
-          CollectSingleQueryParts(symbol_table, storage, single_query),
-          cypher_union});
-    }
+    auto *single_query = cypher_union->single_query_;
+    CHECK(single_query) << "Expected UNION to have a query";
+    query_parts.push_back(
+        QueryPart{CollectSingleQueryParts(symbol_table, storage, single_query),
+                  cypher_union});
   }
   return QueryParts{query_parts, distinct};
-};
+}
 
 }  // namespace query::plan
diff --git a/src/query/plan/pretty_print.cpp b/src/query/plan/pretty_print.cpp
new file mode 100644
index 000000000..2dfec7b1f
--- /dev/null
+++ b/src/query/plan/pretty_print.cpp
@@ -0,0 +1,311 @@
+#include "query/plan/pretty_print.hpp"
+
+#include "database/graph_db_accessor.hpp"
+#include "query/plan/operator.hpp"
+
+namespace query::plan {
+
+namespace {
+
+class PlanPrinter final : public HierarchicalLogicalOperatorVisitor {
+ public:
+  using HierarchicalLogicalOperatorVisitor::PostVisit;
+  using HierarchicalLogicalOperatorVisitor::PreVisit;
+  using HierarchicalLogicalOperatorVisitor::Visit;
+
+  explicit PlanPrinter(const database::GraphDbAccessor *dba, std::ostream *out)
+      : dba_(dba), out_(out) {}
+
+#define PRE_VISIT(TOp)                                   \
+  bool PreVisit(query::plan::TOp &) override {           \
+    WithPrintLn([](auto &out) { out << "* " << #TOp; }); \
+    return true;                                         \
+  }
+
+  PRE_VISIT(CreateNode);
+  PRE_VISIT(CreateExpand);
+  PRE_VISIT(Delete);
+
+  bool PreVisit(query::plan::ScanAll &op) override {
+    WithPrintLn([&](auto &out) {
+      out << "* ScanAll"
+          << " (" << op.output_symbol().name() << ")";
+    });
+    return true;
+  }
+
+  bool PreVisit(query::plan::ScanAllByLabel &op) override {
+    WithPrintLn([&](auto &out) {
+      out << "* ScanAllByLabel"
+          << " (" << op.output_symbol().name() << " :"
+          << dba_->LabelName(op.label()) << ")";
+    });
+    return true;
+  }
+
+  bool PreVisit(query::plan::ScanAllByLabelPropertyValue &op) override {
+    WithPrintLn([&](auto &out) {
+      out << "* ScanAllByLabelPropertyValue"
+          << " (" << op.output_symbol().name() << " :"
+          << dba_->LabelName(op.label()) << " {"
+          << dba_->PropertyName(op.property()) << "})";
+    });
+    return true;
+  }
+
+  bool PreVisit(query::plan::ScanAllByLabelPropertyRange &op) override {
+    WithPrintLn([&](auto &out) {
+      out << "* ScanAllByLabelPropertyRange"
+          << " (" << op.output_symbol().name() << " :"
+          << dba_->LabelName(op.label()) << " {"
+          << dba_->PropertyName(op.property()) << "})";
+    });
+    return true;
+  }
+
+  bool PreVisit(query::plan::Expand &op) override {
+    WithPrintLn([&](auto &out) {
+      out << "* Expand";
+      PrintExpand(out, op);
+    });
+    return true;
+  }
+
+  bool PreVisit(query::plan::ExpandVariable &op) override {
+    WithPrintLn([&](auto &out) {
+      out << "* ExpandVariable";
+      PrintExpand(out, op);
+    });
+    return true;
+  }
+
+  bool PreVisit(query::plan::Produce &op) override {
+    WithPrintLn([&](auto &out) {
+      out << "* Produce {";
+      utils::PrintIterable(
+          out, op.named_expressions(), ", ",
+          [](auto &out, const auto &nexpr) { out << nexpr->name_; });
+      out << "}";
+    });
+    return true;
+  }
+
+  PRE_VISIT(ConstructNamedPath);
+  PRE_VISIT(Filter);
+  PRE_VISIT(SetProperty);
+  PRE_VISIT(SetProperties);
+  PRE_VISIT(SetLabels);
+  PRE_VISIT(RemoveProperty);
+  PRE_VISIT(RemoveLabels);
+  PRE_VISIT(ExpandUniquenessFilter<VertexAccessor>);
+  PRE_VISIT(ExpandUniquenessFilter<EdgeAccessor>);
+  PRE_VISIT(Accumulate);
+
+  bool PreVisit(query::plan::Aggregate &op) override {
+    WithPrintLn([&](auto &out) {
+      out << "* Aggregate {";
+      utils::PrintIterable(
+          out, op.aggregations(), ", ",
+          [](auto &out, const auto &aggr) { out << aggr.output_sym.name(); });
+      out << "} {";
+      utils::PrintIterable(
+          out, op.remember(), ", ",
+          [](auto &out, const auto &sym) { out << sym.name(); });
+      out << "}";
+    });
+    return true;
+  }
+
+  PRE_VISIT(Skip);
+  PRE_VISIT(Limit);
+
+  bool PreVisit(query::plan::OrderBy &op) override {
+    WithPrintLn([&op](auto &out) {
+      out << "* OrderBy {";
+      utils::PrintIterable(
+          out, op.output_symbols(), ", ",
+          [](auto &out, const auto &sym) { out << sym.name(); });
+      out << "}";
+    });
+    return true;
+  }
+
+  bool PreVisit(query::plan::Merge &op) override {
+    WithPrintLn([](auto &out) { out << "* Merge"; });
+    Branch(*op.merge_match(), "On Match");
+    Branch(*op.merge_create(), "On Create");
+    op.input()->Accept(*this);
+    return false;
+  }
+
+  bool PreVisit(query::plan::Optional &op) override {
+    WithPrintLn([](auto &out) { out << "* Optional"; });
+    Branch(*op.optional());
+    op.input()->Accept(*this);
+    return false;
+  }
+
+  PRE_VISIT(Unwind);
+  PRE_VISIT(Distinct);
+
+  bool Visit(query::plan::Once &op) override {
+    // Ignore checking Once, it is implicitly at the end.
+    return true;
+  }
+
+  bool Visit(query::plan::CreateIndex &op) override {
+    WithPrintLn([](auto &out) { out << "* CreateIndex"; });
+    return true;
+  }
+
+  bool Visit(query::plan::AuthHandler &op) override {
+    WithPrintLn([](auto &out) { out << "* AuthHandler"; });
+    return true;
+  }
+
+  bool Visit(query::plan::CreateStream &op) override {
+    WithPrintLn([](auto &out) { out << "* CreateStream"; });
+    return true;
+  }
+
+  bool Visit(query::plan::DropStream &op) override {
+    WithPrintLn([](auto &out) { out << "* DropStream"; });
+    return true;
+  }
+
+  bool Visit(query::plan::ShowStreams &op) override {
+    WithPrintLn([](auto &out) { out << "* ShowStreams"; });
+    return true;
+  }
+
+  bool Visit(query::plan::StartStopStream &op) override {
+    WithPrintLn([](auto &out) { out << "* StartStopStream"; });
+    return true;
+  }
+
+  bool Visit(query::plan::StartStopAllStreams &op) override {
+    WithPrintLn([](auto &out) { out << "* StartStopAllStreams"; });
+    return true;
+  }
+
+  bool Visit(query::plan::TestStream &op) override {
+    WithPrintLn([](auto &out) { out << "* TestStream"; });
+    return true;
+  }
+
+  bool PreVisit(query::plan::Explain &explain) override {
+    WithPrintLn([&explain](auto &out) {
+      out << "* Explain {" << explain.output_symbol().name() << "}";
+    });
+    return true;
+  }
+
+  bool PreVisit(query::plan::PullRemote &op) override {
+    WithPrintLn([&op](auto &out) {
+      out << "* PullRemote [" << op.plan_id() << "] {";
+      utils::PrintIterable(
+          out, op.symbols(), ", ",
+          [](auto &out, const auto &sym) { out << sym.name(); });
+      out << "}";
+    });
+    WithPrintLn([](auto &out) { out << "|\\"; });
+    ++depth_;
+    WithPrintLn([](auto &out) { out << "* workers"; });
+    --depth_;
+    return true;
+  }
+
+  bool PreVisit(query::plan::Synchronize &op) override {
+    WithPrintLn([&op](auto &out) {
+      out << "* Synchronize";
+      if (op.advance_command()) out << " (ADV CMD)";
+    });
+    if (op.pull_remote()) Branch(*op.pull_remote());
+    op.input()->Accept(*this);
+    return false;
+  }
+
+  bool PreVisit(query::plan::Cartesian &op) override {
+    WithPrintLn([&op](auto &out) {
+      out << "* Cartesian {";
+      utils::PrintIterable(
+          out, op.left_symbols(), ", ",
+          [](auto &out, const auto &sym) { out << sym.name(); });
+      out << " : ";
+      utils::PrintIterable(
+          out, op.right_symbols(), ", ",
+          [](auto &out, const auto &sym) { out << sym.name(); });
+      out << "}";
+    });
+    Branch(*op.right_op());
+    op.left_op()->Accept(*this);
+    return false;
+  }
+
+  bool PreVisit(query::plan::PullRemoteOrderBy &op) override {
+    WithPrintLn([&op](auto &out) {
+      out << "* PullRemoteOrderBy {";
+      utils::PrintIterable(
+          out, op.symbols(), ", ",
+          [](auto &out, const auto &sym) { out << sym.name(); });
+      out << "}";
+    });
+
+    WithPrintLn([](auto &out) { out << "|\\"; });
+    ++depth_;
+    WithPrintLn([](auto &out) { out << "* workers"; });
+    --depth_;
+    return true;
+  }
+#undef PRE_VISIT
+
+ private:
+  bool DefaultPreVisit() override {
+    WithPrintLn([](auto &out) { out << "* Unknown operator!"; });
+    return true;
+  }
+
+  // Call fun with output stream. The stream is prefixed with amount of spaces
+  // corresponding to the current depth_.
+  template <class TFun>
+  void WithPrintLn(TFun fun) {
+    *out_ << " ";
+    for (int i = 0; i < depth_; ++i) {
+      *out_ << "|  ";
+    }
+    fun(*out_);
+    *out_ << std::endl;
+  }
+
+  // Forward this printer to another operator branch by incrementing the depth
+  // and printing the branch name.
+  void Branch(query::plan::LogicalOperator &op,
+              const std::string &branch_name = "") {
+    WithPrintLn([&](auto &out) { out << "|\\ " << branch_name; });
+    ++depth_;
+    op.Accept(*this);
+    --depth_;
+  }
+
+  void PrintExpand(std::ostream &out, const query::plan::ExpandCommon &op) {
+    out << " (" << op.input_symbol().name() << ")"
+        << (op.direction() == query::EdgeAtom::Direction::IN ? "<-" : "-")
+        << "[" << op.edge_symbol().name() << "]"
+        << (op.direction() == query::EdgeAtom::Direction::OUT ? "->" : "-")
+        << "(" << op.node_symbol().name() << ")";
+  }
+
+  int depth_ = 0;
+  const database::GraphDbAccessor *dba_{nullptr};
+  std::ostream *out_{nullptr};
+};
+
+}  // namespace
+
+void PrettyPrint(const database::GraphDbAccessor &dba,
+                 LogicalOperator *plan_root, std::ostream *out) {
+  PlanPrinter printer(&dba, out);
+  plan_root->Accept(printer);
+}
+
+}  // namespace query::plan
diff --git a/src/query/plan/pretty_print.hpp b/src/query/plan/pretty_print.hpp
new file mode 100644
index 000000000..2a24d0366
--- /dev/null
+++ b/src/query/plan/pretty_print.hpp
@@ -0,0 +1,27 @@
+/// @file
+#pragma once
+
+#include <iostream>
+
+namespace database {
+class GraphDbAccessor;
+}
+
+namespace query::plan {
+
+class LogicalOperator;
+
+/// Pretty print a `LogicalOperator` plan to a `std::ostream`.
+/// GraphDbAccessor is needed for resolving label and property names.
+/// Note that `plan_root` isn't modified, but we can't take it as a const
+/// because we don't have support for visiting a const LogicalOperator.
+void PrettyPrint(const database::GraphDbAccessor &dba,
+                 LogicalOperator *plan_root, std::ostream *out);
+
+/// Overload of `PrettyPrint` which defaults the `std::ostream` to `std::cout`.
+inline void PrettyPrint(const database::GraphDbAccessor &dba,
+                        LogicalOperator *plan_root) {
+  PrettyPrint(dba, plan_root, &std::cout);
+}
+
+}  // namespace query::plan
diff --git a/tests/manual/query_planner.cpp b/tests/manual/query_planner.cpp
index aa22fff1e..83b32e9f9 100644
--- a/tests/manual/query_planner.cpp
+++ b/tests/manual/query_planner.cpp
@@ -21,6 +21,7 @@
 #include "query/plan/cost_estimator.hpp"
 #include "query/plan/distributed.hpp"
 #include "query/plan/planner.hpp"
+#include "query/plan/pretty_print.hpp"
 #include "query/typed_value.hpp"
 #include "utils/hashing/fnv.hpp"
 #include "utils/string.hpp"
@@ -364,285 +365,6 @@ class InteractiveDbAccessor {
   }
 };
 
-class PlanPrinter : public query::plan::HierarchicalLogicalOperatorVisitor {
- public:
-  using HierarchicalLogicalOperatorVisitor::PostVisit;
-  using HierarchicalLogicalOperatorVisitor::PreVisit;
-  using HierarchicalLogicalOperatorVisitor::Visit;
-
-  explicit PlanPrinter(database::GraphDbAccessor &dba) : dba_(dba) {}
-
-#define PRE_VISIT(TOp)                                   \
-  bool PreVisit(query::plan::TOp &) override {           \
-    WithPrintLn([](auto &out) { out << "* " << #TOp; }); \
-    return true;                                         \
-  }
-
-  PRE_VISIT(CreateNode);
-  PRE_VISIT(CreateExpand);
-  PRE_VISIT(Delete);
-
-  bool PreVisit(query::plan::ScanAll &op) override {
-    WithPrintLn([&](auto &out) {
-      out << "* ScanAll"
-          << " (" << op.output_symbol().name() << ")";
-    });
-    return true;
-  }
-
-  bool PreVisit(query::plan::ScanAllByLabel &op) override {
-    WithPrintLn([&](auto &out) {
-      out << "* ScanAllByLabel"
-          << " (" << op.output_symbol().name() << " :"
-          << dba_.LabelName(op.label()) << ")";
-    });
-    return true;
-  }
-
-  bool PreVisit(query::plan::ScanAllByLabelPropertyValue &op) override {
-    WithPrintLn([&](auto &out) {
-      out << "* ScanAllByLabelPropertyValue"
-          << " (" << op.output_symbol().name() << " :"
-          << dba_.LabelName(op.label()) << " {"
-          << dba_.PropertyName(op.property()) << "})";
-    });
-    return true;
-  }
-
-  bool PreVisit(query::plan::ScanAllByLabelPropertyRange &op) override {
-    WithPrintLn([&](auto &out) {
-      out << "* ScanAllByLabelPropertyRange"
-          << " (" << op.output_symbol().name() << " :"
-          << dba_.LabelName(op.label()) << " {"
-          << dba_.PropertyName(op.property()) << "})";
-    });
-    return true;
-  }
-
-  bool PreVisit(query::plan::Expand &op) override {
-    WithPrintLn([&](auto &out) {
-      out << "* Expand";
-      PrintExpand(out, op);
-    });
-    return true;
-  }
-
-  bool PreVisit(query::plan::ExpandVariable &op) override {
-    WithPrintLn([&](auto &out) {
-      out << "* ExpandVariable";
-      PrintExpand(out, op);
-    });
-    return true;
-  }
-
-  bool PreVisit(query::plan::Produce &op) override {
-    WithPrintLn([&](auto &out) {
-      out << "* Produce {";
-      utils::PrintIterable(
-          out, op.named_expressions(), ", ",
-          [](auto &out, const auto &nexpr) { out << nexpr->name_; });
-      out << "}";
-    });
-    return true;
-  }
-
-  PRE_VISIT(ConstructNamedPath);
-  PRE_VISIT(Filter);
-  PRE_VISIT(SetProperty);
-  PRE_VISIT(SetProperties);
-  PRE_VISIT(SetLabels);
-  PRE_VISIT(RemoveProperty);
-  PRE_VISIT(RemoveLabels);
-  PRE_VISIT(ExpandUniquenessFilter<VertexAccessor>);
-  PRE_VISIT(ExpandUniquenessFilter<EdgeAccessor>);
-  PRE_VISIT(Accumulate);
-
-  bool PreVisit(query::plan::Aggregate &op) override {
-    WithPrintLn([&](auto &out) {
-      out << "* Aggregate {";
-      utils::PrintIterable(
-          out, op.aggregations(), ", ",
-          [](auto &out, const auto &aggr) { out << aggr.output_sym.name(); });
-      out << "} {";
-      utils::PrintIterable(
-          out, op.remember(), ", ",
-          [](auto &out, const auto &sym) { out << sym.name(); });
-      out << "}";
-    });
-    return true;
-  }
-
-  PRE_VISIT(Skip);
-  PRE_VISIT(Limit);
-
-  bool PreVisit(query::plan::OrderBy &op) override {
-    WithPrintLn([&op](auto &out) {
-      out << "* OrderBy {";
-      utils::PrintIterable(
-          out, op.output_symbols(), ", ",
-          [](auto &out, const auto &sym) { out << sym.name(); });
-      out << "}";
-    });
-    return true;
-  }
-
-  bool PreVisit(query::plan::Merge &op) override {
-    WithPrintLn([](auto &out) { out << "* Merge"; });
-    Branch(*op.merge_match(), "On Match");
-    Branch(*op.merge_create(), "On Create");
-    op.input()->Accept(*this);
-    return false;
-  }
-
-  bool PreVisit(query::plan::Optional &op) override {
-    WithPrintLn([](auto &out) { out << "* Optional"; });
-    Branch(*op.optional());
-    op.input()->Accept(*this);
-    return false;
-  }
-
-  PRE_VISIT(Unwind);
-  PRE_VISIT(Distinct);
-
-  bool Visit(query::plan::Once &op) override {
-    // Ignore checking Once, it is implicitly at the end.
-    return true;
-  }
-
-  bool Visit(query::plan::CreateIndex &op) override {
-    WithPrintLn([](auto &out) { out << "* CreateIndex"; });
-    return true;
-  }
-
-  bool Visit(query::plan::AuthHandler &op) override {
-    WithPrintLn([](auto &out) { out << "* AuthHandler"; });
-    return true;
-  }
-
-  bool Visit(query::plan::CreateStream &op) override {
-    WithPrintLn([](auto &out) { out << "* CreateStream"; });
-    return true;
-  }
-
-  bool Visit(query::plan::DropStream &op) override {
-    WithPrintLn([](auto &out) { out << "* DropStream"; });
-    return true;
-  }
-
-  bool Visit(query::plan::ShowStreams &op) override {
-    WithPrintLn([](auto &out) { out << "* ShowStreams"; });
-    return true;
-  }
-
-  bool Visit(query::plan::StartStopStream &op) override {
-    WithPrintLn([](auto &out) { out << "* StartStopStream"; });
-    return true;
-  }
-
-  bool Visit(query::plan::StartStopAllStreams &op) override {
-    WithPrintLn([](auto &out) { out << "* StartStopAllStreams"; });
-    return true;
-  }
-
-  bool Visit(query::plan::TestStream &op) override {
-    WithPrintLn([](auto &out) { out << "* TestStream"; });
-    return true;
-  }
-
-  bool PreVisit(query::plan::PullRemote &op) override {
-    WithPrintLn([&op](auto &out) {
-      out << "* PullRemote [" << op.plan_id() << "] {";
-      utils::PrintIterable(
-          out, op.symbols(), ", ",
-          [](auto &out, const auto &sym) { out << sym.name(); });
-      out << "}";
-    });
-    WithPrintLn([](auto &out) { out << "|\\"; });
-    ++depth_;
-    WithPrintLn([](auto &out) { out << "* workers"; });
-    --depth_;
-    return true;
-  }
-
-  bool PreVisit(query::plan::Synchronize &op) override {
-    WithPrintLn([&op](auto &out) {
-      out << "* Synchronize";
-      if (op.advance_command()) out << " (ADV CMD)";
-    });
-    if (op.pull_remote()) Branch(*op.pull_remote());
-    op.input()->Accept(*this);
-    return false;
-  }
-
-  bool PreVisit(query::plan::Cartesian &op) override {
-    WithPrintLn([&op](auto &out) {
-      out << "* Cartesian {";
-      utils::PrintIterable(
-          out, op.left_symbols(), ", ",
-          [](auto &out, const auto &sym) { out << sym.name(); });
-      out << " : ";
-      utils::PrintIterable(
-          out, op.right_symbols(), ", ",
-          [](auto &out, const auto &sym) { out << sym.name(); });
-      out << "}";
-    });
-    Branch(*op.right_op());
-    op.left_op()->Accept(*this);
-    return false;
-  }
-
-  bool PreVisit(query::plan::PullRemoteOrderBy &op) override {
-    WithPrintLn([&op](auto &out) {
-      out << "* PullRemoteOrderBy {";
-      utils::PrintIterable(
-          out, op.symbols(), ", ",
-          [](auto &out, const auto &sym) { out << sym.name(); });
-      out << "}";
-    });
-
-    WithPrintLn([](auto &out) { out << "|\\"; });
-    ++depth_;
-    WithPrintLn([](auto &out) { out << "* workers"; });
-    --depth_;
-    return true;
-  }
-#undef PRE_VISIT
-
- private:
-  // Call fun with output stream. The stream is prefixed with amount of spaces
-  // corresponding to the current depth_.
-  template <class TFun>
-  void WithPrintLn(TFun fun) {
-    std::cout << " ";
-    for (int i = 0; i < depth_; ++i) {
-      std::cout << "|  ";
-    }
-    fun(std::cout);
-    std::cout << std::endl;
-  }
-
-  // Forward this printer to another operator branch by incrementing the depth
-  // and printing the branch name.
-  void Branch(query::plan::LogicalOperator &op,
-              const std::string &branch_name = "") {
-    WithPrintLn([&](auto &out) { out << "|\\ " << branch_name; });
-    ++depth_;
-    op.Accept(*this);
-    --depth_;
-  }
-
-  void PrintExpand(std::ostream &out, const query::plan::ExpandCommon &op) {
-    out << " (" << op.input_symbol().name() << ")"
-        << (op.direction() == query::EdgeAtom::Direction::IN ? "<-" : "-")
-        << "[" << op.edge_symbol().name() << "]"
-        << (op.direction() == query::EdgeAtom::Direction::OUT ? "->" : "-")
-        << "(" << op.node_symbol().name() << ")";
-  }
-
-  int depth_ = 0;
-  database::GraphDbAccessor &dba_;
-};
-
 // Shorthand for a vector of pairs (logical_plan, cost).
 typedef std::vector<
     std::pair<std::unique_ptr<query::plan::LogicalOperator>, double>>
@@ -671,13 +393,12 @@ DEFCOMMAND(Top) {
   std::stringstream ss(args[0]);
   ss >> n_plans;
   if (ss.fail() || !ss.eof()) return;
-  PlanPrinter printer(dba);
   n_plans = std::min(static_cast<int64_t>(plans.size()), n_plans);
   for (int64_t i = 0; i < n_plans; ++i) {
     auto &plan_pair = plans[i];
     std::cout << "---- Plan #" << i << " ---- " << std::endl;
     std::cout << "cost: " << plan_pair.second << std::endl;
-    plan_pair.first->Accept(printer);
+    query::plan::PrettyPrint(dba, plan_pair.first.get());
     std::cout << std::endl;
   }
 }
@@ -690,8 +411,7 @@ DEFCOMMAND(Show) {
   const auto &plan = plans[plan_ix].first;
   auto cost = plans[plan_ix].second;
   std::cout << "Plan cost: " << cost << std::endl;
-  PlanPrinter printer(dba);
-  plan->Accept(printer);
+  query::plan::PrettyPrint(dba, plan.get());
 }
 
 DEFCOMMAND(ShowDistributed) {
@@ -704,8 +424,7 @@ DEFCOMMAND(ShowDistributed) {
   auto distributed_plan = MakeDistributedPlan(*plan, symbol_table, plan_id);
   {
     std::cout << "---- Master Plan ---- " << std::endl;
-    PlanPrinter printer(dba);
-    distributed_plan.master_plan->Accept(printer);
+    query::plan::PrettyPrint(dba, distributed_plan.master_plan.get());
     std::cout << std::endl;
   }
   for (size_t i = 0; i < distributed_plan.worker_plans.size(); ++i) {
@@ -713,8 +432,7 @@ DEFCOMMAND(ShowDistributed) {
     std::shared_ptr<query::plan::LogicalOperator> worker_plan;
     std::tie(id, worker_plan) = distributed_plan.worker_plans[i];
     std::cout << "---- Worker Plan #" << id << " ---- " << std::endl;
-    PlanPrinter printer(dba);
-    worker_plan->Accept(printer);
+    query::plan::PrettyPrint(dba, worker_plan.get());
     std::cout << std::endl;
   }
 }
diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp
index e3ca7d99e..6132568d3 100644
--- a/tests/unit/cypher_main_visitor.cpp
+++ b/tests/unit/cypher_main_visitor.cpp
@@ -2330,4 +2330,33 @@ TYPED_TEST(CypherMainVisitorTest, TestStream) {
                SyntaxException);
 }
 
+TYPED_TEST(CypherMainVisitorTest, TestExplainRegularQuery) {
+  {
+    TypeParam ast_generator("RETURN n");
+    EXPECT_FALSE(ast_generator.query_->explain_);
+  }
+  {
+    TypeParam ast_generator("EXPLAIN RETURN n");
+    EXPECT_TRUE(ast_generator.query_->explain_);
+  }
+}
+
+TYPED_TEST(CypherMainVisitorTest, TestExplainExplainQuery) {
+  EXPECT_THROW(TypeParam ast_generator("EXPLAIN EXPLAIN RETURN n"),
+               SyntaxException);
+}
+
+TYPED_TEST(CypherMainVisitorTest, TestExplainAuthQuery) {
+  TypeParam ast_generator("SHOW ROLES");
+  EXPECT_FALSE(ast_generator.query_->explain_);
+  EXPECT_THROW(TypeParam ast_generator("EXPLAIN SHOW ROLES"), SyntaxException);
+}
+
+TYPED_TEST(CypherMainVisitorTest, TestExplainStreamQuery) {
+  TypeParam ast_generator("SHOW STREAMS");
+  EXPECT_FALSE(ast_generator.query_->explain_);
+  EXPECT_THROW(TypeParam ast_generator("EXPLAIN SHOW STREAMS"),
+               SyntaxException);
+}
+
 }  // namespace
diff --git a/tests/unit/query_planner.cpp b/tests/unit/query_planner.cpp
index 8bb666b9a..797191d75 100644
--- a/tests/unit/query_planner.cpp
+++ b/tests/unit/query_planner.cpp
@@ -141,6 +141,8 @@ class PlanChecker : public HierarchicalLogicalOperatorVisitor {
   VISIT(StartStopAllStreams);
   VISIT(TestStream);
 
+  PRE_VISIT(Explain);
+
 #undef PRE_VISIT
 #undef VISIT
 
@@ -394,6 +396,8 @@ class ExpectAuthHandler : public OpChecker<AuthHandler> {
     EXPECT_EQ(auth_handler.user(), user_);
     EXPECT_EQ(auth_handler.role(), role_);
     EXPECT_EQ(auth_handler.user_or_role(), user_or_role_);
+    // TODO(mtomic): We need to somehow test the password expression.
+    EXPECT_TRUE(password_);
     EXPECT_TRUE(auth_handler.password());
     EXPECT_EQ(auth_handler.privileges(), privileges_);
   }