diff --git a/CMakeLists.txt b/CMakeLists.txt index 644968078..78418e9bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -360,7 +360,8 @@ set(memgraph_src_files ${src_dir}/query/typed_value.cpp ${src_dir}/query/interpret/awesome_memgraph_functions.cpp ${src_dir}/query/plan/operator.cpp - ${src_dir}/query/plan/planner.cpp + ${src_dir}/query/plan/rule_based_planner.cpp + ${src_dir}/query/plan/variable_start_planner.cpp ${src_dir}/query/frontend/semantic/symbol_generator.cpp ) # ----------------------------------------------------------------------------- diff --git a/src/query/plan/planner.hpp b/src/query/plan/planner.hpp index 6da664b9d..e106b4794 100644 --- a/src/query/plan/planner.hpp +++ b/src/query/plan/planner.hpp @@ -12,60 +12,76 @@ class SymbolTable; namespace plan { -// Normalized representation of a pattern that needs to be matched. +/// @brief Normalized representation of a pattern that needs to be matched. struct Expansion { - // The first node in the expansion, it can be a single node. + /// @brief The first node in the expansion, it can be a single node. NodeAtom *node1 = nullptr; - // Optional edge which connects the 2 nodes. + /// @brief Optional edge which connects the 2 nodes. EdgeAtom *edge = nullptr; - // Optional node at the other end of an edge. If the expansion contains an - // edge, then this node is required. + /// @brief Direction of the edge, it may be flipped compared to original + /// @c EdgeAtom during plan generation. + EdgeAtom::Direction direction = EdgeAtom::Direction::BOTH; + /// @brief Optional node at the other end of an edge. If the expansion + /// contains an edge, then this node is required. NodeAtom *node2 = nullptr; }; -// Normalized representation of a single or multiple Match clauses. -// -// For example, `MATCH (a :Label) -[e1]- (b) -[e2]- (c) MATCH (n) -[e3]- (m) -// WHERE c.prop < 42` will produce the following. -// Expansions will store `(a) -[e1]-(b)`, `(b) -[e2]- (c)` and `(n) -[e3]- (m)`. -// Edge symbols for Cyphermorphism will only contain the set `{e1, e2}` for the -// first `MATCH` and the set `{e3}` for the second. -// Filters will contain 2 pairs. One for testing `:Label` on symbol `a` and the -// other obtained from `WHERE` on symbol `c`. +/// @brief Normalized representation of a single or multiple Match clauses. +/// +/// For example, `MATCH (a :Label) -[e1]- (b) -[e2]- (c) MATCH (n) -[e3]- (m) +/// WHERE c.prop < 42` will produce the following. +/// Expansions will store `(a) -[e1]-(b)`, `(b) -[e2]- (c)` and +/// `(n) -[e3]- (m)`. +/// Edge symbols for Cyphermorphism will only contain the set `{e1, e2}` for the +/// first `MATCH` and the set `{e3}` for the second. +/// Filters will contain 2 pairs. One for testing `:Label` on symbol `a` and the +/// other obtained from `WHERE` on symbol `c`. struct Matching { - // All expansions that need to be performed across Match clauses. + /// @brief All expansions that need to be performed across @c Match clauses. std::vector<Expansion> expansions; - // Symbols for edges established in match, used to ensure Cyphermorphism. - // There are multiple sets, because each Match clause determines a single set. + /// @brief Symbols for edges established in match, used to ensure + /// Cyphermorphism. + /// + /// There are multiple sets, because each Match clause determines a single + /// set. std::vector<std::unordered_set<Symbol>> edge_symbols; - // Pairs of filter expression and symbols used in them. The list should be - // filled using CollectPatternFilters function. + /// @brief Pairs of filter expression and symbols used in them. std::vector<std::pair<Expression *, std::unordered_set<Symbol>>> filters; }; -// Represents a read (+ write) part of a query. Each part ends with either: -// * RETURN clause; -// * WITH clause or -// * any of the write clauses. -// -// For a query `MATCH (n) MERGE (n) -[e]- (m) SET n.x = 42 MERGE (l)` the -// generated QueryPart will have `matching` generated for the `MATCH`. -// `remaining_clauses` will contain `Merge`, `SetProperty` and `Merge` clauses -// in that exact order. The pattern inside the first `MERGE` will be used to -// generate the first `merge_matching` element, and the second `MERGE` pattern -// will produce the second `merge_matching` element. This way, if someone -// traverses `remaining_clauses`, the order of appearance of `Merge` clauses is -// in the same order as their respective `merge_matching` elements. +/// @brief Represents a read (+ write) part of a query. Parts are split on +/// `WITH` clauses. +/// +/// Each part ends with either: +/// +/// * `RETURN` clause; +/// * `WITH` clause or +/// * any of the write clauses. +/// +/// For a query `MATCH (n) MERGE (n) -[e]- (m) SET n.x = 42 MERGE (l)` the +/// generated QueryPart will have `matching` generated for the `MATCH`. +/// `remaining_clauses` will contain `Merge`, `SetProperty` and `Merge` clauses +/// in that exact order. The pattern inside the first `MERGE` will be used to +/// generate the first `merge_matching` element, and the second `MERGE` pattern +/// will produce the second `merge_matching` element. This way, if someone +/// traverses `remaining_clauses`, the order of appearance of `Merge` clauses is +/// in the same order as their respective `merge_matching` elements. struct QueryPart { - // All MATCH clauses merged into one Matching. + /// @brief All `MATCH` clauses merged into one @c Matching. Matching matching; - // Each OPTIONAL MATCH converted to Matching. + /// @brief Each `OPTIONAL MATCH` converted to @c Matching. std::vector<Matching> optional_matching; - // Matching for each MERGE clause. Since Merge is contained in - // remaining_clauses, this vector contains matching in the same order as Merge - // appears. + /// @brief @c Matching for each `MERGE` clause. + /// + /// Storing the normalized pattern of a @c Merge does not preclude storing the + /// @c Merge clause itself inside `remaining_clauses`. The reason is that we + /// need to have access to other parts of the clause, such as `SET` clauses + /// which need to be run. + /// + /// Since @c Merge is contained in `remaining_clauses`, this vector contains + /// matching in the same order as @c Merge appears. std::vector<Matching> merge_matching; - // All the remaining clauses (without Match). + /// @brief All the remaining clauses (without @c Match). std::vector<Clause *> remaining_clauses; }; @@ -93,17 +109,49 @@ struct PlanningContext { std::unordered_set<Symbol> bound_symbols; }; +/// @brief Planner which uses hardcoded rules to produce operators. +/// +/// @sa MakeLogicalPlan class RuleBasedPlanner { public: RuleBasedPlanner(PlanningContext &context) : context_(context) {} + /// @brief The result of plan generation is the root of the generated operator + /// tree. using PlanResult = std::unique_ptr<LogicalOperator>; + /// @brief Generates the operator tree based on explicitly set rules. PlanResult Plan(std::vector<QueryPart> &); private: PlanningContext &context_; }; +/// @brief Planner which generates multiple plans by changing the order of graph +/// traversal. +/// +/// This planner picks different starting nodes from which to start graph +/// traversal. Generating a single plan is backed by @c RuleBasedPlanner. +/// +/// @sa MakeLogicalPlan +class VariableStartPlanner { + public: + VariableStartPlanner(PlanningContext &context) : context_(context) {} + + /// @brief The result of plan generation is a vector of roots to multiple + /// generated operator trees. + using PlanResult = std::vector<std::unique_ptr<LogicalOperator>>; + /// @brief Generate multiple plans by varying the order of graph traversal. + PlanResult Plan(std::vector<QueryPart> &); + + private: + PlanningContext &context_; +}; + +/// @brief Convert the AST to multiple @c QueryParts. +/// +/// This function will normalize patterns inside @c Match and @c Merge clauses +/// and do some other preprocessing in order to generate multiple @c QueryPart +/// structures. std::vector<QueryPart> CollectQueryParts(const SymbolTable &, AstTreeStorage &); /// @brief Generates the LogicalOperator tree and returns the resulting plan. @@ -117,6 +165,10 @@ std::vector<QueryPart> CollectQueryParts(const SymbolTable &, AstTreeStorage &); /// table. /// @param db Optional @c GraphDbAccessor, which is used to query database /// information in order to improve generated plans. +/// @return @c PlanResult which depends on the @c TPlanner used. +/// +/// @sa RuleBasedPlanner +/// @sa VariableStartPlanner template <class TPlanner> typename TPlanner::PlanResult MakeLogicalPlan( AstTreeStorage &storage, SymbolTable &symbol_table, diff --git a/src/query/plan/planner.cpp b/src/query/plan/rule_based_planner.cpp similarity index 99% rename from src/query/plan/planner.cpp rename to src/query/plan/rule_based_planner.cpp index 1590d56a0..4a9a878bd 100644 --- a/src/query/plan/planner.cpp +++ b/src/query/plan/rule_based_planner.cpp @@ -596,7 +596,8 @@ std::vector<Expansion> NormalizePatterns( auto ignore_node = [&](auto *node) {}; auto collect_expansion = [&](auto *prev_node, auto *edge, auto *current_node) { - expansions.emplace_back(Expansion{prev_node, edge, current_node}); + expansions.emplace_back( + Expansion{prev_node, edge, edge->direction_, current_node}); }; for (const auto &pattern : patterns) { if (pattern->atoms_.size() == 1U) { @@ -710,7 +711,7 @@ LogicalOperator *PlanMatching(const Matching &matching, context.new_symbols.emplace_back(edge_symbol); } last_op = - new Expand(node_symbol, edge_symbol, expansion.edge->direction_, + new Expand(node_symbol, edge_symbol, expansion.direction, std::shared_ptr<LogicalOperator>(last_op), node1_symbol, existing_node, existing_edge, context.graph_view); if (!existing_edge) { diff --git a/src/query/plan/variable_start_planner.cpp b/src/query/plan/variable_start_planner.cpp new file mode 100644 index 000000000..5481cbc86 --- /dev/null +++ b/src/query/plan/variable_start_planner.cpp @@ -0,0 +1,253 @@ +#include "query/plan/planner.hpp" + +namespace query::plan { + +namespace { + +class NodeSymbolHash { + public: + NodeSymbolHash(const SymbolTable &symbol_table) + : symbol_table_(symbol_table) {} + + size_t operator()(const NodeAtom *node_atom) const { + return std::hash<Symbol>{}(symbol_table_.at(*node_atom->identifier_)); + } + + private: + const SymbolTable &symbol_table_; +}; + +class NodeSymbolEqual { + public: + NodeSymbolEqual(const SymbolTable &symbol_table) + : symbol_table_(symbol_table) {} + + size_t operator()(const NodeAtom *node_atom1, + const NodeAtom *node_atom2) const { + return symbol_table_.at(*node_atom1->identifier_) == + symbol_table_.at(*node_atom2->identifier_); + } + + private: + const SymbolTable &symbol_table_; +}; + +// Finds the next Expansion which has one of its nodes among the already +// expanded nodes. The function may modify expansions, by flipping their nodes +// and direction. This is done, so that the return iterator always points to the +// expansion whose node1 is the already expanded one, while node2 may not be. +auto NextExpansion(const std::unordered_set<const NodeAtom *, NodeSymbolHash, + NodeSymbolEqual> &expanded_nodes, + std::vector<Expansion> &expansions) { + auto expansion_it = expansions.begin(); + for (; expansion_it != expansions.end(); ++expansion_it) { + if (expanded_nodes.find(expansion_it->node1) != expanded_nodes.end()) { + return expansion_it; + } + auto *node2 = expansion_it->node2; + if (node2 && expanded_nodes.find(node2) != expanded_nodes.end()) { + // We need to flip the expansion, since we want to expand from node2. + std::swap(expansion_it->node2, expansion_it->node1); + if (expansion_it->direction != EdgeAtom::Direction::BOTH) { + expansion_it->direction = + expansion_it->direction == EdgeAtom::Direction::IN + ? EdgeAtom::Direction::OUT + : EdgeAtom::Direction::IN; + } + return expansion_it; + } + } + return expansion_it; +} + +// Generates expansions emanating from the start_node by forming a chain. When +// the chain can no longer be continued, a different starting node is picked +// among remaining expansions and the process continues. This is done until all +// original_expansions are used. +std::vector<Expansion> ExpansionsFrom( + const NodeAtom *start_node, std::vector<Expansion> original_expansions, + const SymbolTable &symbol_table) { + std::vector<Expansion> expansions; + std::unordered_set<const NodeAtom *, NodeSymbolHash, NodeSymbolEqual> + expanded_nodes({start_node}, original_expansions.size(), + NodeSymbolHash(symbol_table), + NodeSymbolEqual(symbol_table)); + while (!original_expansions.empty()) { + auto next_it = NextExpansion(expanded_nodes, original_expansions); + if (next_it == original_expansions.end()) { + // Pick a new starting expansion, since we cannot continue the chain. + next_it = original_expansions.begin(); + } + expanded_nodes.insert(next_it->node1); + if (next_it->node2) { + expanded_nodes.insert(next_it->node2); + } + expansions.emplace_back(*next_it); + original_expansions.erase(next_it); + } + return expansions; +} + +// Collect all unique nodes from expansions. Uniqueness is determined by +// symbol uniqueness. +auto ExpansionNodes(const std::vector<Expansion> &expansions, + const SymbolTable &symbol_table) { + std::unordered_set<NodeAtom *, NodeSymbolHash, NodeSymbolEqual> nodes( + expansions.size(), NodeSymbolHash(symbol_table), + NodeSymbolEqual(symbol_table)); + for (const auto &expansion : expansions) { + // TODO: Handle labels and properties from different node atoms. + nodes.insert(expansion.node1); + if (expansion.node2) { + nodes.insert(expansion.node2); + } + } + return nodes; +} + +// Generates n matchings, where n is the number of nodes to match. Each Matching +// will have a different node as a starting node for expansion. +std::vector<Matching> VaryMatchingStart(const Matching &matching, + const SymbolTable &symbol_table) { + if (matching.expansions.empty()) { + return std::vector<Matching>{matching}; + } + const auto start_nodes = ExpansionNodes(matching.expansions, symbol_table); + std::vector<Matching> permutations; + for (const auto &start_node : start_nodes) { + permutations.emplace_back( + Matching{ExpansionsFrom(start_node, matching.expansions, symbol_table), + matching.edge_symbols, matching.filters}); + } + return permutations; +} + +// Produces a Cartesian product among vectors between begin and end iterator. +// For example: +// +// std::vector<int> first_set{1,2,3}; +// std::vector<int> second_set{4,5}; +// std::vector<std::vector<int>> all_sets{first_set, second_set}; +// // prod should be {{1, 4}, {1, 5}, {2, 4}, {2, 5}, {3, 4}, {3, 5}} +// auto prod = CartesianProduct(all_sets.cbegin(), all_sets.cend()) +template <typename T> +std::vector<std::vector<T>> CartesianProduct( + typename std::vector<std::vector<T>>::const_iterator begin, + typename std::vector<std::vector<T>>::const_iterator end) { + std::vector<std::vector<T>> products; + if (begin == end) { + return products; + } + auto later_products = CartesianProduct<T>(begin + 1, end); + for (const auto &elem : *begin) { + if (later_products.empty()) { + products.emplace_back(std::vector<T>{elem}); + } else { + for (const auto &rest : later_products) { + std::vector<T> product{elem}; + product.insert(product.end(), rest.begin(), rest.end()); + products.emplace_back(std::move(product)); + } + } + } + return products; +} + +// Similar to VaryMatchingStart, but varies the starting nodes for all given +// matchings. After all matchings produce multiple alternative starts, the +// Cartesian product of all of them is returned. +std::vector<std::vector<Matching>> VaryMultiMatchingStarts( + const std::vector<Matching> &matchings, const SymbolTable &symbol_table) { + std::vector<std::vector<Matching>> variants; + for (const auto &matching : matchings) { + variants.emplace_back(VaryMatchingStart(matching, symbol_table)); + } + return CartesianProduct<Matching>(variants.cbegin(), variants.cend()); +} + +// Produces alternative query parts out of a single part by varying how each +// graph matching is done. +std::vector<QueryPart> VaryQuertPartMatching(const QueryPart &query_part, + const SymbolTable &symbol_table) { + std::vector<QueryPart> variants; + // Get multiple regular matchings, each starting from different node. + auto matchings = VaryMatchingStart(query_part.matching, symbol_table); + // Get multiple optional matchings, where each combination has different + // starting nodes. + auto optional_matchings = + VaryMultiMatchingStarts(query_part.optional_matching, symbol_table); + // Like optional matching, but for merge matchings. + auto merge_matchings = + VaryMultiMatchingStarts(query_part.merge_matching, symbol_table); + // After we have all valid combinations of each matching, we need to produce + // combinations of them. This is similar to Cartesian product, but some + // matchings can be empty (optional and merge) and `matchings` is of different + // type (vector) than `optional_matchings` and `merge_matchings` (which are + // vectors of vectors). + for (const auto &matching : matchings) { + // matchings will always have at least a single element, so we can use a for + // loop. On the other hand, optional and merge matchings can be empty so we + // need an iterator and do...while loop. + auto optional_it = optional_matchings.begin(); + auto optional_end = optional_matchings.end(); + do { + auto merge_it = merge_matchings.begin(); + auto merge_end = merge_matchings.end(); + do { + // Produce parts for each possible combination. E.g. if we have: + // * matchings (m1) and (m2) + // * optional matchings (o1) and (o2) + // * merge matching (g1) + // We want to produce parts for: + // * (m1), (o1), (g1) + // * (m1), (o2), (g1) + // * (m2), (o1), (g1) + // * (m2), (o2), (g1) + variants.emplace_back(QueryPart{matching}); + variants.back().remaining_clauses = query_part.remaining_clauses; + if (optional_it != optional_matchings.end()) { + // In case we started with empty optional matchings. + variants.back().optional_matching = *optional_it; + } + if (merge_it != merge_matchings.end()) { + // In case we started with empty merge matchings. + variants.back().merge_matching = *merge_it; + } + // Since we can start with the iterator at the end, we have to first + // compare it and then increment it. After we increment, we need to + // check again to avoid generating with empty matching. + } while (merge_it != merge_end && ++merge_it != merge_end); + } while (optional_it != optional_end && ++optional_it != optional_end); + } + return variants; +} + +// Generates different, equivalent query parts by taking different graph +// matching routes for each query part. +std::vector<std::vector<QueryPart>> VaryQueryMatching( + const std::vector<QueryPart> &query_parts, + const SymbolTable &symbol_table) { + std::vector<std::vector<QueryPart>> alternative_query_parts; + for (const auto &query_part : query_parts) { + alternative_query_parts.emplace_back( + VaryQuertPartMatching(query_part, symbol_table)); + } + return CartesianProduct<QueryPart>(alternative_query_parts.cbegin(), + alternative_query_parts.cend()); +} + +} // namespace + +std::vector<std::unique_ptr<LogicalOperator>> VariableStartPlanner::Plan( + std::vector<QueryPart> &query_parts) { + std::vector<std::unique_ptr<LogicalOperator>> plans; + auto alternatives = VaryQueryMatching(query_parts, context_.symbol_table); + RuleBasedPlanner rule_planner(context_); + for (auto &alternative_query_parts : alternatives) { + context_.bound_symbols.clear(); + plans.emplace_back(rule_planner.Plan(alternative_query_parts)); + } + return plans; +} + +} // namespace query::plan diff --git a/tests/unit/query_variable_start_planner.cpp b/tests/unit/query_variable_start_planner.cpp new file mode 100644 index 000000000..a67d1d274 --- /dev/null +++ b/tests/unit/query_variable_start_planner.cpp @@ -0,0 +1,214 @@ +#include <algorithm> + +#include "gtest/gtest.h" + +#include "dbms/dbms.hpp" +#include "query/frontend/semantic/symbol_generator.hpp" +#include "query/frontend/semantic/symbol_table.hpp" +#include "query/plan/planner.hpp" +#include "utils/algorithm.hpp" + +#include "query_plan_common.hpp" + +using namespace query::plan; +using query::AstTreeStorage; +using Direction = query::EdgeAtom::Direction; + +namespace std { + +// Overloads for printing resulting rows from a query. +std::ostream &operator<<(std::ostream &stream, + const std::vector<TypedValue> &row) { + PrintIterable(stream, row); + return stream; +} +std::ostream &operator<<(std::ostream &stream, + const std::vector<std::vector<TypedValue>> &rows) { + PrintIterable(stream, rows, "\n"); + return stream; +} + +} // namespace std + +namespace { + +auto MakeSymbolTable(query::Query &query) { + query::SymbolTable symbol_table; + query::SymbolGenerator symbol_generator(symbol_table); + query.Accept(symbol_generator); + return symbol_table; +} + +void AssertRows(const std::vector<std::vector<TypedValue>> &datum, + std::vector<std::vector<TypedValue>> expected) { + auto row_equal = [](const auto &row1, const auto &row2) { + if (row1.size() != row2.size()) { + return false; + } + TypedValue::BoolEqual value_eq; + auto row1_it = row1.begin(); + for (auto row2_it = row2.begin(); row2_it != row2.end(); + ++row1_it, ++row2_it) { + if (!value_eq(*row1_it, *row2_it)) { + return false; + } + } + return true; + }; + ASSERT_TRUE(std::is_permutation(datum.begin(), datum.end(), expected.begin(), + expected.end(), row_equal)) + << "Actual rows:" << std::endl + << datum << std::endl + << "Expected rows:" << std::endl + << expected; +}; + +void CheckPlansProduce( + size_t expected_plan_count, AstTreeStorage &storage, GraphDbAccessor &dba, + std::function<void(const std::vector<std::vector<TypedValue>> &)> check) { + auto symbol_table = MakeSymbolTable(*storage.query()); + auto plans = + MakeLogicalPlan<VariableStartPlanner>(storage, symbol_table, &dba); + EXPECT_EQ(std::distance(plans.begin(), plans.end()), expected_plan_count); + for (const auto &plan : plans) { + auto *produce = dynamic_cast<Produce *>(plan.get()); + ASSERT_TRUE(produce); + auto results = CollectProduce(produce, symbol_table, dba); + check(results); + } +} + +TEST(TestVariableStartPlanner, MatchReturn) { + Dbms dbms; + auto dba = dbms.active(); + // Make a graph (v1) -[:r]-> (v2) + auto v1 = dba->insert_vertex(); + auto v2 = dba->insert_vertex(); + dba->insert_edge(v1, v2, dba->edge_type("r")); + dba->advance_command(); + // Test MATCH (n) -[r]-> (m) RETURN n + AstTreeStorage storage; + QUERY( + MATCH(PATTERN(NODE("n"), EDGE("r", nullptr, Direction::OUT), NODE("m"))), + RETURN("n")); + // We have 2 nodes `n` and `m` from which we could start, so expect 2 plans. + CheckPlansProduce(2, storage, *dba, [&](const auto &results) { + // We expect to produce only a single (v1) node. + AssertRows(results, {{v1}}); + }); +} + +TEST(TestVariableStartPlanner, MatchTripletPatternReturn) { + Dbms dbms; + auto dba = dbms.active(); + // Make a graph (v1) -[:r]-> (v2) -[:r]-> (v3) + auto v1 = dba->insert_vertex(); + auto v2 = dba->insert_vertex(); + auto v3 = dba->insert_vertex(); + dba->insert_edge(v1, v2, dba->edge_type("r")); + dba->insert_edge(v2, v3, dba->edge_type("r")); + dba->advance_command(); + { + // Test `MATCH (n) -[r]-> (m) -[e]-> (l) RETURN n` + AstTreeStorage storage; + QUERY( + MATCH(PATTERN(NODE("n"), EDGE("r", nullptr, Direction::OUT), NODE("m"), + EDGE("e", nullptr, Direction::OUT), NODE("l"))), + RETURN("n")); + // We have 3 nodes: `n`, `m` and `l` from which we could start. + CheckPlansProduce(3, storage, *dba, [&](const auto &results) { + // We expect to produce only a single (v1) node. + AssertRows(results, {{v1}}); + }); + } + { + // Equivalent to `MATCH (n) -[r]-> (m), (m) -[e]-> (l) RETURN n`. + AstTreeStorage storage; + QUERY( + MATCH( + PATTERN(NODE("n"), EDGE("r", nullptr, Direction::OUT), NODE("m")), + PATTERN(NODE("m"), EDGE("e", nullptr, Direction::OUT), NODE("l"))), + RETURN("n")); + CheckPlansProduce(3, storage, *dba, [&](const auto &results) { + AssertRows(results, {{v1}}); + }); + } +} + +TEST(TestVariableStartPlanner, MatchOptionalMatchReturn) { + Dbms dbms; + auto dba = dbms.active(); + // Make a graph (v1) -[:r]-> (v2) -[:r]-> (v3) + auto v1 = dba->insert_vertex(); + auto v2 = dba->insert_vertex(); + auto v3 = dba->insert_vertex(); + dba->insert_edge(v1, v2, dba->edge_type("r")); + dba->insert_edge(v2, v3, dba->edge_type("r")); + dba->advance_command(); + // Test MATCH (n) -[r]-> (m) OPTIONAL MATCH (m) -[e]-> (l) RETURN n, l + AstTreeStorage storage; + QUERY( + MATCH(PATTERN(NODE("n"), EDGE("r", nullptr, Direction::OUT), NODE("m"))), + OPTIONAL_MATCH( + PATTERN(NODE("m"), EDGE("e", nullptr, Direction::OUT), NODE("l"))), + RETURN("n", "l")); + // We have 2 nodes `n` and `m` from which we could start the MATCH, and 2 + // nodes for OPTIONAL MATCH. This should produce 2 * 2 plans. + CheckPlansProduce(4, storage, *dba, [&](const auto &results) { + // We expect to produce 2 rows: + // * (v1), (v3) + // * (v2), null + AssertRows(results, {{v1, v3}, {v2, TypedValue::Null}}); + }); +} + +TEST(TestVariableStartPlanner, MatchOptionalMatchMergeReturn) { + Dbms dbms; + auto dba = dbms.active(); + // Graph (v1) -[:r]-> (v2) + auto v1 = dba->insert_vertex(); + auto v2 = dba->insert_vertex(); + auto r_type = dba->edge_type("r"); + dba->insert_edge(v1, v2, r_type); + dba->advance_command(); + // Test MATCH (n) -[r]-> (m) OPTIONAL MATCH (m) -[e]-> (l) + // MERGE (u) -[q:r]-> (v) RETURN n, m, l, u, v + AstTreeStorage storage; + QUERY( + MATCH(PATTERN(NODE("n"), EDGE("r", nullptr, Direction::OUT), NODE("m"))), + OPTIONAL_MATCH( + PATTERN(NODE("m"), EDGE("e", nullptr, Direction::OUT), NODE("l"))), + MERGE(PATTERN(NODE("u"), EDGE("q", r_type, Direction::OUT), NODE("v"))), + RETURN("n", "m", "l", "u", "v")); + // Since MATCH, OPTIONAL MATCH and MERGE each have 2 nodes from which we can + // start, we generate 2 * 2 * 2 plans. + CheckPlansProduce(8, storage, *dba, [&](const auto &results) { + // We expect to produce a single row: (v1), (v2), null, (v1), (v2) + AssertRows(results, {{v1, v2, TypedValue::Null, v1, v2}}); + }); +} + +TEST(TestVariableStartPlanner, MatchWithMatchReturn) { + Dbms dbms; + auto dba = dbms.active(); + // Graph (v1) -[:r]-> (v2) + auto v1 = dba->insert_vertex(); + auto v2 = dba->insert_vertex(); + dba->insert_edge(v1, v2, dba->edge_type("r")); + dba->advance_command(); + // Test MATCH (n) -[r]-> (m) WITH n MATCH (m) -[r]-> (l) RETURN n, m, l + AstTreeStorage storage; + QUERY( + MATCH(PATTERN(NODE("n"), EDGE("r", nullptr, Direction::OUT), NODE("m"))), + WITH("n"), + MATCH(PATTERN(NODE("m"), EDGE("r", nullptr, Direction::OUT), NODE("l"))), + RETURN("n", "m", "l")); + // We can start from 2 nodes in each match. Since WITH separates query parts, + // we expect to get 2 plans for each, which totals 2 * 2. + CheckPlansProduce(4, storage, *dba, [&](const auto &results) { + // We expect to produce a single row: (v1), (v1), (v2) + AssertRows(results, {{v1, v1, v2}}); + }); +} + +} // namespace