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 ¶meters, 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_); }