From 8fa574026e2cf2c41413eb88dbe6f56137d37844 Mon Sep 17 00:00:00 2001 From: Teon Banek Date: Wed, 26 Apr 2017 13:49:41 +0200 Subject: [PATCH] Plan Merge operator Summary: Check symbols in Merge. Support MERGE macro in query tests. Test SymbolGenerator with MERGE. Test planning Merge. Reviewers: florijan, mislav.bradac Reviewed By: florijan Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D317 --- .../frontend/semantic/symbol_generator.cpp | 10 +- .../frontend/semantic/symbol_generator.hpp | 11 +- src/query/interpreter.hpp | 3 +- src/query/plan/operator.cpp | 4 +- src/query/plan/operator.hpp | 8 +- src/query/plan/planner.cpp | 62 ++++++-- tests/unit/query_common.hpp | 39 +++++- tests/unit/query_planner.cpp | 132 +++++++++++++----- tests/unit/query_semantic.cpp | 71 ++++++++++ 9 files changed, 274 insertions(+), 66 deletions(-) diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp index 5be5130dd..79c8b0db0 100644 --- a/src/query/frontend/semantic/symbol_generator.cpp +++ b/src/query/frontend/semantic/symbol_generator.cpp @@ -111,6 +111,9 @@ bool SymbolGenerator::PreVisit(With &with) { void SymbolGenerator::Visit(Where &) { scope_.in_where = true; } void SymbolGenerator::PostVisit(Where &) { scope_.in_where = false; } +void SymbolGenerator::Visit(Merge &) { scope_.in_merge = true; } +void SymbolGenerator::PostVisit(Merge &) { scope_.in_merge = false; } + // Expressions void SymbolGenerator::Visit(Identifier &ident) { @@ -178,7 +181,7 @@ void SymbolGenerator::PostVisit(Aggregation &aggr) { void SymbolGenerator::Visit(Pattern &pattern) { scope_.in_pattern = true; - if (scope_.in_create && pattern.atoms_.size() == 1U) { + if ((scope_.in_create || scope_.in_merge) && pattern.atoms_.size() == 1U) { debug_assert(dynamic_cast(pattern.atoms_[0]), "Expected a single NodeAtom in Pattern"); scope_.in_create_node = true; @@ -213,14 +216,15 @@ void SymbolGenerator::PostVisit(NodeAtom &node_atom) { void SymbolGenerator::Visit(EdgeAtom &edge_atom) { scope_.in_edge_atom = true; - if (scope_.in_create) { + if (scope_.in_create || scope_.in_merge) { scope_.in_create_edge = true; if (edge_atom.edge_types_.size() != 1U) { throw SemanticException( "A single relationship type must be specified " "when creating an edge."); } - if (edge_atom.direction_ == EdgeAtom::Direction::BOTH) { + if (scope_.in_create && // Merge allows bidirectionality + edge_atom.direction_ == EdgeAtom::Direction::BOTH) { throw SemanticException( "Bidirectional relationship are not supported " "when creating an edge"); diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp index b6ba6ec34..977e6c955 100644 --- a/src/query/frontend/semantic/symbol_generator.hpp +++ b/src/query/frontend/semantic/symbol_generator.hpp @@ -31,6 +31,8 @@ class SymbolGenerator : public TreeVisitorBase { bool PreVisit(With &) override; void Visit(Where &) override; void PostVisit(Where &) override; + void Visit(Merge &) override; + void PostVisit(Merge &) override; // Expressions void Visit(Identifier &) override; @@ -50,11 +52,14 @@ class SymbolGenerator : public TreeVisitorBase { // names to symbols. struct Scope { bool in_pattern{false}; + bool in_merge{false}; bool in_create{false}; - // in_create_node is true if we are creating *only* a node. Therefore, it - // is *not* equivalent to in_create && in_node_atom. + // in_create_node is true if we are creating or merging *only* a node. + // Therefore, it is *not* equivalent to (in_create || in_merge) && + // in_node_atom. bool in_create_node{false}; - // True if creating an edge; shortcut for in_create && in_edge_atom. + // True if creating an edge; + // shortcut for (in_create || in_merge) && in_edge_atom. bool in_create_edge{false}; bool in_node_atom{false}; bool in_edge_atom{false}; diff --git a/src/query/interpreter.hpp b/src/query/interpreter.hpp index a4f5eeebe..a40cc592b 100644 --- a/src/query/interpreter.hpp +++ b/src/query/interpreter.hpp @@ -77,7 +77,8 @@ void Interpret(const std::string &query, GraphDbAccessor &db_accessor, dynamic_cast(logical_plan.get()) || dynamic_cast(logical_plan.get()) || dynamic_cast(logical_plan.get()) || - dynamic_cast(logical_plan.get())) { + dynamic_cast(logical_plan.get()) || + dynamic_cast(logical_plan.get())) { stream.Header(header); auto cursor = logical_plan->MakeCursor(db_accessor); while (cursor->Pull(frame, symbol_table)) continue; diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index eff2d687c..4d75a1390 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -85,7 +85,7 @@ CreateExpand::CreateExpand(const NodeAtom *node_atom, const EdgeAtom *edge_atom, Symbol input_symbol, bool existing_node) : node_atom_(node_atom), edge_atom_(edge_atom), - input_(input), + input_(input ? input : std::make_shared()), input_symbol_(input_symbol), existing_node_(existing_node) {} @@ -223,7 +223,7 @@ Expand::Expand(const NodeAtom *node_atom, const EdgeAtom *edge_atom, GraphView graph_view) : node_atom_(node_atom), edge_atom_(edge_atom), - input_(input), + input_(input ? input : std::make_shared()), input_symbol_(input_symbol), existing_node_(existing_node), existing_edge_(existing_edge), diff --git a/src/query/plan/operator.hpp b/src/query/plan/operator.hpp index 247782c50..f33b9af42 100644 --- a/src/query/plan/operator.hpp +++ b/src/query/plan/operator.hpp @@ -185,7 +185,7 @@ class CreateExpand : public LogicalOperator { * @param node_atom @c NodeAtom at the end of the edge. Used to create a node, * unless it refers to an existing one. * @param edge_atom @c EdgeAtom with information for the edge to be created. - * @param input Required. Previous @c LogicalOperator which will be pulled. + * @param input Optional. Previous @c LogicalOperator which will be pulled. * For each successful @c Cursor::Pull, this operator will create an * expansion. * @param input_symbol @c Symbol for the node at the start of the edge. @@ -321,7 +321,7 @@ class Expand : public LogicalOperator { * identifier is used, labels and properties are ignored. * @param edge_atom Describes the edge to be expanded. Identifier * and direction are used, edge type and properties are ignored. - * @param input LogicalOperation that preceeds this one. + * @param input Optional LogicalOperator that preceeds this one. * @param input_symbol Symbol that points to a VertexAccessor * in the Frame that expansion should emanate from. * @param existing_node If or not the node to be expanded is already @@ -1165,6 +1165,10 @@ class Merge : public LogicalOperator { void Accept(LogicalOperatorVisitor &visitor) override; std::unique_ptr MakeCursor(GraphDbAccessor &db) override; + auto input() const { return input_; } + auto merge_match() const { return merge_match_; } + auto merge_create() const { return merge_create_; } + private: const std::shared_ptr input_; const std::shared_ptr merge_match_; diff --git a/src/query/plan/planner.cpp b/src/query/plan/planner.cpp index 5225bc724..b330b8dce 100644 --- a/src/query/plan/planner.cpp +++ b/src/query/plan/planner.cpp @@ -7,8 +7,7 @@ #include "query/frontend/ast/ast.hpp" #include "utils/exceptions.hpp" -namespace query { -namespace plan { +namespace query::plan { namespace { @@ -105,13 +104,15 @@ auto GenCreate(Create &create, LogicalOperator *input_op, auto GenMatchForPattern(Pattern &pattern, LogicalOperator *input_op, const SymbolTable &symbol_table, std::unordered_set &bound_symbols, - std::vector &edge_symbols) { + std::vector &edge_symbols, + GraphView graph_view = GraphView::OLD) { auto base = [&](NodeAtom *node) { LogicalOperator *last_op = input_op; // If the first atom binds a symbol, we generate a ScanAll which writes it. // Otherwise, someone else generates it (e.g. a previous ScanAll). if (BindSymbol(bound_symbols, symbol_table.at(*node->identifier_))) { - last_op = new ScanAll(node, std::shared_ptr(last_op)); + last_op = new ScanAll(node, std::shared_ptr(last_op), + graph_view); } // Even though we may skip generating ScanAll, we still want to add a filter // in case this atom adds more labels/properties for filtering. @@ -126,20 +127,21 @@ auto GenMatchForPattern(Pattern &pattern, LogicalOperator *input_op, // Store the symbol from the first node as the input to Expand. const auto &input_symbol = symbol_table.at(*prev_node->identifier_); // If the expand symbols were already bound, then we need to indicate - // this as a cycle. The Expand will then check whether the pattern holds + // that they exist. The Expand will then check whether the pattern holds // instead of writing the expansion to symbols. - auto node_cycle = false; - auto edge_cycle = false; + auto existing_node = false; + auto existing_edge = false; if (!BindSymbol(bound_symbols, symbol_table.at(*node->identifier_))) { - node_cycle = true; + existing_node = true; } const auto &edge_symbol = symbol_table.at(*edge->identifier_); if (!BindSymbol(bound_symbols, edge_symbol)) { - edge_cycle = true; + existing_edge = true; } - last_op = new Expand(node, edge, std::shared_ptr(last_op), - input_symbol, node_cycle, edge_cycle); - if (!edge_cycle) { + last_op = + new Expand(node, edge, std::shared_ptr(last_op), + input_symbol, existing_node, existing_edge, graph_view); + if (!existing_edge) { // Ensure Cyphermorphism (different edge symbols always map to different // edges). if (!edge_symbols.empty()) { @@ -455,6 +457,32 @@ LogicalOperator *HandleWriteClause(Clause *clause, LogicalOperator *input_op, return nullptr; } +auto GenMerge(query::Merge &merge, LogicalOperator *input_op, + const SymbolTable &symbol_table, + std::unordered_set &bound_symbols) { + // Copy the bound symbol set, because we don't want to use the updated version + // when generating the create part. + std::unordered_set bound_symbols_copy(bound_symbols); + std::vector edge_symbols; + auto on_match = + GenMatchForPattern(*merge.pattern_, nullptr, symbol_table, + bound_symbols_copy, edge_symbols, GraphView::NEW); + // Use the original bound_symbols, so we fill it with new symbols. + auto on_create = GenCreateForPattern(*merge.pattern_, nullptr, symbol_table, + bound_symbols); + for (auto &set : merge.on_create_) { + on_create = HandleWriteClause(set, on_create, symbol_table, bound_symbols); + debug_assert(on_create, "Expected SET in MERGE ... ON CREATE"); + } + for (auto &set : merge.on_match_) { + on_match = HandleWriteClause(set, on_match, symbol_table, bound_symbols); + debug_assert(on_match, "Expected SET in MERGE ... ON MATCH"); + } + return new plan::Merge(std::shared_ptr(input_op), + std::shared_ptr(on_match), + std::shared_ptr(on_create)); +} + } // namespace std::unique_ptr MakeLogicalPlan( @@ -464,7 +492,7 @@ std::unique_ptr MakeLogicalPlan( // or write it. E.g. `MATCH (n) -[r]- (n)` would bind (and write) the first // `n`, but the latter `n` would only read the already written information. std::unordered_set bound_symbols; - // Set to true if a query command performs a writes to the database. + // Set to true if a query command writes to the database. bool is_write = false; LogicalOperator *input_op = nullptr; for (auto &clause : query.clauses_) { @@ -473,6 +501,11 @@ std::unique_ptr MakeLogicalPlan( input_op = GenMatch(*match, input_op, symbol_table, bound_symbols); } else if (auto *ret = dynamic_cast(clause)) { input_op = GenReturn(*ret, input_op, symbol_table, is_write); + } else if (auto *merge = dynamic_cast(clause)) { + input_op = GenMerge(*merge, input_op, symbol_table, bound_symbols); + // Treat MERGE clause as write, because we do not know if it will create + // anything. + is_write = true; } else if (auto *with = dynamic_cast(clause)) { input_op = GenWith(*with, input_op, symbol_table, is_write, bound_symbols); @@ -489,5 +522,4 @@ std::unique_ptr MakeLogicalPlan( return std::unique_ptr(input_op); } -} // namespace plan -} // namespace query +} // namespace query::plan diff --git a/tests/unit/query_common.hpp b/tests/unit/query_common.hpp index 705471a94..7af3d2463 100644 --- a/tests/unit/query_common.hpp +++ b/tests/unit/query_common.hpp @@ -28,8 +28,8 @@ namespace query { namespace test_common { -// Custom types for ORDER BY, SKIP and LIMIT and expressions, so that they can -// be used to resolve function calls. +// Custom types for ORDER BY, SKIP, LIMIT, ON MATCH and ON CREATE expressions, +// so that they can be used to resolve function calls. struct OrderBy { std::vector> expressions; }; @@ -39,6 +39,12 @@ struct Skip { struct Limit { Expression *expression = nullptr; }; +struct OnMatch { + std::vector set; +}; +struct OnCreate { + std::vector set; +}; // Helper functions for filling the OrderBy with expressions. auto FillOrderBy(OrderBy &order_by, Expression *expression, @@ -301,6 +307,26 @@ auto GetRemove(AstTreeStorage &storage, const std::string &name, return storage.Create(storage.Create(name), labels); } +/// +/// Create a Merge clause for given Pattern with optional OnMatch and OnCreate +/// parts. +/// +auto GetMerge(AstTreeStorage &storage, Pattern *pattern, + OnCreate on_create = OnCreate{}) { + auto *merge = storage.Create(); + merge->pattern_ = pattern; + merge->on_create_ = on_create.set; + return merge; +} +auto GetMerge(AstTreeStorage &storage, Pattern *pattern, OnMatch on_match, + OnCreate on_create = OnCreate{}) { + auto *merge = storage.Create(); + merge->pattern_ = pattern; + merge->on_match_ = on_match.set; + merge->on_create_ = on_create.set; + return merge; +} + } // namespace test_common } // namespace query @@ -346,6 +372,15 @@ auto GetRemove(AstTreeStorage &storage, const std::string &name, query::test_common::GetDelete(storage, {__VA_ARGS__}, true) #define SET(...) query::test_common::GetSet(storage, __VA_ARGS__) #define REMOVE(...) query::test_common::GetRemove(storage, __VA_ARGS__) +#define MERGE(...) query::test_common::GetMerge(storage, __VA_ARGS__) +#define ON_MATCH(...) \ + query::test_common::OnMatch { \ + std::vector { __VA_ARGS__ } \ + } +#define ON_CREATE(...) \ + query::test_common::OnCreate { \ + std::vector { __VA_ARGS__ } \ + } #define QUERY(...) query::test_common::GetQuery(storage, __VA_ARGS__) // Various operators #define ADD(expr1, expr2) \ diff --git a/tests/unit/query_planner.cpp b/tests/unit/query_planner.cpp index 9abb0dc19..032b78112 100644 --- a/tests/unit/query_planner.cpp +++ b/tests/unit/query_planner.cpp @@ -29,6 +29,57 @@ class BaseOpChecker { virtual void CheckOp(LogicalOperator &, const SymbolTable &) = 0; }; +class PlanChecker : public LogicalOperatorVisitor { + public: + using LogicalOperatorVisitor::PreVisit; + using LogicalOperatorVisitor::Visit; + using LogicalOperatorVisitor::PostVisit; + + PlanChecker(const std::list &checkers, + const SymbolTable &symbol_table) + : checkers_(checkers), symbol_table_(symbol_table) {} + + void Visit(CreateNode &op) override { CheckOp(op); } + void Visit(CreateExpand &op) override { CheckOp(op); } + void Visit(Delete &op) override { CheckOp(op); } + void Visit(ScanAll &op) override { CheckOp(op); } + void Visit(Expand &op) override { CheckOp(op); } + void Visit(NodeFilter &op) override { CheckOp(op); } + void Visit(EdgeFilter &op) override { CheckOp(op); } + void Visit(Filter &op) override { CheckOp(op); } + void Visit(Produce &op) override { CheckOp(op); } + void Visit(SetProperty &op) override { CheckOp(op); } + void Visit(SetProperties &op) override { CheckOp(op); } + void Visit(SetLabels &op) override { CheckOp(op); } + void Visit(RemoveProperty &op) override { CheckOp(op); } + void Visit(RemoveLabels &op) override { CheckOp(op); } + void Visit(ExpandUniquenessFilter &op) override { + CheckOp(op); + } + void Visit(ExpandUniquenessFilter &op) override { CheckOp(op); } + void Visit(Accumulate &op) override { CheckOp(op); } + void Visit(Aggregate &op) override { CheckOp(op); } + void Visit(Skip &op) override { CheckOp(op); } + void Visit(Limit &op) override { CheckOp(op); } + void Visit(OrderBy &op) override { CheckOp(op); } + bool PreVisit(Merge &op) override { + CheckOp(op); + op.input()->Accept(*this); + return false; + } + + std::list checkers_; + + private: + void CheckOp(LogicalOperator &op) { + ASSERT_FALSE(checkers_.empty()); + checkers_.back()->CheckOp(op, symbol_table_); + checkers_.pop_back(); + } + + const SymbolTable &symbol_table_; +}; + template class OpChecker : public BaseOpChecker { public: @@ -103,49 +154,22 @@ class ExpectAggregate : public OpChecker { const std::unordered_set group_by_; }; -class PlanChecker : public LogicalOperatorVisitor { +class ExpectMerge : public OpChecker { public: - using LogicalOperatorVisitor::Visit; - using LogicalOperatorVisitor::PostVisit; + ExpectMerge(const std::list &on_match, + const std::list &on_create) + : on_match_(on_match), on_create_(on_create) {} - PlanChecker(const std::list &checkers, - const SymbolTable &symbol_table) - : checkers_(checkers), symbol_table_(symbol_table) {} - - void Visit(CreateNode &op) override { CheckOp(op); } - void Visit(CreateExpand &op) override { CheckOp(op); } - void Visit(Delete &op) override { CheckOp(op); } - void Visit(ScanAll &op) override { CheckOp(op); } - void Visit(Expand &op) override { CheckOp(op); } - void Visit(NodeFilter &op) override { CheckOp(op); } - void Visit(EdgeFilter &op) override { CheckOp(op); } - void Visit(Filter &op) override { CheckOp(op); } - void Visit(Produce &op) override { CheckOp(op); } - void Visit(SetProperty &op) override { CheckOp(op); } - void Visit(SetProperties &op) override { CheckOp(op); } - void Visit(SetLabels &op) override { CheckOp(op); } - void Visit(RemoveProperty &op) override { CheckOp(op); } - void Visit(RemoveLabels &op) override { CheckOp(op); } - void Visit(ExpandUniquenessFilter &op) override { - CheckOp(op); + void ExpectOp(Merge &merge, const SymbolTable &symbol_table) override { + PlanChecker check_match(on_match_, symbol_table); + merge.merge_match()->Accept(check_match); + PlanChecker check_create(on_create_, symbol_table); + merge.merge_create()->Accept(check_create); } - void Visit(ExpandUniquenessFilter &op) override { CheckOp(op); } - void Visit(Accumulate &op) override { CheckOp(op); } - void Visit(Aggregate &op) override { CheckOp(op); } - void Visit(Skip &op) override { CheckOp(op); } - void Visit(Limit &op) override { CheckOp(op); } - void Visit(OrderBy &op) override { CheckOp(op); } - - std::list checkers_; private: - void CheckOp(LogicalOperator &op) { - ASSERT_FALSE(checkers_.empty()); - checkers_.back()->CheckOp(op, symbol_table_); - checkers_.pop_back(); - } - - const SymbolTable &symbol_table_; + const std::list &on_match_; + const std::list &on_create_; }; auto MakeSymbolTable(query::Query &query) { @@ -571,4 +595,36 @@ TEST(TestLogicalPlanner, ReturnAddSumCountOrderBy) { CheckPlan(*query, aggr, ExpectProduce(), ExpectOrderBy()); } +TEST(TestLogicalPlanner, MatchMerge) { + // Test MATCH (n) MERGE (n) -[r :r]- (m) + // ON MATCH SET n.prop = 42 ON CREATE SET m = n + // RETURN n AS n + Dbms dbms; + auto dba = dbms.active(); + auto r_type = dba->edge_type("r"); + auto prop = dba->property("prop"); + AstTreeStorage storage; + auto ident_n = IDENT("n"); + auto query = + QUERY(MATCH(PATTERN(NODE("n"))), + MERGE(PATTERN(NODE("n"), EDGE("r", r_type), NODE("m")), + ON_MATCH(SET(PROPERTY_LOOKUP("n", prop), LITERAL(42))), + ON_CREATE(SET("m", IDENT("n")))), + RETURN(ident_n, AS("n"))); + std::list on_match{ + new ExpectExpand(), new ExpectEdgeFilter(), new ExpectSetProperty()}; + std::list on_create{new ExpectCreateExpand(), + new ExpectSetProperties()}; + auto symbol_table = MakeSymbolTable(*query); + // We expect Accumulate after Merge, because it is considered as a write. + auto acc = ExpectAccumulate({symbol_table.at(*ident_n)}); + auto plan = MakeLogicalPlan(*query, symbol_table); + CheckPlan(*plan, symbol_table, ExpectScanAll(), + ExpectMerge(on_match, on_create), acc, ExpectProduce()); + for (auto &op : on_match) delete op; + on_match.clear(); + for (auto &op : on_create) delete op; + on_create.clear(); +} + } // namespace diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp index 8d01d5f79..e912f7eed 100644 --- a/tests/unit/query_semantic.cpp +++ b/tests/unit/query_semantic.cpp @@ -649,4 +649,75 @@ TEST(TestSymbolGenerator, OrderBy) { } } +TEST(TestSymbolGenerator, Merge) { + // Test MATCH (n) MERGE (n) + { + AstTreeStorage storage; + auto query = QUERY(MATCH(PATTERN(NODE("n"))), MERGE(PATTERN(NODE("n")))); + SymbolTable symbol_table; + SymbolGenerator symbol_generator(symbol_table); + EXPECT_THROW(query->Accept(symbol_generator), RedeclareVariableError); + } + // Test MATCH (n) -[r]- (m) MERGE (a) -[r :rel]- (b) + { + Dbms dbms; + auto dba = dbms.active(); + auto rel = dba->edge_type("rel"); + AstTreeStorage storage; + auto query = QUERY(MATCH(PATTERN(NODE("n"), EDGE("r"), NODE("m"))), + MERGE(PATTERN(NODE("a"), EDGE("r", rel), NODE("b")))); + SymbolTable symbol_table; + SymbolGenerator symbol_generator(symbol_table); + EXPECT_THROW(query->Accept(symbol_generator), RedeclareVariableError); + } + // Test MERGE (a) -[r]- (b) + { + AstTreeStorage storage; + auto query = QUERY(MERGE(PATTERN(NODE("a"), EDGE("r"), NODE("b")))); + SymbolTable symbol_table; + SymbolGenerator symbol_generator(symbol_table); + // Edge must have a type, since it doesn't we raise. + EXPECT_THROW(query->Accept(symbol_generator), SemanticException); + } + // Test MATCH (n) MERGE (n) -[r :rel]- (m) ON MATCH SET n.prop = 42 + // ON CREATE SET m.prop = 42 RETURN r AS r + { + Dbms dbms; + auto dba = dbms.active(); + auto rel = dba->edge_type("rel"); + auto prop = dba->property("prop"); + AstTreeStorage storage; + auto match_n = NODE("n"); + auto merge_n = NODE("n"); + auto edge_r = EDGE("r", rel); + auto node_m = NODE("m"); + auto n_prop = PROPERTY_LOOKUP("n", prop); + auto m_prop = PROPERTY_LOOKUP("m", prop); + auto ident_r = IDENT("r"); + auto as_r = AS("r"); + auto query = QUERY(MATCH(PATTERN(match_n)), + MERGE(PATTERN(merge_n, edge_r, node_m), + ON_MATCH(SET(n_prop, LITERAL(42))), + ON_CREATE(SET(m_prop, LITERAL(42)))), + RETURN(ident_r, as_r)); + SymbolTable symbol_table; + SymbolGenerator symbol_generator(symbol_table); + query->Accept(symbol_generator); + // Symbols for: `n`, `r`, `m` and `AS r`. + EXPECT_EQ(symbol_table.max_position(), 4); + auto n = symbol_table.at(*match_n->identifier_); + EXPECT_EQ(n, symbol_table.at(*merge_n->identifier_)); + EXPECT_EQ(n, symbol_table.at(*n_prop->expression_)); + auto r = symbol_table.at(*edge_r->identifier_); + EXPECT_NE(r, n); + EXPECT_EQ(r, symbol_table.at(*ident_r)); + EXPECT_NE(r, symbol_table.at(*as_r)); + auto m = symbol_table.at(*node_m->identifier_); + EXPECT_NE(m, n); + EXPECT_NE(m, r); + EXPECT_NE(m, symbol_table.at(*as_r)); + EXPECT_EQ(m, symbol_table.at(*m_prop->expression_)); + } +} + }