From 0f77c8582401a1f4d86333e30b8264c3ea896bd4 Mon Sep 17 00:00:00 2001 From: Andi Date: Fri, 9 Dec 2022 11:44:07 +0100 Subject: [PATCH] Fix cursor exhaustion by adding EmptyResult operator (#667) --- src/query/plan/operator.cpp | 51 +++++++++++++++++++ src/query/plan/operator.lcp | 38 +++++++++++++- src/query/plan/pretty_print.cpp | 12 +++++ src/query/plan/pretty_print.hpp | 2 + src/query/plan/read_write_type_checker.cpp | 12 +++-- src/query/plan/read_write_type_checker.hpp | 1 + src/query/plan/rewrite/index_lookup.hpp | 9 ++++ src/query/plan/rule_based_planner.hpp | 11 +++- src/utils/event_counter.cpp | 1 + .../features/update_clauses.feature | 16 ++++++ tests/unit/interpreter.cpp | 2 +- tests/unit/query_plan.cpp | 41 ++++++++------- tests/unit/query_plan_checker.hpp | 2 + 13 files changed, 171 insertions(+), 27 deletions(-) diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index d37f0b141..07ba7c877 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -114,6 +114,7 @@ extern const Event UnionOperator; extern const Event CartesianOperator; extern const Event CallProcedureOperator; extern const Event ForeachOperator; +extern const Event EmptyResultOperator; } // namespace EventCounter namespace memgraph::query::plan { @@ -3058,6 +3059,56 @@ void EdgeUniquenessFilter::EdgeUniquenessFilterCursor::Shutdown() { input_cursor void EdgeUniquenessFilter::EdgeUniquenessFilterCursor::Reset() { input_cursor_->Reset(); } +EmptyResult::EmptyResult(const std::shared_ptr &input) + : input_(input ? input : std::make_shared()) {} + +ACCEPT_WITH_INPUT(EmptyResult) + +std::vector EmptyResult::OutputSymbols(const SymbolTable &) const { // NOLINT(hicpp-named-parameter) + return {}; +} + +std::vector EmptyResult::ModifiedSymbols(const SymbolTable &) const { // NOLINT(hicpp-named-parameter) + return {}; +} + +class EmptyResultCursor : public Cursor { + public: + EmptyResultCursor(const EmptyResult &self, utils::MemoryResource *mem) + : input_cursor_(self.input_->MakeCursor(mem)) {} + + bool Pull(Frame &frame, ExecutionContext &context) override { + SCOPED_PROFILE_OP("EmptyResult"); + + if (!pulled_all_input_) { + while (input_cursor_->Pull(frame, context)) { + if (MustAbort(context)) { + throw HintedAbortError(); + } + } + pulled_all_input_ = true; + } + return false; + } + + void Shutdown() override { input_cursor_->Shutdown(); } + + void Reset() override { + input_cursor_->Reset(); + pulled_all_input_ = false; + } + + private: + const UniqueCursorPtr input_cursor_; + bool pulled_all_input_{false}; +}; + +UniqueCursorPtr EmptyResult::MakeCursor(utils::MemoryResource *mem) const { + EventCounter::IncrementCounter(EventCounter::EmptyResultOperator); + + return MakeUniqueCursorPtr(mem, *this, mem); +} + Accumulate::Accumulate(const std::shared_ptr &input, const std::vector &symbols, bool advance_command) : input_(input), symbols_(symbols), advance_command_(advance_command) {} diff --git a/src/query/plan/operator.lcp b/src/query/plan/operator.lcp index 195a09088..4b1e10ce1 100644 --- a/src/query/plan/operator.lcp +++ b/src/query/plan/operator.lcp @@ -132,6 +132,7 @@ class Cartesian; class CallProcedure; class LoadCsv; class Foreach; +class EmptyResult; using LogicalOperatorCompositeVisitor = utils::CompositeVisitor< Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel, @@ -140,7 +141,7 @@ using LogicalOperatorCompositeVisitor = utils::CompositeVisitor< Expand, ExpandVariable, ConstructNamedPath, Filter, Produce, Delete, SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels, EdgeUniquenessFilter, Accumulate, Aggregate, Skip, Limit, OrderBy, Merge, - Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv, Foreach>; + Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv, Foreach, EmptyResult>; using LogicalOperatorLeafVisitor = utils::LeafVisitor; @@ -1554,6 +1555,41 @@ edge lists).") (:serialize (:slk)) (:clone)) + +(lcp:define-class empty-result (logical-operator) + ((input "std::shared_ptr" :scope :public + :slk-save #'slk-save-operator-pointer + :slk-load #'slk-load-operator-pointer)) + (:documentation + "Pulls everything from the input and discards it. + +On the first Pull from this operator's Cursor the input Cursor will be Pulled +until it is empty. The results won't be accumulated in the temporary cache. + +This technique is used for ensuring that the cursor has been exhausted after +a WriteHandleClause. A typical use case is a `MATCH--SET` query with RETURN statement +missing. +@param input Input @c LogicalOperator. ") + (:public + #>cpp + EmptyResult() {} + + EmptyResult(const std::shared_ptr &input); + bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override; + UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override; + std::vector OutputSymbols(const SymbolTable &) const override; + std::vector ModifiedSymbols(const SymbolTable &) const override; + + bool HasSingleInput() const override { return true; } + std::shared_ptr input() const override { return input_; } + void set_input(std::shared_ptr input) override { + input_ = input; + } + cpp<#) + (:serialize (:slk)) + (:clone)) + + (lcp:define-class accumulate (logical-operator) ((input "std::shared_ptr" :scope :public :slk-save #'slk-save-operator-pointer diff --git a/src/query/plan/pretty_print.cpp b/src/query/plan/pretty_print.cpp index 99b4826a6..e800fdb7b 100644 --- a/src/query/plan/pretty_print.cpp +++ b/src/query/plan/pretty_print.cpp @@ -156,6 +156,7 @@ PRE_VISIT(RemoveProperty); PRE_VISIT(RemoveLabels); PRE_VISIT(EdgeUniquenessFilter); PRE_VISIT(Accumulate); +PRE_VISIT(EmptyResult); bool PlanPrinter::PreVisit(query::plan::Aggregate &op) { WithPrintLn([&](auto &out) { @@ -705,6 +706,17 @@ bool PlanToJsonVisitor::PreVisit(EdgeUniquenessFilter &op) { return false; } +bool PlanToJsonVisitor::PreVisit(EmptyResult &op) { + json self; + self["name"] = "EmptyResult"; + + op.input_->Accept(*this); + self["input"] = PopOutput(); + + output_ = std::move(self); + return false; +} + bool PlanToJsonVisitor::PreVisit(Accumulate &op) { json self; self["name"] = "Accumulate"; diff --git a/src/query/plan/pretty_print.hpp b/src/query/plan/pretty_print.hpp index 44ae102a5..dcd9c8095 100644 --- a/src/query/plan/pretty_print.hpp +++ b/src/query/plan/pretty_print.hpp @@ -80,6 +80,7 @@ class PlanPrinter : public virtual HierarchicalLogicalOperatorVisitor { bool PreVisit(Optional &) override; bool PreVisit(Cartesian &) override; + bool PreVisit(EmptyResult &) override; bool PreVisit(Produce &) override; bool PreVisit(Accumulate &) override; bool PreVisit(Aggregate &) override; @@ -195,6 +196,7 @@ class PlanToJsonVisitor : public virtual HierarchicalLogicalOperatorVisitor { bool PreVisit(ScanAllByLabelProperty &) override; bool PreVisit(ScanAllById &) override; + bool PreVisit(EmptyResult &) override; bool PreVisit(Produce &) override; bool PreVisit(Accumulate &) override; bool PreVisit(Aggregate &) override; diff --git a/src/query/plan/read_write_type_checker.cpp b/src/query/plan/read_write_type_checker.cpp index 1d2e752d4..94252eb3f 100644 --- a/src/query/plan/read_write_type_checker.cpp +++ b/src/query/plan/read_write_type_checker.cpp @@ -11,10 +11,11 @@ #include "query/plan/read_write_type_checker.hpp" -#define PRE_VISIT(TOp, RWType, continue_visiting) \ - bool ReadWriteTypeChecker::PreVisit(TOp &op) { \ - UpdateType(RWType); \ - return continue_visiting; \ +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define PRE_VISIT(TOp, RWType, continue_visiting) \ + bool ReadWriteTypeChecker::PreVisit(TOp &) { /*NOLINT(bugprone-macro-parentheses)*/ \ + UpdateType(RWType); \ + return continue_visiting; \ } namespace memgraph::query::plan { @@ -54,6 +55,7 @@ bool ReadWriteTypeChecker::PreVisit(Cartesian &op) { return false; } +PRE_VISIT(EmptyResult, RWType::NONE, true) PRE_VISIT(Produce, RWType::NONE, true) PRE_VISIT(Accumulate, RWType::NONE, true) PRE_VISIT(Aggregate, RWType::NONE, true) @@ -86,7 +88,7 @@ bool ReadWriteTypeChecker::PreVisit([[maybe_unused]] Foreach &op) { #undef PRE_VISIT -bool ReadWriteTypeChecker::Visit(Once &op) { return false; } +bool ReadWriteTypeChecker::Visit(Once &) { return false; } // NOLINT(hicpp-named-parameter) void ReadWriteTypeChecker::UpdateType(RWType op_type) { // Update type only if it's not the NONE type and the current operator's type diff --git a/src/query/plan/read_write_type_checker.hpp b/src/query/plan/read_write_type_checker.hpp index 8b7f53987..42d9569b0 100644 --- a/src/query/plan/read_write_type_checker.hpp +++ b/src/query/plan/read_write_type_checker.hpp @@ -73,6 +73,7 @@ class ReadWriteTypeChecker : public virtual HierarchicalLogicalOperatorVisitor { bool PreVisit(Optional &) override; bool PreVisit(Cartesian &) override; + bool PreVisit(EmptyResult &) override; bool PreVisit(Produce &) override; bool PreVisit(Accumulate &) override; bool PreVisit(Aggregate &) override; diff --git a/src/query/plan/rewrite/index_lookup.hpp b/src/query/plan/rewrite/index_lookup.hpp index 791379018..d832645e1 100644 --- a/src/query/plan/rewrite/index_lookup.hpp +++ b/src/query/plan/rewrite/index_lookup.hpp @@ -298,6 +298,15 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor { return true; } + bool PreVisit(EmptyResult &op) override { + prev_ops_.push_back(&op); + return true; + } + bool PostVisit(EmptyResult &) override { + prev_ops_.pop_back(); + return true; + } + bool PreVisit(Delete &op) override { prev_ops_.push_back(&op); return true; diff --git a/src/query/plan/rule_based_planner.hpp b/src/query/plan/rule_based_planner.hpp index 563768ea0..e95befe7a 100644 --- a/src/query/plan/rule_based_planner.hpp +++ b/src/query/plan/rule_based_planner.hpp @@ -180,7 +180,7 @@ class RuleBasedPlanner { } } uint64_t merge_id = 0; - for (auto *clause : query_part.remaining_clauses) { + for (const auto &clause : query_part.remaining_clauses) { MG_ASSERT(!utils::IsSubtype(*clause, Match::kType), "Unexpected Match in remaining clauses"); if (auto *ret = utils::Downcast(clause)) { input_op = impl::GenReturn(*ret, std::move(input_op), *context.symbol_table, is_write, context.bound_symbols, @@ -203,6 +203,7 @@ class RuleBasedPlanner { context.bound_symbols.insert(symbol); input_op = std::make_unique(std::move(input_op), unwind->named_expression_->expression_, symbol); + } else if (auto *call_proc = utils::Downcast(clause)) { std::vector result_symbols; result_symbols.reserve(call_proc->result_identifiers_.size()); @@ -224,6 +225,7 @@ class RuleBasedPlanner { input_op = std::make_unique(std::move(input_op), load_csv->file_, load_csv->with_header_, load_csv->ignore_bad_, load_csv->delimiter_, load_csv->quote_, row_sym); + } else if (auto *foreach = utils::Downcast(clause)) { is_write = true; input_op = HandleForeachClause(foreach, std::move(input_op), *context.symbol_table, context.bound_symbols, @@ -233,6 +235,10 @@ class RuleBasedPlanner { } } } + // Is this the only situation that should be covered + if (input_op->OutputSymbols(*context.symbol_table).empty()) { + input_op = std::make_unique(std::move(input_op)); + } return input_op; } @@ -418,7 +424,8 @@ class RuleBasedPlanner { std::optional weight_lambda; std::optional total_weight; - if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) { + if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || + edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) { weight_lambda.emplace(ExpansionLambda{symbol_table.at(*edge->weight_lambda_.inner_edge), symbol_table.at(*edge->weight_lambda_.inner_node), edge->weight_lambda_.expression}); diff --git a/src/utils/event_counter.cpp b/src/utils/event_counter.cpp index 634b6eae2..c011a4503 100644 --- a/src/utils/event_counter.cpp +++ b/src/utils/event_counter.cpp @@ -37,6 +37,7 @@ M(RemovePropertyOperator, "Number of times RemoveProperty operator was used.") \ M(RemoveLabelsOperator, "Number of times RemoveLabels operator was used.") \ M(EdgeUniquenessFilterOperator, "Number of times EdgeUniquenessFilter operator was used.") \ + M(EmptyResultOperator, "Number of times EmptyResult operator was used.") \ M(AccumulateOperator, "Number of times Accumulate operator was used.") \ M(AggregateOperator, "Number of times Aggregate operator was used.") \ M(SkipOperator, "Number of times Skip operator was used.") \ diff --git a/tests/gql_behave/tests/memgraph_V1/features/update_clauses.feature b/tests/gql_behave/tests/memgraph_V1/features/update_clauses.feature index fd1a2d7d4..ddd204cdd 100644 --- a/tests/gql_behave/tests/memgraph_V1/features/update_clauses.feature +++ b/tests/gql_behave/tests/memgraph_V1/features/update_clauses.feature @@ -97,6 +97,22 @@ Feature: Update clauses | a | b | c | | (:q{x: 'y'}) | [:X{x: 'y'}] | ({y: 't'}) | + Scenario: Match node set properties without return + Given an empty graph + And having executed + """ + CREATE (n1:Node {test: 1}) + CREATE (n2:Node {test: 2}) + CREATE (n3:Node {test: 3}) + """ + When executing query: + """ + MATCH (n:Node) + SET n.test = 4 + """ + Then the result should be empty + + Scenario: Match, set properties from relationship to relationship, return test Given an empty graph When executing query: diff --git a/tests/unit/interpreter.cpp b/tests/unit/interpreter.cpp index 276cae48b..4097a8cab 100644 --- a/tests/unit/interpreter.cpp +++ b/tests/unit/interpreter.cpp @@ -853,7 +853,7 @@ TEST_F(InterpreterTest, ProfileQueryWithLiterals) { auto stream = Interpret("PROFILE UNWIND range(1, 1000) AS x CREATE (:Node {id: x});", {}); std::vector expected_header{"OPERATOR", "ACTUAL HITS", "RELATIVE TIME", "ABSOLUTE TIME"}; EXPECT_EQ(stream.GetHeader(), expected_header); - std::vector expected_rows{"* CreateNode", "* Unwind", "* Once"}; + std::vector expected_rows{"* EmptyResult", "* CreateNode", "* Unwind", "* Once"}; ASSERT_EQ(stream.GetResults().size(), expected_rows.size()); auto expected_it = expected_rows.begin(); for (const auto &row : stream.GetResults()) { diff --git a/tests/unit/query_plan.cpp b/tests/unit/query_plan.cpp index ce9f74dc9..24812b26e 100644 --- a/tests/unit/query_plan.cpp +++ b/tests/unit/query_plan.cpp @@ -121,14 +121,14 @@ TYPED_TEST(TestPlanner, CreateExpand) { FakeDbAccessor dba; auto relationship = "relationship"; auto *query = QUERY(SINGLE_QUERY(CREATE(PATTERN(NODE("n"), EDGE("r", Direction::OUT, {relationship}), NODE("m"))))); - CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateExpand()); + CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectEmptyResult()); } TYPED_TEST(TestPlanner, CreateMultipleNode) { // Test CREATE (n), (m) AstStorage storage; auto *query = QUERY(SINGLE_QUERY(CREATE(PATTERN(NODE("n")), PATTERN(NODE("m"))))); - CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateNode()); + CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateNode(), ExpectEmptyResult()); } TYPED_TEST(TestPlanner, CreateNodeExpandNode) { @@ -138,7 +138,8 @@ TYPED_TEST(TestPlanner, CreateNodeExpandNode) { auto relationship = "rel"; auto *query = QUERY(SINGLE_QUERY( CREATE(PATTERN(NODE("n"), EDGE("r", Direction::OUT, {relationship}), NODE("m")), PATTERN(NODE("l"))))); - CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectCreateNode()); + CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectCreateNode(), + ExpectEmptyResult()); } TYPED_TEST(TestPlanner, CreateNamedPattern) { @@ -148,7 +149,8 @@ TYPED_TEST(TestPlanner, CreateNamedPattern) { auto relationship = "rel"; auto *query = QUERY(SINGLE_QUERY(CREATE(NAMED_PATTERN("p", NODE("n"), EDGE("r", Direction::OUT, {relationship}), NODE("m"))))); - CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectConstructNamedPath()); + CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectConstructNamedPath(), + ExpectEmptyResult()); } TYPED_TEST(TestPlanner, MatchCreateExpand) { @@ -158,7 +160,7 @@ TYPED_TEST(TestPlanner, MatchCreateExpand) { auto relationship = "relationship"; auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), CREATE(PATTERN(NODE("n"), EDGE("r", Direction::OUT, {relationship}), NODE("m"))))); - CheckPlan(query, storage, ExpectScanAll(), ExpectCreateExpand()); + CheckPlan(query, storage, ExpectScanAll(), ExpectCreateExpand(), ExpectEmptyResult()); } TYPED_TEST(TestPlanner, MatchLabeledNodes) { @@ -260,7 +262,7 @@ TYPED_TEST(TestPlanner, MatchDelete) { // Test MATCH (n) DELETE n AstStorage storage; auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), DELETE(IDENT("n")))); - CheckPlan(query, storage, ExpectScanAll(), ExpectDelete()); + CheckPlan(query, storage, ExpectScanAll(), ExpectDelete(), ExpectEmptyResult()); } TYPED_TEST(TestPlanner, MatchNodeSet) { @@ -271,7 +273,8 @@ TYPED_TEST(TestPlanner, MatchNodeSet) { auto label = "label"; auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), SET(PROPERTY_LOOKUP("n", prop), LITERAL(42)), SET("n", IDENT("n")), SET("n", {label}))); - CheckPlan(query, storage, ExpectScanAll(), ExpectSetProperty(), ExpectSetProperties(), ExpectSetLabels()); + CheckPlan(query, storage, ExpectScanAll(), ExpectSetProperty(), ExpectSetProperties(), ExpectSetLabels(), + ExpectEmptyResult()); } TYPED_TEST(TestPlanner, MatchRemove) { @@ -282,7 +285,8 @@ TYPED_TEST(TestPlanner, MatchRemove) { auto label = "label"; auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), REMOVE(PROPERTY_LOOKUP("n", prop)), REMOVE("n", {label}))); - CheckPlan(query, storage, ExpectScanAll(), ExpectRemoveProperty(), ExpectRemoveLabels()); + CheckPlan(query, storage, ExpectScanAll(), ExpectRemoveProperty(), ExpectRemoveLabels(), + ExpectEmptyResult()); } TYPED_TEST(TestPlanner, MatchMultiPattern) { @@ -387,7 +391,8 @@ TYPED_TEST(TestPlanner, CreateMultiExpand) { AstStorage storage; auto *query = QUERY(SINGLE_QUERY(CREATE(PATTERN(NODE("n"), EDGE("r", Direction::OUT, {r}), NODE("m")), PATTERN(NODE("n"), EDGE("p", Direction::OUT, {p}), NODE("l"))))); - CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectCreateExpand()); + CheckPlan(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectCreateExpand(), + ExpectEmptyResult()); } TYPED_TEST(TestPlanner, MatchWithSumWhereReturn) { @@ -491,7 +496,7 @@ TYPED_TEST(TestPlanner, MatchWithCreate) { AstStorage storage; auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), WITH("n", AS("a")), CREATE(PATTERN(NODE("a"), EDGE("r", Direction::OUT, {r_type}), NODE("b"))))); - CheckPlan(query, storage, ExpectScanAll(), ExpectProduce(), ExpectCreateExpand()); + CheckPlan(query, storage, ExpectScanAll(), ExpectProduce(), ExpectCreateExpand(), ExpectEmptyResult()); } TYPED_TEST(TestPlanner, MatchReturnSkipLimit) { @@ -597,7 +602,7 @@ TYPED_TEST(TestPlanner, CreateWithOrderByWhere) { }); auto planner = MakePlanner(&dba, storage, symbol_table, query); CheckPlan(planner.plan(), symbol_table, ExpectCreateNode(), ExpectCreateExpand(), acc, ExpectProduce(), - ExpectOrderBy(), ExpectFilter()); + ExpectOrderBy(), ExpectFilter(), ExpectEmptyResult()); } TYPED_TEST(TestPlanner, ReturnAddSumCountOrderBy) { @@ -854,7 +859,7 @@ TYPED_TEST(TestPlanner, UnwindMergeNodeProperty) { auto *query = QUERY(SINGLE_QUERY(UNWIND(LIST(LITERAL(1)), AS("i")), MERGE(PATTERN(node_n)))); std::list on_match{new ExpectScanAll(), new ExpectFilter()}; std::list on_create{new ExpectCreateNode()}; - CheckPlan(query, storage, ExpectUnwind(), ExpectMerge(on_match, on_create)); + CheckPlan(query, storage, ExpectUnwind(), ExpectMerge(on_match, on_create), ExpectEmptyResult()); DeleteListContent(&on_match); DeleteListContent(&on_create); } @@ -874,7 +879,7 @@ TYPED_TEST(TestPlanner, UnwindMergeNodePropertyWithIndex) { std::list on_create{new ExpectCreateNode()}; auto symbol_table = memgraph::query::MakeSymbolTable(query); auto planner = MakePlanner(&dba, storage, symbol_table, query); - CheckPlan(planner.plan(), symbol_table, ExpectUnwind(), ExpectMerge(on_match, on_create)); + CheckPlan(planner.plan(), symbol_table, ExpectUnwind(), ExpectMerge(on_match, on_create), ExpectEmptyResult()); DeleteListContent(&on_match); DeleteListContent(&on_create); } @@ -1637,7 +1642,7 @@ TYPED_TEST(TestPlanner, Foreach) { auto create = ExpectCreateNode(); std::list updates{&create}; std::list input; - CheckPlan(query, storage, ExpectForeach(input, updates)); + CheckPlan(query, storage, ExpectForeach(input, updates), ExpectEmptyResult()); } { auto *i = NEXPR("i", IDENT("i")); @@ -1645,7 +1650,7 @@ TYPED_TEST(TestPlanner, Foreach) { auto del = ExpectDelete(); std::list updates{&del}; std::list input; - CheckPlan(query, storage, ExpectForeach({input}, updates)); + CheckPlan(query, storage, ExpectForeach({input}, updates), ExpectEmptyResult()); } { auto prop = dba.Property("prop"); @@ -1654,7 +1659,7 @@ TYPED_TEST(TestPlanner, Foreach) { auto set_prop = ExpectSetProperty(); std::list updates{&set_prop}; std::list input; - CheckPlan(query, storage, ExpectForeach({input}, updates)); + CheckPlan(query, storage, ExpectForeach({input}, updates), ExpectEmptyResult()); } { auto *i = NEXPR("i", IDENT("i")); @@ -1666,7 +1671,7 @@ TYPED_TEST(TestPlanner, Foreach) { std::list nested_updates{{&create, &del}}; auto nested_foreach = ExpectForeach(input, nested_updates); std::list updates{&nested_foreach}; - CheckPlan(query, storage, ExpectForeach(input, updates)); + CheckPlan(query, storage, ExpectForeach(input, updates), ExpectEmptyResult()); } { auto *i = NEXPR("i", IDENT("i")); @@ -1678,7 +1683,7 @@ TYPED_TEST(TestPlanner, Foreach) { std::list input{&input_op}; auto *query = QUERY(SINGLE_QUERY(FOREACH(i, {CREATE(PATTERN(NODE("n")))}), FOREACH(j, {CREATE(PATTERN(NODE("n")))}))); - CheckPlan(query, storage, ExpectForeach(input, updates)); + CheckPlan(query, storage, ExpectForeach(input, updates), ExpectEmptyResult()); } } } // namespace diff --git a/tests/unit/query_plan_checker.hpp b/tests/unit/query_plan_checker.hpp index 1577187fc..1a6e2fcb4 100644 --- a/tests/unit/query_plan_checker.hpp +++ b/tests/unit/query_plan_checker.hpp @@ -66,6 +66,7 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor { PRE_VISIT(ExpandVariable); PRE_VISIT(Filter); PRE_VISIT(ConstructNamedPath); + PRE_VISIT(EmptyResult); PRE_VISIT(Produce); PRE_VISIT(SetProperty); PRE_VISIT(SetProperties); @@ -143,6 +144,7 @@ using ExpectExpand = OpChecker; using ExpectFilter = OpChecker; using ExpectConstructNamedPath = OpChecker; using ExpectProduce = OpChecker; +using ExpectEmptyResult = OpChecker; using ExpectSetProperty = OpChecker; using ExpectSetProperties = OpChecker; using ExpectSetLabels = OpChecker;