Fix cursor exhaustion by adding EmptyResult operator (#667)

This commit is contained in:
Andi 2022-12-09 11:44:07 +01:00 committed by GitHub
parent d6d4153fb7
commit 0f77c85824
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 171 additions and 27 deletions

View File

@ -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<LogicalOperator> &input)
: input_(input ? input : std::make_shared<Once>()) {}
ACCEPT_WITH_INPUT(EmptyResult)
std::vector<Symbol> EmptyResult::OutputSymbols(const SymbolTable &) const { // NOLINT(hicpp-named-parameter)
return {};
}
std::vector<Symbol> 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<EmptyResultCursor>(mem, *this, mem);
}
Accumulate::Accumulate(const std::shared_ptr<LogicalOperator> &input, const std::vector<Symbol> &symbols,
bool advance_command)
: input_(input), symbols_(symbols), advance_command_(advance_command) {}

View File

@ -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<Once>;
@ -1554,6 +1555,41 @@ edge lists).")
(:serialize (:slk))
(:clone))
(lcp:define-class empty-result (logical-operator)
((input "std::shared_ptr<LogicalOperator>" :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<LogicalOperator> &input);
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
UniqueCursorPtr MakeCursor(utils::MemoryResource *) 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<#)
(:serialize (:slk))
(:clone))
(lcp:define-class accumulate (logical-operator)
((input "std::shared_ptr<LogicalOperator>" :scope :public
:slk-save #'slk-save-operator-pointer

View File

@ -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";

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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<Return>(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<plan::Unwind>(std::move(input_op), unwind->named_expression_->expression_, symbol);
} else if (auto *call_proc = utils::Downcast<query::CallProcedure>(clause)) {
std::vector<Symbol> result_symbols;
result_symbols.reserve(call_proc->result_identifiers_.size());
@ -224,6 +225,7 @@ class RuleBasedPlanner {
input_op =
std::make_unique<plan::LoadCsv>(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<query::Foreach>(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<EmptyResult>(std::move(input_op));
}
return input_op;
}
@ -418,7 +424,8 @@ class RuleBasedPlanner {
std::optional<ExpansionLambda> weight_lambda;
std::optional<Symbol> 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});

View File

@ -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.") \

View File

@ -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:

View File

@ -853,7 +853,7 @@ TEST_F(InterpreterTest, ProfileQueryWithLiterals) {
auto stream = Interpret("PROFILE UNWIND range(1, 1000) AS x CREATE (:Node {id: x});", {});
std::vector<std::string> expected_header{"OPERATOR", "ACTUAL HITS", "RELATIVE TIME", "ABSOLUTE TIME"};
EXPECT_EQ(stream.GetHeader(), expected_header);
std::vector<std::string> expected_rows{"* CreateNode", "* Unwind", "* Once"};
std::vector<std::string> 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()) {

View File

@ -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<TypeParam>(query, storage, ExpectCreateNode(), ExpectCreateExpand());
CheckPlan<TypeParam>(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<TypeParam>(query, storage, ExpectCreateNode(), ExpectCreateNode());
CheckPlan<TypeParam>(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<TypeParam>(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectCreateNode());
CheckPlan<TypeParam>(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<TypeParam>(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectConstructNamedPath());
CheckPlan<TypeParam>(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<TypeParam>(query, storage, ExpectScanAll(), ExpectCreateExpand());
CheckPlan<TypeParam>(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<TypeParam>(query, storage, ExpectScanAll(), ExpectDelete());
CheckPlan<TypeParam>(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<TypeParam>(query, storage, ExpectScanAll(), ExpectSetProperty(), ExpectSetProperties(), ExpectSetLabels());
CheckPlan<TypeParam>(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<TypeParam>(query, storage, ExpectScanAll(), ExpectRemoveProperty(), ExpectRemoveLabels());
CheckPlan<TypeParam>(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<TypeParam>(query, storage, ExpectCreateNode(), ExpectCreateExpand(), ExpectCreateExpand());
CheckPlan<TypeParam>(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<TypeParam>(query, storage, ExpectScanAll(), ExpectProduce(), ExpectCreateExpand());
CheckPlan<TypeParam>(query, storage, ExpectScanAll(), ExpectProduce(), ExpectCreateExpand(), ExpectEmptyResult());
}
TYPED_TEST(TestPlanner, MatchReturnSkipLimit) {
@ -597,7 +602,7 @@ TYPED_TEST(TestPlanner, CreateWithOrderByWhere) {
});
auto planner = MakePlanner<TypeParam>(&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<BaseOpChecker *> on_match{new ExpectScanAll(), new ExpectFilter()};
std::list<BaseOpChecker *> on_create{new ExpectCreateNode()};
CheckPlan<TypeParam>(query, storage, ExpectUnwind(), ExpectMerge(on_match, on_create));
CheckPlan<TypeParam>(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<BaseOpChecker *> on_create{new ExpectCreateNode()};
auto symbol_table = memgraph::query::MakeSymbolTable(query);
auto planner = MakePlanner<TypeParam>(&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<BaseOpChecker *> updates{&create};
std::list<BaseOpChecker *> input;
CheckPlan<TypeParam>(query, storage, ExpectForeach(input, updates));
CheckPlan<TypeParam>(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<BaseOpChecker *> updates{&del};
std::list<BaseOpChecker *> input;
CheckPlan<TypeParam>(query, storage, ExpectForeach({input}, updates));
CheckPlan<TypeParam>(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<BaseOpChecker *> updates{&set_prop};
std::list<BaseOpChecker *> input;
CheckPlan<TypeParam>(query, storage, ExpectForeach({input}, updates));
CheckPlan<TypeParam>(query, storage, ExpectForeach({input}, updates), ExpectEmptyResult());
}
{
auto *i = NEXPR("i", IDENT("i"));
@ -1666,7 +1671,7 @@ TYPED_TEST(TestPlanner, Foreach) {
std::list<BaseOpChecker *> nested_updates{{&create, &del}};
auto nested_foreach = ExpectForeach(input, nested_updates);
std::list<BaseOpChecker *> updates{&nested_foreach};
CheckPlan<TypeParam>(query, storage, ExpectForeach(input, updates));
CheckPlan<TypeParam>(query, storage, ExpectForeach(input, updates), ExpectEmptyResult());
}
{
auto *i = NEXPR("i", IDENT("i"));
@ -1678,7 +1683,7 @@ TYPED_TEST(TestPlanner, Foreach) {
std::list<BaseOpChecker *> input{&input_op};
auto *query =
QUERY(SINGLE_QUERY(FOREACH(i, {CREATE(PATTERN(NODE("n")))}), FOREACH(j, {CREATE(PATTERN(NODE("n")))})));
CheckPlan<TypeParam>(query, storage, ExpectForeach(input, updates));
CheckPlan<TypeParam>(query, storage, ExpectForeach(input, updates), ExpectEmptyResult());
}
}
} // namespace

View File

@ -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<Expand>;
using ExpectFilter = OpChecker<Filter>;
using ExpectConstructNamedPath = OpChecker<ConstructNamedPath>;
using ExpectProduce = OpChecker<Produce>;
using ExpectEmptyResult = OpChecker<EmptyResult>;
using ExpectSetProperty = OpChecker<SetProperty>;
using ExpectSetProperties = OpChecker<SetProperties>;
using ExpectSetLabels = OpChecker<SetLabels>;