diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp index ad3cdb7c5..ed354f6ca 100644 --- a/src/query/frontend/ast/ast.hpp +++ b/src/query/frontend/ast/ast.hpp @@ -21,6 +21,7 @@ #include "query/interpret/awesome_memgraph_functions.hpp" #include "query/typed_value.hpp" #include "storage/v2/property_value.hpp" +#include "utils/exceptions.hpp" #include "utils/typeinfo.hpp" namespace memgraph::query { @@ -3586,7 +3587,7 @@ class PatternComprehension : public memgraph::query::Expression { bool Accept(HierarchicalTreeVisitor &visitor) override { if (visitor.PreVisit(*this)) { if (variable_) { - variable_->Accept(visitor); + throw utils::NotYetImplemented("Variable in pattern comprehension."); } pattern_->Accept(visitor); if (filter_) { @@ -3615,7 +3616,8 @@ class PatternComprehension : public memgraph::query::Expression { int32_t symbol_pos_{-1}; PatternComprehension *Clone(AstStorage *storage) const override { - PatternComprehension *object = storage->Create(); + auto *object = storage->Create(); + object->variable_ = variable_ ? variable_->Clone(storage) : nullptr; object->pattern_ = pattern_ ? pattern_->Clone(storage) : nullptr; object->filter_ = filter_ ? filter_->Clone(storage) : nullptr; object->resultExpr_ = resultExpr_ ? resultExpr_->Clone(storage) : nullptr; @@ -3625,7 +3627,8 @@ class PatternComprehension : public memgraph::query::Expression { } protected: - PatternComprehension(Identifier *variable, Pattern *pattern) : variable_(variable), pattern_(pattern) {} + PatternComprehension(Identifier *variable, Pattern *pattern, Where *filter, Expression *resultExpr) + : variable_(variable), pattern_(pattern), filter_(filter), resultExpr_(resultExpr) {} private: friend class AstStorage; diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp index e8ef3cba5..2cfbee584 100644 --- a/src/query/frontend/semantic/symbol_generator.cpp +++ b/src/query/frontend/semantic/symbol_generator.cpp @@ -721,6 +721,32 @@ bool SymbolGenerator::PostVisit(EdgeAtom &) { return true; } +bool SymbolGenerator::PreVisit(PatternComprehension &pc) { + auto &scope = scopes_.back(); + + if (scope.in_set_property) { + throw utils::NotYetImplemented("Pattern Comprehension cannot be used within SET clause.!"); + } + + if (scope.in_with) { + throw utils::NotYetImplemented("Pattern Comprehension cannot be used within WITH!"); + } + + if (scope.in_reduce) { + throw utils::NotYetImplemented("Pattern Comprehension cannot be used within REDUCE!"); + } + + if (scope.num_if_operators) { + throw utils::NotYetImplemented("IF operator cannot be used with Pattern Comprehension!"); + } + + const auto &symbol = CreateAnonymousSymbol(); + pc.MapTo(symbol); + return true; +} + +bool SymbolGenerator::PostVisit(PatternComprehension & /*pc*/) { return true; } + void SymbolGenerator::VisitWithIdentifiers(Expression *expr, const std::vector &identifiers) { auto &scope = scopes_.back(); std::vector, Identifier *>> prev_symbols; diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp index f9e6468f6..e5b46fbfe 100644 --- a/src/query/frontend/semantic/symbol_generator.hpp +++ b/src/query/frontend/semantic/symbol_generator.hpp @@ -1,4 +1,4 @@ -// Copyright 2023 Memgraph Ltd. +// Copyright 2024 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -97,6 +97,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor { bool PostVisit(NodeAtom &) override; bool PreVisit(EdgeAtom &) override; bool PostVisit(EdgeAtom &) override; + bool PreVisit(PatternComprehension &) override; + bool PostVisit(PatternComprehension &) override; private: // Scope stores the state of where we are when visiting the AST and a map of diff --git a/src/query/plan/hint_provider.hpp b/src/query/plan/hint_provider.hpp index 74dde2f46..b70de9aaf 100644 --- a/src/query/plan/hint_provider.hpp +++ b/src/query/plan/hint_provider.hpp @@ -1,4 +1,4 @@ -// Copyright 2023 Memgraph Ltd. +// Copyright 2024 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -206,6 +206,14 @@ class PlanHintsProvider final : public HierarchicalLogicalOperatorVisitor { bool PostVisit(IndexedJoin & /*unused*/) override { return true; } + bool PreVisit(RollUpApply &op) override { + op.input()->Accept(*this); + op.list_collection_branch_->Accept(*this); + return false; + } + + bool PostVisit(RollUpApply & /*unused*/) override { return true; } + private: const SymbolTable &symbol_table_; std::vector hints_; diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 75b531261..ba421b653 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -5624,4 +5624,25 @@ UniqueCursorPtr HashJoin::MakeCursor(utils::MemoryResource *mem) const { return MakeUniqueCursorPtr(mem, *this, mem); } +RollUpApply::RollUpApply(const std::shared_ptr &input, + std::shared_ptr &&second_branch) + : input_(input), list_collection_branch_(second_branch) {} + +std::vector RollUpApply::OutputSymbols(const SymbolTable & /*symbol_table*/) const { + std::vector symbols; + return symbols; +} + +std::vector RollUpApply::ModifiedSymbols(const SymbolTable &table) const { return OutputSymbols(table); } + +bool RollUpApply::Accept(HierarchicalLogicalOperatorVisitor &visitor) { + if (visitor.PreVisit(*this)) { + if (!input_ || !list_collection_branch_) { + throw utils::NotYetImplemented("One of the branches in pattern comprehension is null! Please contact support."); + } + input_->Accept(visitor) && list_collection_branch_->Accept(visitor); + } + return visitor.PostVisit(*this); +} + } // namespace memgraph::query::plan diff --git a/src/query/plan/operator.hpp b/src/query/plan/operator.hpp index 516ef2e38..cdaca2875 100644 --- a/src/query/plan/operator.hpp +++ b/src/query/plan/operator.hpp @@ -1,4 +1,4 @@ -// Copyright 2023 Memgraph Ltd. +// Copyright 2024 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -130,6 +130,7 @@ class EvaluatePatternFilter; class Apply; class IndexedJoin; class HashJoin; +class RollUpApply; using LogicalOperatorCompositeVisitor = utils::CompositeVisitor; + Foreach, EmptyResult, EvaluatePatternFilter, Apply, IndexedJoin, HashJoin, RollUpApply>; using LogicalOperatorLeafVisitor = utils::LeafVisitor; @@ -2634,5 +2635,38 @@ class HashJoin : public memgraph::query::plan::LogicalOperator { } }; +/// RollUpApply operator is used to execute an expression which takes as input a pattern, +/// and returns a list with content from the matched pattern +/// It's used for a pattern expression or pattern comprehension in a query. +class RollUpApply : public memgraph::query::plan::LogicalOperator { + public: + static const utils::TypeInfo kType; + const utils::TypeInfo &GetTypeInfo() const override { return kType; } + + RollUpApply() = default; + RollUpApply(const std::shared_ptr &input, std::shared_ptr &&second_branch); + + bool HasSingleInput() const override { return false; } + std::shared_ptr input() const override { return input_; } + void set_input(std::shared_ptr input) override { input_ = input; } + + bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override; + UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override { + throw utils::NotYetImplemented("Execution of Pattern comprehension is currently unsupported."); + } + std::vector OutputSymbols(const SymbolTable &) const override; + std::vector ModifiedSymbols(const SymbolTable &) const override; + + std::unique_ptr Clone(AstStorage *storage) const override { + auto object = std::make_unique(); + object->input_ = input_ ? input_->Clone(storage) : nullptr; + object->list_collection_branch_ = list_collection_branch_ ? list_collection_branch_->Clone(storage) : nullptr; + return object; + } + + std::shared_ptr input_; + std::shared_ptr list_collection_branch_; +}; + } // namespace plan } // namespace memgraph::query diff --git a/src/query/plan/operator_type_info.cpp b/src/query/plan/operator_type_info.cpp index 3b3ffe14e..168137552 100644 --- a/src/query/plan/operator_type_info.cpp +++ b/src/query/plan/operator_type_info.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Memgraph Ltd. +// Copyright 2024 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -154,4 +154,7 @@ constexpr utils::TypeInfo query::plan::IndexedJoin::kType{utils::TypeId::INDEXED constexpr utils::TypeInfo query::plan::HashJoin::kType{utils::TypeId::HASH_JOIN, "HashJoin", &query::plan::LogicalOperator::kType}; + +constexpr utils::TypeInfo query::plan::RollUpApply::kType{utils::TypeId::ROLLUP_APPLY, "RollUpApply", + &query::plan::LogicalOperator::kType}; } // namespace memgraph diff --git a/src/query/plan/preprocess.cpp b/src/query/plan/preprocess.cpp index c3bfdf462..1554ce8ce 100644 --- a/src/query/plan/preprocess.cpp +++ b/src/query/plan/preprocess.cpp @@ -632,20 +632,20 @@ void AddMatching(const Match &match, SymbolTable &symbol_table, AstStorage &stor // If there are any pattern filters, we add those as well for (auto &filter : matching.filters) { - PatternFilterVisitor visitor(symbol_table, storage); + PatternVisitor visitor(symbol_table, storage); filter.expression->Accept(visitor); - filter.matchings = visitor.getMatchings(); + filter.matchings = visitor.getFilterMatchings(); } } -PatternFilterVisitor::PatternFilterVisitor(SymbolTable &symbol_table, AstStorage &storage) +PatternVisitor::PatternVisitor(SymbolTable &symbol_table, AstStorage &storage) : symbol_table_(symbol_table), storage_(storage) {} -PatternFilterVisitor::PatternFilterVisitor(const PatternFilterVisitor &) = default; -PatternFilterVisitor::PatternFilterVisitor(PatternFilterVisitor &&) noexcept = default; -PatternFilterVisitor::~PatternFilterVisitor() = default; +PatternVisitor::PatternVisitor(const PatternVisitor &) = default; +PatternVisitor::PatternVisitor(PatternVisitor &&) noexcept = default; +PatternVisitor::~PatternVisitor() = default; -void PatternFilterVisitor::Visit(Exists &op) { +void PatternVisitor::Visit(Exists &op) { std::vector patterns; patterns.push_back(op.pattern_); @@ -655,10 +655,10 @@ void PatternFilterVisitor::Visit(Exists &op) { filter_matching.type = PatternFilterType::EXISTS; filter_matching.symbol = std::make_optional(symbol_table_.at(op)); - matchings_.push_back(std::move(filter_matching)); + filter_matchings_.push_back(std::move(filter_matching)); } -std::vector PatternFilterVisitor::getMatchings() { return matchings_; } +std::vector PatternVisitor::getFilterMatchings() { return filter_matchings_; } static void ParseForeach(query::Foreach &foreach, SingleQueryPart &query_part, AstStorage &storage, SymbolTable &symbol_table) { @@ -672,6 +672,30 @@ static void ParseForeach(query::Foreach &foreach, SingleQueryPart &query_part, A } } +static void ParseReturn(query::Return &ret, AstStorage &storage, SymbolTable &symbol_table, + std::unordered_map &matchings) { + PatternVisitor visitor(symbol_table, storage); + + for (auto *expr : ret.body_.named_expressions) { + expr->Accept(visitor); + auto pattern_comprehension_matchings = visitor.getPatternComprehensionMatchings(); + for (auto &matching : pattern_comprehension_matchings) { + matchings.emplace(expr->name_, matching); + } + } +} + +void PatternVisitor::Visit(NamedExpression &op) { op.expression_->Accept(*this); } + +void PatternVisitor::Visit(PatternComprehension &op) { + PatternComprehensionMatching matching; + AddMatching({op.pattern_}, op.filter_, symbol_table_, storage_, matching); + matching.result_expr = storage_.Create(symbol_table_.at(op).name(), op.resultExpr_); + matching.result_expr->MapTo(symbol_table_.at(op)); + + pattern_comprehension_matchings_.push_back(std::move(matching)); +} + // Converts a Query to multiple QueryParts. In the process new Ast nodes may be // created, e.g. filter expressions. std::vector CollectSingleQueryParts(SymbolTable &symbol_table, AstStorage &storage, @@ -703,7 +727,8 @@ std::vector CollectSingleQueryParts(SymbolTable &symbol_table, // This query part is done, continue with a new one. query_parts.emplace_back(SingleQueryPart{}); query_part = &query_parts.back(); - } else if (utils::IsSubtype(*clause, Return::kType)) { + } else if (auto *ret = utils::Downcast(clause)) { + ParseReturn(*ret, storage, symbol_table, query_part->pattern_comprehension_matchings); return query_parts; } } diff --git a/src/query/plan/preprocess.hpp b/src/query/plan/preprocess.hpp index 01b10ebaf..b1ad253c4 100644 --- a/src/query/plan/preprocess.hpp +++ b/src/query/plan/preprocess.hpp @@ -153,19 +153,20 @@ struct Expansion { ExpansionGroupId expansion_group_id = ExpansionGroupId(); }; +struct PatternComprehensionMatching; struct FilterMatching; enum class PatternFilterType { EXISTS }; -/// Collects matchings from filters that include patterns -class PatternFilterVisitor : public ExpressionVisitor { +/// Collects matchings that include patterns +class PatternVisitor : public ExpressionVisitor { public: - explicit PatternFilterVisitor(SymbolTable &symbol_table, AstStorage &storage); - PatternFilterVisitor(const PatternFilterVisitor &); - PatternFilterVisitor &operator=(const PatternFilterVisitor &) = delete; - PatternFilterVisitor(PatternFilterVisitor &&) noexcept; - PatternFilterVisitor &operator=(PatternFilterVisitor &&) noexcept = delete; - ~PatternFilterVisitor() override; + explicit PatternVisitor(SymbolTable &symbol_table, AstStorage &storage); + PatternVisitor(const PatternVisitor &); + PatternVisitor &operator=(const PatternVisitor &) = delete; + PatternVisitor(PatternVisitor &&) noexcept; + PatternVisitor &operator=(PatternVisitor &&) noexcept = delete; + ~PatternVisitor() override; using ExpressionVisitor::Visit; @@ -233,18 +234,24 @@ class PatternFilterVisitor : public ExpressionVisitor { void Visit(PropertyLookup &op) override{}; void Visit(AllPropertiesLookup &op) override{}; void Visit(ParameterLookup &op) override{}; - void Visit(NamedExpression &op) override{}; void Visit(RegexMatch &op) override{}; - void Visit(PatternComprehension &op) override{}; + void Visit(NamedExpression &op) override; + void Visit(PatternComprehension &op) override; - std::vector getMatchings(); + std::vector getFilterMatchings(); + std::vector getPatternComprehensionMatchings() { + return pattern_comprehension_matchings_; + } SymbolTable &symbol_table_; AstStorage &storage_; private: /// Collection of matchings in the filter expression being analyzed. - std::vector matchings_; + std::vector filter_matchings_; + + /// Collection of matchings in the pattern comprehension being analyzed. + std::vector pattern_comprehension_matchings_; }; /// Stores the symbols and expression used to filter a property. @@ -495,6 +502,11 @@ inline auto Filters::IdFilters(const Symbol &symbol) const -> std::vector merge_matching{}; + + /// @brief @c NamedExpression name to @c PatternComprehensionMatching for each pattern comprehension. + /// + /// Storing the normalized pattern of a @c PatternComprehension does not preclude storing the + /// @c PatternComprehension clause itself inside `remaining_clauses`. The reason is that we + /// need to have access to other parts of the clause, such as pattern, filter clauses. + std::unordered_map pattern_comprehension_matchings{}; + /// @brief All the remaining clauses (without @c Match). std::vector remaining_clauses{}; /// The subqueries vector are all the subqueries in this query part ordered in a list by diff --git a/src/query/plan/pretty_print.cpp b/src/query/plan/pretty_print.cpp index a2df9422c..7938f9c73 100644 --- a/src/query/plan/pretty_print.cpp +++ b/src/query/plan/pretty_print.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Memgraph Ltd. +// Copyright 2024 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -143,6 +143,13 @@ bool PlanPrinter::PreVisit(query::plan::Union &op) { return false; } +bool PlanPrinter::PreVisit(query::plan::RollUpApply &op) { + WithPrintLn([&op](auto &out) { out << "* " << op.ToString(); }); + Branch(*op.list_collection_branch_); + op.input_->Accept(*this); + return false; +} + bool PlanPrinter::PreVisit(query::plan::CallProcedure &op) { WithPrintLn([&op](auto &out) { out << "* " << op.ToString(); }); return true; diff --git a/src/query/plan/pretty_print.hpp b/src/query/plan/pretty_print.hpp index 645fe17a5..af8429b85 100644 --- a/src/query/plan/pretty_print.hpp +++ b/src/query/plan/pretty_print.hpp @@ -1,4 +1,4 @@ -// Copyright 2023 Memgraph Ltd. +// Copyright 2024 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -91,6 +91,7 @@ class PlanPrinter : public virtual HierarchicalLogicalOperatorVisitor { bool PreVisit(OrderBy &) override; bool PreVisit(Distinct &) override; bool PreVisit(Union &) override; + bool PreVisit(RollUpApply &) override; bool PreVisit(Unwind &) override; bool PreVisit(CallProcedure &) override; diff --git a/src/query/plan/rewrite/index_lookup.hpp b/src/query/plan/rewrite/index_lookup.hpp index 09c6e2014..90c222b42 100644 --- a/src/query/plan/rewrite/index_lookup.hpp +++ b/src/query/plan/rewrite/index_lookup.hpp @@ -595,6 +595,18 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor { return true; } + bool PreVisit(RollUpApply &op) override { + prev_ops_.push_back(&op); + op.input()->Accept(*this); + RewriteBranch(&op.list_collection_branch_); + return false; + } + + bool PostVisit(RollUpApply &) override { + prev_ops_.pop_back(); + return true; + } + std::shared_ptr new_root_; private: diff --git a/src/query/plan/rewrite/join.hpp b/src/query/plan/rewrite/join.hpp index e346ded45..9ef6c6aec 100644 --- a/src/query/plan/rewrite/join.hpp +++ b/src/query/plan/rewrite/join.hpp @@ -1,4 +1,4 @@ -// Copyright 2023 Memgraph Ltd. +// Copyright 2024 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -455,6 +455,18 @@ class JoinRewriter final : public HierarchicalLogicalOperatorVisitor { return true; } + bool PreVisit(RollUpApply &op) override { + prev_ops_.push_back(&op); + op.input()->Accept(*this); + RewriteBranch(&op.list_collection_branch_); + return false; + } + + bool PostVisit(RollUpApply &) override { + prev_ops_.pop_back(); + return true; + } + std::shared_ptr new_root_; private: diff --git a/src/query/plan/rule_based_planner.cpp b/src/query/plan/rule_based_planner.cpp index bf5e66158..54b5c3834 100644 --- a/src/query/plan/rule_based_planner.cpp +++ b/src/query/plan/rule_based_planner.cpp @@ -14,9 +14,12 @@ #include #include #include +#include #include #include +#include "query/frontend/ast/ast.hpp" +#include "query/plan/operator.hpp" #include "query/plan/preprocess.hpp" #include "utils/algorithm.hpp" #include "utils/exceptions.hpp" @@ -40,7 +43,8 @@ namespace { class ReturnBodyContext : public HierarchicalTreeVisitor { public: ReturnBodyContext(const ReturnBody &body, SymbolTable &symbol_table, const std::unordered_set &bound_symbols, - AstStorage &storage, Where *where = nullptr) + AstStorage &storage, std::unordered_map> pc_ops, + Where *where = nullptr) : body_(body), symbol_table_(symbol_table), bound_symbols_(bound_symbols), storage_(storage), where_(where) { // Collect symbols from named expressions. output_symbols_.reserve(body_.named_expressions.size()); @@ -53,6 +57,14 @@ class ReturnBodyContext : public HierarchicalTreeVisitor { output_symbols_.emplace_back(symbol_table_.at(*named_expr)); named_expr->Accept(*this); named_expressions_.emplace_back(named_expr); + if (pattern_comprehension_) { + if (auto it = pc_ops.find(named_expr->name_); it != pc_ops.end()) { + pattern_comprehension_op_ = std::move(it->second); + pc_ops.erase(it); + } else { + throw utils::NotYetImplemented("Operation on top of pattern comprehension"); + } + } } // Collect symbols used in group by expressions. if (!aggregations_.empty()) { @@ -386,8 +398,20 @@ class ReturnBodyContext : public HierarchicalTreeVisitor { return true; } - bool PostVisit(PatternComprehension & /*unused*/) override { - throw utils::NotYetImplemented("Planner can not handle pattern comprehension."); + bool PreVisit(PatternComprehension & /*unused*/) override { + pattern_compression_aggregations_start_index_ = has_aggregation_.size(); + return true; + } + + bool PostVisit(PatternComprehension &pattern_comprehension) override { + bool has_aggr = false; + for (auto i = has_aggregation_.size(); i > pattern_compression_aggregations_start_index_; --i) { + has_aggr |= has_aggregation_.back(); + has_aggregation_.pop_back(); + } + has_aggregation_.emplace_back(has_aggr); + pattern_comprehension_ = &pattern_comprehension; + return true; } // Creates NamedExpression with an Identifier for each user declared symbol. @@ -444,6 +468,10 @@ class ReturnBodyContext : public HierarchicalTreeVisitor { // named_expressions. const auto &output_symbols() const { return output_symbols_; } + const auto *pattern_comprehension() const { return pattern_comprehension_; } + + std::shared_ptr pattern_comprehension_op() const { return pattern_comprehension_op_; } + private: const ReturnBody &body_; SymbolTable &symbol_table_; @@ -465,10 +493,13 @@ class ReturnBodyContext : public HierarchicalTreeVisitor { // group by it. std::list has_aggregation_; std::vector named_expressions_; + PatternComprehension *pattern_comprehension_ = nullptr; + std::shared_ptr pattern_comprehension_op_; + size_t pattern_compression_aggregations_start_index_ = 0; }; std::unique_ptr GenReturnBody(std::unique_ptr input_op, bool advance_command, - const ReturnBodyContext &body, bool accumulate = false) { + const ReturnBodyContext &body, bool accumulate) { std::vector used_symbols(body.used_symbols().begin(), body.used_symbols().end()); auto last_op = std::move(input_op); if (accumulate) { @@ -482,6 +513,11 @@ std::unique_ptr GenReturnBody(std::unique_ptr std::vector remember(body.group_by_used_symbols().begin(), body.group_by_used_symbols().end()); last_op = std::make_unique(std::move(last_op), body.aggregations(), body.group_by(), remember); } + + if (body.pattern_comprehension()) { + last_op = std::make_unique(std::move(last_op), body.pattern_comprehension_op()); + } + last_op = std::make_unique(std::move(last_op), body.named_expressions()); // Distinct in ReturnBody only makes Produce values unique, so plan after it. if (body.distinct()) { @@ -506,6 +542,7 @@ std::unique_ptr GenReturnBody(std::unique_ptr last_op = std::make_unique(std::move(last_op), std::vector>{}, body.where()->expression_); } + return last_op; } @@ -543,8 +580,9 @@ Expression *ExtractFilters(const std::unordered_set &bound_symbols, Filt return filter_expr; } -std::unordered_set GetSubqueryBoundSymbols(const std::vector &single_query_parts, - SymbolTable &symbol_table, AstStorage &storage) { +std::unordered_set GetSubqueryBoundSymbols( + const std::vector &single_query_parts, SymbolTable &symbol_table, AstStorage &storage, + std::unordered_map> pc_ops) { const auto &query = single_query_parts[0]; if (!query.matching.expansions.empty() || query.remaining_clauses.empty()) { @@ -552,7 +590,7 @@ std::unordered_set GetSubqueryBoundSymbols(const std::vector bound_symbols; auto *with = utils::Downcast(query.remaining_clauses[0])) { - auto input_op = impl::GenWith(*with, nullptr, symbol_table, false, bound_symbols, storage); + auto input_op = impl::GenWith(*with, nullptr, symbol_table, false, bound_symbols, storage, pc_ops); return bound_symbols; } @@ -583,7 +621,8 @@ std::unique_ptr GenNamedPaths(std::unique_ptr std::unique_ptr GenReturn(Return &ret, std::unique_ptr input_op, SymbolTable &symbol_table, bool is_write, - const std::unordered_set &bound_symbols, AstStorage &storage) { + const std::unordered_set &bound_symbols, AstStorage &storage, + std::unordered_map> pc_ops) { // Similar to WITH clause, but we want to accumulate when the query writes to // the database. This way we handle the case when we want to return // expressions with the latest updated results. For example, `MATCH (n) -- () @@ -592,13 +631,14 @@ std::unique_ptr GenReturn(Return &ret, std::unique_ptr GenWith(With &with, std::unique_ptr input_op, SymbolTable &symbol_table, bool is_write, - std::unordered_set &bound_symbols, AstStorage &storage) { + std::unordered_set &bound_symbols, AstStorage &storage, + std::unordered_map> pc_ops) { // WITH clause is Accumulate/Aggregate (advance_command) + Produce and // optional Filter. In case of update and aggregation, we want to accumulate // first, so that when aggregating, we get the latest results. Similar to @@ -606,7 +646,7 @@ std::unique_ptr GenWith(With &with, std::unique_ptr &bound_symbols, cons // Returns the set of symbols for the subquery that are actually referenced from the outer scope and // used in the subquery. -std::unordered_set GetSubqueryBoundSymbols(const std::vector &single_query_parts, - SymbolTable &symbol_table, AstStorage &storage); +std::unordered_set GetSubqueryBoundSymbols( + const std::vector &single_query_parts, SymbolTable &symbol_table, AstStorage &storage, + std::unordered_map> pc_ops); Symbol GetSymbol(NodeAtom *atom, const SymbolTable &symbol_table); Symbol GetSymbol(EdgeAtom *atom, const SymbolTable &symbol_table); @@ -142,11 +144,13 @@ std::unique_ptr GenNamedPaths(std::unique_ptr std::unique_ptr GenReturn(Return &ret, std::unique_ptr input_op, SymbolTable &symbol_table, bool is_write, - const std::unordered_set &bound_symbols, AstStorage &storage); + const std::unordered_set &bound_symbols, AstStorage &storage, + std::unordered_map> pc_ops); std::unique_ptr GenWith(With &with, std::unique_ptr input_op, SymbolTable &symbol_table, bool is_write, - std::unordered_set &bound_symbols, AstStorage &storage); + std::unordered_set &bound_symbols, AstStorage &storage, + std::unordered_map> pc_ops); std::unique_ptr GenUnion(const CypherUnion &cypher_union, std::shared_ptr left_op, std::shared_ptr right_op, SymbolTable &symbol_table); @@ -190,11 +194,24 @@ class RuleBasedPlanner { uint64_t merge_id = 0; uint64_t subquery_id = 0; + std::unordered_map> pattern_comprehension_ops; + + if (single_query_part.pattern_comprehension_matchings.size() > 1) { + throw utils::NotYetImplemented("Multiple pattern comprehensions."); + } + for (const auto &matching : single_query_part.pattern_comprehension_matchings) { + std::unique_ptr new_input; + MatchContext match_ctx{matching.second, *context.symbol_table, context.bound_symbols}; + new_input = PlanMatching(match_ctx, std::move(new_input)); + new_input = std::make_unique(std::move(new_input), std::vector{matching.second.result_expr}); + pattern_comprehension_ops.emplace(matching.first, std::move(new_input)); + } + for (const auto &clause : single_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, context.is_write_query, - context.bound_symbols, *context.ast_storage); + context.bound_symbols, *context.ast_storage, pattern_comprehension_ops); } else if (auto *merge = utils::Downcast(clause)) { input_op = GenMerge(*merge, std::move(input_op), single_query_part.merge_matching[merge_id++]); // Treat MERGE clause as write, because we do not know if it will @@ -202,7 +219,7 @@ class RuleBasedPlanner { context.is_write_query = true; } else if (auto *with = utils::Downcast(clause)) { input_op = impl::GenWith(*with, std::move(input_op), *context.symbol_table, context.is_write_query, - context.bound_symbols, *context.ast_storage); + context.bound_symbols, *context.ast_storage, pattern_comprehension_ops); // WITH clause advances the command, so reset the flag. context.is_write_query = false; } else if (auto op = HandleWriteClause(clause, input_op, *context.symbol_table, context.bound_symbols)) { @@ -241,7 +258,7 @@ class RuleBasedPlanner { single_query_part, merge_id); } else if (auto *call_sub = utils::Downcast(clause)) { input_op = HandleSubquery(std::move(input_op), single_query_part.subqueries[subquery_id++], - *context.symbol_table, *context_->ast_storage); + *context.symbol_table, *context_->ast_storage, pattern_comprehension_ops); } else { throw utils::NotYetImplemented("clause '{}' conversion to operator(s)", clause->GetTypeInfo().name); } @@ -860,15 +877,15 @@ class RuleBasedPlanner { symbol); } - std::unique_ptr HandleSubquery(std::unique_ptr last_op, - std::shared_ptr subquery, SymbolTable &symbol_table, - AstStorage &storage) { + std::unique_ptr HandleSubquery( + std::unique_ptr last_op, std::shared_ptr subquery, SymbolTable &symbol_table, + AstStorage &storage, std::unordered_map> pc_ops) { std::unordered_set outer_scope_bound_symbols; outer_scope_bound_symbols.insert(std::make_move_iterator(context_->bound_symbols.begin()), std::make_move_iterator(context_->bound_symbols.end())); context_->bound_symbols = - impl::GetSubqueryBoundSymbols(subquery->query_parts[0].single_query_parts, symbol_table, storage); + impl::GetSubqueryBoundSymbols(subquery->query_parts[0].single_query_parts, symbol_table, storage, pc_ops); auto subquery_op = Plan(*subquery); diff --git a/src/utils/typeinfo.hpp b/src/utils/typeinfo.hpp index 592473b8b..3ed6128fc 100644 --- a/src/utils/typeinfo.hpp +++ b/src/utils/typeinfo.hpp @@ -68,6 +68,7 @@ enum class TypeId : uint64_t { APPLY, INDEXED_JOIN, HASH_JOIN, + ROLLUP_APPLY, // Replication // NOTE: these NEED to be stable in the 2000+ range (see rpc version) diff --git a/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature b/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature index 8c5538d6b..a6a4b15d2 100644 --- a/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature +++ b/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature @@ -291,3 +291,45 @@ Feature: List operators # Then the result should be: # | years | # | [2021,2003,2003,1999] | + + Scenario: Multiple entries with list pattern comprehension + Given graph "graph_keanu" + When executing query: + """ + MATCH (n:Person) + RETURN n.name, [(n)-->(b:Movie) WHERE b.title CONTAINS 'Matrix' | b.released] AS years + """ + Then an error should be raised + + Scenario: Multiple list pattern comprehensions in Return + Given graph "graph_keanu" + When executing query: + """ + MATCH (n:Person) + RETURN n.name, + [(n)-->(b:Movie) WHERE b.title CONTAINS 'Matrix' | b.released] AS years, + [(n)-->(b:Movie) WHERE b.title CONTAINS 'Matrix' | b.title] AS titles + """ + Then an error should be raised + + Scenario: Function inside pattern comprehension's expression + Given graph "graph_keanu" + When executing query: + """ + MATCH (keanu:Person {name: 'Keanu Reeves'}) + RETURN [p = (keanu)-->(b:Movie) WHERE b.title CONTAINS 'Matrix' | size(nodes(p))] AS nodes + """ + Then an error should be raised + + Scenario: Multiple list pattern comprehensions in With + Given graph "graph_keanu" + When executing query: + """ + MATCH (n) WHERE size(n.name) > 5 + WITH + n AS actor, + [(n)-->(m) WHERE m.released > 2000 | m.title] AS titles, + [(n)-->(m) WHERE m.released > 2000 | m.released] AS years + RETURN actor.name, years, titles; + """ + Then an error should be raised diff --git a/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher b/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher index a7a72aced..98f48c3c1 100644 --- a/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher +++ b/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher @@ -1,5 +1,7 @@ CREATE (keanu:Person {name: 'Keanu Reeves'}), + (trinity:Person {name: 'Carrie-Anne Moss'}), + (morpheus:Person {name: 'Laurence Fishburne'}), (johnnyMnemonic:Movie {title: 'Johnny Mnemonic', released: 1995}), (theMatrixRevolutions:Movie {title: 'The Matrix Revolutions', released: 2003}), (theMatrixReloaded:Movie {title: 'The Matrix Reloaded', released: 2003}), @@ -13,4 +15,7 @@ CREATE (keanu)-[:ACTED_IN]->(theReplacements), (keanu)-[:ACTED_IN]->(theMatrix), (keanu)-[:ACTED_IN]->(theDevilsAdvocate), - (keanu)-[:ACTED_IN]->(theMatrixResurrections); + (keanu)-[:ACTED_IN]->(theMatrixResurrections), + (trinity)-[:ACTED_IN]->(theMatrix), + (trinity)-[:ACTED_IN]->(theMatrixReloaded), + (morpheus)-[:ACTED_IN]->(theMatrix); diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index 63cca3aa4..bcc6767f4 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -4624,3 +4624,101 @@ TEST_P(CypherMainVisitorTest, CallSubquery) { ASSERT_TRUE(nested_match); } } + +TEST_P(CypherMainVisitorTest, PatternComprehension) { + auto &ast_generator = *GetParam(); + { + const auto *query = + dynamic_cast(ast_generator.ParseQuery("MATCH (n) RETURN [(n)-->(b) | b.val] AS res;")); + const auto *ret = dynamic_cast(query->single_query_->clauses_[1]); + + const auto *pc = dynamic_cast(ret->body_.named_expressions[0]->expression_); + ASSERT_TRUE(pc); + + // Check for variable_ + EXPECT_EQ(pc->variable_, nullptr); + + // Check for pattern_ + const auto pattern = pc->pattern_; + ASSERT_TRUE(pattern->atoms_.size() == 3); + + const auto *node1 = dynamic_cast(pattern->atoms_[0]); + const auto *edge = dynamic_cast(pattern->atoms_[1]); + const auto *node2 = dynamic_cast(pattern->atoms_[2]); + + ASSERT_TRUE(node1); + ASSERT_TRUE(edge); + ASSERT_TRUE(node2); + + // Check for filter_ + EXPECT_EQ(pc->filter_, nullptr); + + // Check for resultExpr_ + const auto *result_expr = pc->resultExpr_; + ASSERT_TRUE(result_expr); + } + { + const auto *query = dynamic_cast( + ast_generator.ParseQuery("MATCH (n) RETURN [(n)-->(b) WHERE b.id=1 | b.val] AS res;")); + const auto *ret = dynamic_cast(query->single_query_->clauses_[1]); + + const auto *pc = dynamic_cast(ret->body_.named_expressions[0]->expression_); + ASSERT_TRUE(pc); + + // Check for variable_ + EXPECT_EQ(pc->variable_, nullptr); + + // Check for pattern_ + const auto pattern = pc->pattern_; + ASSERT_TRUE(pattern->atoms_.size() == 3); + + const auto *node1 = dynamic_cast(pattern->atoms_[0]); + const auto *edge = dynamic_cast(pattern->atoms_[1]); + const auto *node2 = dynamic_cast(pattern->atoms_[2]); + + ASSERT_TRUE(node1); + ASSERT_TRUE(edge); + ASSERT_TRUE(node2); + + // Check for filter_ + const auto *filter = pc->filter_; + ASSERT_TRUE(filter); + ASSERT_TRUE(filter->expression_); + + // Check for resultExpr_ + const auto *result_expr = pc->resultExpr_; + ASSERT_TRUE(result_expr); + } + { + const auto *query = dynamic_cast( + ast_generator.ParseQuery("MATCH (n) RETURN [p = (n)-->(b) WHERE b.id=1 | b.val] AS res;")); + const auto *ret = dynamic_cast(query->single_query_->clauses_[1]); + + const auto *pc = dynamic_cast(ret->body_.named_expressions[0]->expression_); + ASSERT_TRUE(pc); + + // Check for variable_ + ASSERT_TRUE(pc->variable_); + + // Check for pattern_ + const auto pattern = pc->pattern_; + ASSERT_TRUE(pattern->atoms_.size() == 3); + + const auto *node1 = dynamic_cast(pattern->atoms_[0]); + const auto *edge = dynamic_cast(pattern->atoms_[1]); + const auto *node2 = dynamic_cast(pattern->atoms_[2]); + + ASSERT_TRUE(node1); + ASSERT_TRUE(edge); + ASSERT_TRUE(node2); + + // Check for filter_ + const auto *filter = pc->filter_; + ASSERT_TRUE(filter); + ASSERT_TRUE(filter->expression_); + + // Check for resultExpr_ + const auto *result_expr = pc->resultExpr_; + ASSERT_TRUE(result_expr); + } +} diff --git a/tests/unit/query_common.hpp b/tests/unit/query_common.hpp index a14ef2d30..c18e06abf 100644 --- a/tests/unit/query_common.hpp +++ b/tests/unit/query_common.hpp @@ -1,4 +1,4 @@ -// Copyright 2023 Memgraph Ltd. +// Copyright 2024 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -635,3 +635,5 @@ auto GetForeach(AstStorage &storage, NamedExpression *named_expr, const std::vec #define DROP_USER(usernames) storage.Create((usernames)) #define CALL_PROCEDURE(...) memgraph::query::test_common::GetCallProcedure(storage, __VA_ARGS__) #define CALL_SUBQUERY(...) memgraph::query::test_common::GetCallSubquery(this->storage, __VA_ARGS__) +#define PATTERN_COMPREHENSION(variable, pattern, filter, resultExpr) \ + this->storage.template Create(variable, pattern, filter, resultExpr) diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp index c4bb966eb..50a52c828 100644 --- a/tests/unit/query_semantic.cpp +++ b/tests/unit/query_semantic.cpp @@ -1442,3 +1442,27 @@ TYPED_TEST(TestSymbolGenerator, PropertyCachingMixedLookups2) { ASSERT_TRUE(prop3_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); ASSERT_TRUE(prop4_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); } + +TYPED_TEST(TestSymbolGenerator, PatternComprehension) { + auto prop = this->dba.NameToProperty("prop"); + + // MATCH (n) RETURN [(n)-[edge]->(m) | m.prop] AS alias + auto query = QUERY(SINGLE_QUERY( + MATCH(PATTERN(NODE("n"))), + RETURN(NEXPR("alias", PATTERN_COMPREHENSION(nullptr, + PATTERN(NODE("n"), EDGE("edge", EdgeAtom::Direction::BOTH, {}, false), + NODE("m", std::nullopt, false)), + nullptr, PROPERTY_LOOKUP(this->dba, "m", prop)))))); + + auto symbol_table = MakeSymbolTable(query); + ASSERT_EQ(symbol_table.max_position(), 7); + + memgraph::query::plan::UsedSymbolsCollector collector(symbol_table); + auto *ret = dynamic_cast(query->single_query_->clauses_[1]); + auto *pc = dynamic_cast(ret->body_.named_expressions[0]->expression_); + + pc->Accept(collector); + + // n, edge, m, Path + ASSERT_EQ(collector.symbols_.size(), 4); +}