From fa55c130bace1c6ddb205d55509e47df65af29b5 Mon Sep 17 00:00:00 2001 From: Teon Banek Date: Wed, 7 Feb 2018 10:57:38 +0100 Subject: [PATCH] Add REDUCE function to openCypher Reviewers: florijan, msantl Reviewed By: florijan Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D1175 --- CHANGELOG.md | 1 + docs/user_technical/open-cypher.md | 1 + src/query/frontend/ast/ast.cpp | 1 + src/query/frontend/ast/ast.hpp | 78 +++++++++++++++++++ src/query/frontend/ast/ast_visitor.hpp | 13 ++-- .../frontend/ast/cypher_main_visitor.cpp | 15 ++++ .../frontend/opencypher/grammar/Cypher.g4 | 6 ++ .../frontend/semantic/symbol_generator.cpp | 9 +++ .../frontend/semantic/symbol_generator.hpp | 1 + .../frontend/stripped_lexer_constants.hpp | 3 +- src/query/interpret/eval.hpp | 21 +++++ src/query/plan/preprocess.hpp | 9 +++ src/query/plan/rule_based_planner.cpp | 17 ++++ .../memgraph_V1/features/functions.feature | 24 ++++++ tests/unit/cypher_main_visitor.cpp | 21 +++++ tests/unit/query_common.hpp | 4 + tests/unit/query_expression_evaluator.cpp | 18 +++++ tests/unit/query_semantic.cpp | 30 +++++++ 18 files changed, 265 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ef5e0ae..287ae54b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Next Release * Snapshot format changed (not backward compatible). +* `reduce` function added. ## v0.9.0 diff --git a/docs/user_technical/open-cypher.md b/docs/user_technical/open-cypher.md index 7f7e54859..5d8c87c7a 100644 --- a/docs/user_technical/open-cypher.md +++ b/docs/user_technical/open-cypher.md @@ -550,6 +550,7 @@ functions. `endsWith` | Check if the first argument ends with the second. `contains` | Check if the first argument has an element which is equal to the second argument. `all` | Check if all elements of a list satisfy a predicate.
The syntax is: `all(variable IN list WHERE predicate)`.
NOTE: Whenever possible, use Memgraph's lambda functions when [matching](#filtering-variable-length-paths) instead. + `reduce` | Accumulate list elements into a single result by applying an expression. The syntax is:
`reduce(accumulator = initial_value, variable IN list | expression)`. `assert` | Raises an exception reported to the client if the given argument is not `true`. `counter` | Generates integers that are guaranteed to be unique on the database level, for the given counter name. `counterSet` | Sets the counter with the given name to the given value. diff --git a/src/query/frontend/ast/ast.cpp b/src/query/frontend/ast/ast.cpp index eb2d2ecc6..144c7aabf 100644 --- a/src/query/frontend/ast/ast.cpp +++ b/src/query/frontend/ast/ast.cpp @@ -76,6 +76,7 @@ BOOST_CLASS_EXPORT_IMPLEMENT(query::PropertyLookup); BOOST_CLASS_EXPORT_IMPLEMENT(query::LabelsTest); BOOST_CLASS_EXPORT_IMPLEMENT(query::Aggregation); BOOST_CLASS_EXPORT_IMPLEMENT(query::Function); +BOOST_CLASS_EXPORT_IMPLEMENT(query::Reduce); BOOST_CLASS_EXPORT_IMPLEMENT(query::All); BOOST_CLASS_EXPORT_IMPLEMENT(query::ParameterLookup); BOOST_CLASS_EXPORT_IMPLEMENT(query::Create); diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp index b95b24c8d..dc748f576 100644 --- a/src/query/frontend/ast/ast.hpp +++ b/src/query/frontend/ast/ast.hpp @@ -1391,6 +1391,81 @@ class Aggregation : public BinaryOperator { const unsigned int); }; +class Reduce : public Expression { + friend class AstTreeStorage; + + public: + DEFVISITABLE(TreeVisitor); + bool Accept(HierarchicalTreeVisitor &visitor) override { + if (visitor.PreVisit(*this)) { + accumulator_->Accept(visitor) && initializer_->Accept(visitor) && + identifier_->Accept(visitor) && list_->Accept(visitor) && + expression_->Accept(visitor); + } + return visitor.PostVisit(*this); + } + + Reduce *Clone(AstTreeStorage &storage) const override { + return storage.Create( + accumulator_->Clone(storage), initializer_->Clone(storage), + identifier_->Clone(storage), list_->Clone(storage), + expression_->Clone(storage)); + } + // None of these should be nullptr after construction. + + /// Identifier for the accumulating variable + Identifier *accumulator_ = nullptr; + /// Expression which produces the initial accumulator value. + Expression *initializer_ = nullptr; + /// Identifier for the list element. + Identifier *identifier_ = nullptr; + /// Expression which produces a list which will be reduced. + Expression *list_ = nullptr; + /// Expression which does the reduction, i.e. produces the new accumulator + /// value. + Expression *expression_ = nullptr; + + protected: + Reduce(int uid, Identifier *accumulator, Expression *initializer, + Identifier *identifier, Expression *list, Expression *expression) + : Expression(uid), + accumulator_(accumulator), + initializer_(initializer), + identifier_(identifier), + list_(list), + expression_(expression) {} + + private: + friend class boost::serialization::access; + + BOOST_SERIALIZATION_SPLIT_MEMBER(); + + template + void save(TArchive &ar, const unsigned int) const { + ar << boost::serialization::base_object(*this); + SavePointer(ar, accumulator_); + SavePointer(ar, initializer_); + SavePointer(ar, identifier_); + SavePointer(ar, list_); + SavePointer(ar, expression_); + } + + template + void load(TArchive &ar, const unsigned int) { + ar >> boost::serialization::base_object(*this); + LoadPointer(ar, accumulator_); + LoadPointer(ar, initializer_); + LoadPointer(ar, identifier_); + LoadPointer(ar, list_); + LoadPointer(ar, expression_); + } + + template + friend void boost::serialization::load_construct_data(TArchive &, Reduce *, + const unsigned int); +}; + +// TODO: Think about representing All and Any as Reduce. class All : public Expression { friend class AstTreeStorage; @@ -2847,6 +2922,8 @@ LOAD_AND_CONSTRUCT(query::LabelsTest, 0, nullptr, LOAD_AND_CONSTRUCT(query::Function, 0); LOAD_AND_CONSTRUCT(query::Aggregation, 0, nullptr, nullptr, query::Aggregation::Op::COUNT); +LOAD_AND_CONSTRUCT(query::Reduce, 0, nullptr, nullptr, nullptr, nullptr, + nullptr); LOAD_AND_CONSTRUCT(query::All, 0, nullptr, nullptr, nullptr); LOAD_AND_CONSTRUCT(query::ParameterLookup, 0); LOAD_AND_CONSTRUCT(query::NamedExpression, 0); @@ -2906,6 +2983,7 @@ BOOST_CLASS_EXPORT_KEY(query::PropertyLookup); BOOST_CLASS_EXPORT_KEY(query::LabelsTest); BOOST_CLASS_EXPORT_KEY(query::Aggregation); BOOST_CLASS_EXPORT_KEY(query::Function); +BOOST_CLASS_EXPORT_KEY(query::Reduce); BOOST_CLASS_EXPORT_KEY(query::All); BOOST_CLASS_EXPORT_KEY(query::ParameterLookup); BOOST_CLASS_EXPORT_KEY(query::Create); diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp index b03c3a043..cd9b9657a 100644 --- a/src/query/frontend/ast/ast_visitor.hpp +++ b/src/query/frontend/ast/ast_visitor.hpp @@ -14,6 +14,7 @@ class PropertyLookup; class LabelsTest; class Aggregation; class Function; +class Reduce; class All; class ParameterLookup; class Create; @@ -67,8 +68,8 @@ using TreeCompositeVisitor = ::utils::CompositeVisitor< GreaterEqualOperator, InListOperator, ListMapIndexingOperator, ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral, MapLiteral, PropertyLookup, LabelsTest, - Aggregation, Function, All, Create, Match, Return, With, Pattern, NodeAtom, - EdgeAtom, Delete, Where, SetProperty, SetProperties, SetLabels, + Aggregation, Function, Reduce, All, Create, Match, Return, With, Pattern, + NodeAtom, EdgeAtom, Delete, Where, SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels, Merge, Unwind>; using TreeLeafVisitor = ::utils::LeafVisitor; + LabelsTest, Aggregation, Function, Reduce, All, ParameterLookup, Create, + Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete, Where, + SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels, Merge, + Unwind, Identifier, PrimitiveLiteral, CreateIndex>; } // namespace query diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index 3d0bc163b..a5bb5286c 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -865,6 +865,21 @@ antlrcpp::Any CypherMainVisitor::visitAtom(CypherParser::AtomContext *ctx) { Where *where = ctx->filterExpression()->where()->accept(this); return static_cast( storage_.Create(ident, list_expr, where)); + } else if (ctx->REDUCE()) { + auto *accumulator = storage_.Create( + ctx->reduceExpression()->accumulator->accept(this).as()); + Expression *initializer = ctx->reduceExpression()->initial->accept(this); + auto *ident = storage_.Create(ctx->reduceExpression() + ->idInColl() + ->variable() + ->accept(this) + .as()); + Expression *list = + ctx->reduceExpression()->idInColl()->expression()->accept(this); + Expression *expr = + ctx->reduceExpression()->expression().back()->accept(this); + return static_cast( + storage_.Create(accumulator, initializer, ident, list, expr)); } else if (ctx->caseExpression()) { return static_cast(ctx->caseExpression()->accept(this)); } diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4 index 41deb0f7d..63b5cb3df 100644 --- a/src/query/frontend/opencypher/grammar/Cypher.g4 +++ b/src/query/frontend/opencypher/grammar/Cypher.g4 @@ -187,6 +187,7 @@ atom : literal | patternComprehension | ( FILTER SP? '(' SP? filterExpression SP? ')' ) | ( EXTRACT SP? '(' SP? filterExpression SP? ( SP? '|' expression )? ')' ) + | ( REDUCE SP? '(' SP? reduceExpression SP? ')' ) | ( ALL SP? '(' SP? filterExpression SP? ')' ) | ( ANY SP? '(' SP? filterExpression SP? ')' ) | ( NONE SP? '(' SP? filterExpression SP? ')' ) @@ -226,6 +227,8 @@ relationshipsPattern : nodePattern ( SP? patternElementChain )+ ; filterExpression : idInColl ( SP? where )? ; +reduceExpression : accumulator=variable SP? '=' SP? initial=expression SP? ',' SP? idInColl SP? '|' SP? expression ; + idInColl : variable SP IN SP expression ; functionInvocation : functionName SP? '(' SP? ( DISTINCT SP? )? ( expression SP? ( ',' SP? expression SP? )* )? ')' ; @@ -327,6 +330,7 @@ symbolicName : UnescapedSymbolicName | EscapedSymbolicName | UNION | ALL + | REDUCE | OPTIONAL | MATCH | UNWIND @@ -380,6 +384,8 @@ UNION : ( 'U' | 'u' ) ( 'N' | 'n' ) ( 'I' | 'i' ) ( 'O' | 'o' ) ( 'N' | 'n' ) ; ALL : ( 'A' | 'a' ) ( 'L' | 'l' ) ( 'L' | 'l' ) ; +REDUCE : ( 'R' | 'r' ) ( 'E' | 'e' ) ( 'D' | 'd' ) ( 'U' | 'u' ) ( 'C' | 'c' ) ( 'E' | 'e' ) ; + OPTIONAL : ( 'O' | 'o' ) ( 'P' | 'p' ) ( 'T' | 't' ) ( 'I' | 'i' ) ( 'O' | 'o' ) ( 'N' | 'n' ) ( 'A' | 'a' ) ( 'L' | 'l' ) ; MATCH : ( 'M' | 'm' ) ( 'A' | 'a' ) ( 'T' | 't' ) ( 'C' | 'c' ) ( 'H' | 'h' ) ; diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp index ed2743dfd..3da15d266 100644 --- a/src/query/frontend/semantic/symbol_generator.cpp +++ b/src/query/frontend/semantic/symbol_generator.cpp @@ -333,6 +333,15 @@ bool SymbolGenerator::PreVisit(All &all) { return false; } +bool SymbolGenerator::PreVisit(Reduce &reduce) { + reduce.initializer_->Accept(*this); + reduce.list_->Accept(*this); + VisitWithIdentifiers(*reduce.expression_, + {reduce.accumulator_, reduce.identifier_}); + return false; +} + + // Pattern and its subparts. bool SymbolGenerator::PreVisit(Pattern &pattern) { diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp index 4e7ee3d8c..0b68415f0 100644 --- a/src/query/frontend/semantic/symbol_generator.hpp +++ b/src/query/frontend/semantic/symbol_generator.hpp @@ -57,6 +57,7 @@ class SymbolGenerator : public HierarchicalTreeVisitor { bool PreVisit(IfOperator &) override; bool PostVisit(IfOperator &) override; bool PreVisit(All &) override; + bool PreVisit(Reduce &) override; // Pattern and its subparts. bool PreVisit(Pattern &) override; diff --git a/src/query/frontend/stripped_lexer_constants.hpp b/src/query/frontend/stripped_lexer_constants.hpp index 6751f7468..5c5e485eb 100644 --- a/src/query/frontend/stripped_lexer_constants.hpp +++ b/src/query/frontend/stripped_lexer_constants.hpp @@ -86,7 +86,8 @@ const trie::Trie kKeywords = { "where", "or", "xor", "and", "not", "in", "starts", "ends", "contains", "is", "null", "case", "when", "then", "else", "end", "count", "filter", - "extract", "any", "none", "single", "true", "false"}; + "extract", "any", "none", "single", "true", "false", + "reduce"}; // Unicode codepoints that are allowed at the start of the unescaped name. const std::bitset kUnescapedNameAllowedStarts(std::string( diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp index 58fb63771..44bb40d4d 100644 --- a/src/query/interpret/eval.hpp +++ b/src/query/interpret/eval.hpp @@ -354,6 +354,27 @@ class ExpressionEvaluator : public TreeVisitor { return function.function()(arguments, db_accessor_); } + TypedValue Visit(Reduce &reduce) override { + auto list_value = reduce.list_->Accept(*this); + if (list_value.IsNull()) { + return TypedValue::Null; + } + if (list_value.type() != TypedValue::Type::List) { + throw QueryRuntimeException("'REDUCE' expected a list, but got {}", + list_value.type()); + } + const auto &list = list_value.Value>(); + const auto &element_symbol = symbol_table_.at(*reduce.identifier_); + const auto &accumulator_symbol = symbol_table_.at(*reduce.accumulator_); + auto accumulator = reduce.initializer_->Accept(*this); + for (const auto &element : list) { + frame_[accumulator_symbol] = accumulator; + frame_[element_symbol] = element; + accumulator = reduce.expression_->Accept(*this); + } + return accumulator; + } + TypedValue Visit(All &all) override { auto list_value = all.list_expression_->Accept(*this); if (list_value.IsNull()) { diff --git a/src/query/plan/preprocess.hpp b/src/query/plan/preprocess.hpp index 774ce207a..3f7e9b24f 100644 --- a/src/query/plan/preprocess.hpp +++ b/src/query/plan/preprocess.hpp @@ -30,6 +30,15 @@ class UsedSymbolsCollector : public HierarchicalTreeVisitor { return true; } + bool PostVisit(Reduce &reduce) override { + // Remove the symbols bound by reduce, because we are only interested + // in free (unbound) symbols. + symbols_.erase(symbol_table_.at(*reduce.accumulator_)); + symbols_.erase(symbol_table_.at(*reduce.identifier_)); + return true; + } + + bool Visit(Identifier &ident) override { symbols_.insert(symbol_table_.at(ident)); return true; diff --git a/src/query/plan/rule_based_planner.cpp b/src/query/plan/rule_based_planner.cpp index 2a87944dc..5888491d5 100644 --- a/src/query/plan/rule_based_planner.cpp +++ b/src/query/plan/rule_based_planner.cpp @@ -210,6 +210,23 @@ class ReturnBodyContext : public HierarchicalTreeVisitor { return true; } + bool PostVisit(Reduce &reduce) override { + // Remove the symbols bound by reduce, because we are only interested + // in free (unbound) symbols. + used_symbols_.erase(symbol_table_.at(*reduce.accumulator_)); + used_symbols_.erase(symbol_table_.at(*reduce.identifier_)); + DCHECK(has_aggregation_.size() >= 5U) + << "Expected 5 has_aggregation_ flags for REDUCE arguments"; + bool has_aggr = false; + for (int i = 0; i < 5; ++i) { + has_aggr = has_aggr || has_aggregation_.back(); + has_aggregation_.pop_back(); + } + has_aggregation_.emplace_back(has_aggr); + return true; + } + + bool Visit(Identifier &ident) override { const auto &symbol = symbol_table_.at(ident); if (!utils::Contains(output_symbols_, symbol)) { diff --git a/tests/qa/tck_engine/tests/memgraph_V1/features/functions.feature b/tests/qa/tck_engine/tests/memgraph_V1/features/functions.feature index 275bb7ea8..c0c7b0631 100644 --- a/tests/qa/tck_engine/tests/memgraph_V1/features/functions.feature +++ b/tests/qa/tck_engine/tests/memgraph_V1/features/functions.feature @@ -678,6 +678,30 @@ Feature: Functions """ Then an error should be raised + Scenario: Reduce test 01: + When executing query: + """ + RETURN reduce(a = true, x IN [1, 2, '3'] | a AND x < 2) AS a + """ + Then the result should be: + | a | + | false | + + Scenario: Reduce test 02: + When executing query: + """ + RETURN reduce(s = 0, x IN [1, 2, 3] | s + x) AS s + """ + Then the result should be: + | s | + | 6 | + + Scenario: Reduce test 03: + When executing query: + """ + RETURN reduce(a = true, x IN [true, true, '3'] | a AND x) AS a + """ + Then an error should be raised Scenario: Assert test fail, no message: Given an empty graph diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index af87bec3c..7cc9aa31c 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -1563,6 +1563,27 @@ TYPED_TEST(CypherMainVisitorTest, ReturnAll) { EXPECT_TRUE(eq); } +TYPED_TEST(CypherMainVisitorTest, ReturnReduce) { + TypeParam ast_generator("RETURN reduce(sum = 0, x IN [1,2,3] | sum + x)"); + auto *query = ast_generator.query_; + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + ASSERT_EQ(single_query->clauses_.size(), 1U); + auto *ret = dynamic_cast(single_query->clauses_[0]); + ASSERT_TRUE(ret); + ASSERT_EQ(ret->body_.named_expressions.size(), 1U); + auto *reduce = + dynamic_cast(ret->body_.named_expressions[0]->expression_); + ASSERT_TRUE(reduce); + EXPECT_EQ(reduce->accumulator_->name_, "sum"); + CheckLiteral(ast_generator.context_, reduce->initializer_, 0); + EXPECT_EQ(reduce->identifier_->name_, "x"); + auto *list_literal = dynamic_cast(reduce->list_); + EXPECT_TRUE(list_literal); + auto *add = dynamic_cast(reduce->expression_); + EXPECT_TRUE(add); +} + TYPED_TEST(CypherMainVisitorTest, MatchBfsReturn) { TypeParam ast_generator( "MATCH (n) -[r:type1|type2 *bfs..10 (e, n|e.prop = 42)]-> (m) RETURN r"); diff --git a/tests/unit/query_common.hpp b/tests/unit/query_common.hpp index fa471fa9c..d4cafade0 100644 --- a/tests/unit/query_common.hpp +++ b/tests/unit/query_common.hpp @@ -597,3 +597,7 @@ auto GetMerge(AstTreeStorage &storage, Pattern *pattern, OnMatch on_match, #define ALL(variable, list, where) \ storage.Create(storage.Create(variable), \ list, where) +#define REDUCE(accumulator, initializer, variable, list, expr) \ + storage.Create( \ + storage.Create(accumulator), initializer, \ + storage.Create(variable), list, expr) diff --git a/tests/unit/query_expression_evaluator.cpp b/tests/unit/query_expression_evaluator.cpp index ecd05d5b8..b88214dab 100644 --- a/tests/unit/query_expression_evaluator.cpp +++ b/tests/unit/query_expression_evaluator.cpp @@ -1221,6 +1221,24 @@ TEST(ExpressionEvaluator, FunctionAllWhereWrongType) { EXPECT_THROW(all->Accept(eval.eval), QueryRuntimeException); } +TEST(ExpressionEvaluator, FunctionReduce) { + AstTreeStorage storage; + auto *ident_sum = IDENT("sum"); + auto *ident_x = IDENT("x"); + auto *reduce = REDUCE("sum", LITERAL(0), "x", LIST(LITERAL(1), LITERAL(2)), + ADD(ident_sum, ident_x)); + NoContextExpressionEvaluator eval; + const auto sum_sym = eval.symbol_table.CreateSymbol("sum", true); + eval.symbol_table[*reduce->accumulator_] = sum_sym; + eval.symbol_table[*ident_sum] = sum_sym; + const auto x_sym = eval.symbol_table.CreateSymbol("x", true); + eval.symbol_table[*reduce->identifier_] = x_sym; + eval.symbol_table[*ident_x] = x_sym; + auto value = reduce->Accept(eval.eval); + ASSERT_EQ(value.type(), TypedValue::Type::Int); + EXPECT_EQ(value.Value(), 3); +} + TEST(ExpressionEvaluator, FunctionAssert) { // Invalid calls. ASSERT_THROW(EvaluateFunction("ASSERT", {}), QueryRuntimeException); diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp index 4d634b1bc..97f5871eb 100644 --- a/tests/unit/query_semantic.cpp +++ b/tests/unit/query_semantic.cpp @@ -795,6 +795,36 @@ TEST_F(TestSymbolGenerator, WithReturnAll) { EXPECT_NE(symbol_table.at(*all->identifier_), symbol_table.at(*ret_as_x)); } +TEST_F(TestSymbolGenerator, WithReturnReduce) { + // Test WITH 42 AS x RETURN reduce(y = 0, x IN [x] y + x) AS x, x AS y + auto *with_as_x = AS("x"); + auto *list_x = IDENT("x"); + auto *expr_x = IDENT("x"); + auto *expr_y = IDENT("y"); + auto *reduce = + REDUCE("y", LITERAL(0), "x", LIST(list_x), ADD(expr_y, expr_x)); + auto *ret_as_x = AS("x"); + auto *ret_x = IDENT("x"); + auto *ret_as_y = AS("y"); + auto query = QUERY(SINGLE_QUERY(WITH(LITERAL(42), with_as_x), + RETURN(reduce, ret_as_x, ret_x, ret_as_y))); + query->Accept(symbol_generator); + // Symbols for `WITH .. AS x`, `REDUCE(y, x ...)`, `REDUCE(...) AS x` and `AS + // y`. + EXPECT_EQ(symbol_table.max_position(), 5); + // Check `WITH .. AS x` is the same as `[x]` and `RETURN ... x AS y` + EXPECT_EQ(symbol_table.at(*with_as_x), symbol_table.at(*list_x)); + EXPECT_EQ(symbol_table.at(*with_as_x), symbol_table.at(*ret_x)); + EXPECT_NE(symbol_table.at(*with_as_x), symbol_table.at(*reduce->identifier_)); + EXPECT_NE(symbol_table.at(*with_as_x), symbol_table.at(*ret_as_x)); + // Check `REDUCE(y, x ...)` is only equal to `y + x` + EXPECT_EQ(symbol_table.at(*reduce->identifier_), symbol_table.at(*expr_x)); + EXPECT_NE(symbol_table.at(*reduce->identifier_), symbol_table.at(*ret_as_x)); + EXPECT_EQ(symbol_table.at(*reduce->accumulator_), symbol_table.at(*expr_y)); + EXPECT_NE(symbol_table.at(*reduce->accumulator_), symbol_table.at(*ret_as_y)); +} + + TEST_F(TestSymbolGenerator, MatchBfsReturn) { // Test MATCH (n) -[r *bfs..n.prop] (r, n | r.prop)]-> (m) RETURN r AS r auto prop = dba.Property("prop");