From 0819b40202897ce85f9c4d571e2af0b7dd1dd6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Sa=C4=8Dari=C4=87?= Date: Wed, 29 Mar 2023 16:39:41 +0200 Subject: [PATCH] Fix bug on AllShortest with multiple edges between nodes (#832) --- src/query/plan/operator.cpp | 199 +++++----- .../features/memgraph_allshortest.feature | 368 +++++++++--------- 2 files changed, 303 insertions(+), 264 deletions(-) diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 322a08155..f9e8b7b45 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -1834,7 +1834,7 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { : self_(self), input_cursor_(self_.input_->MakeCursor(mem)), visited_cost_(mem), - expanded_(mem), + total_cost_(mem), next_edges_(mem), traversal_stack_(mem), pq_(mem) {} @@ -1844,6 +1844,9 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor, storage::View::OLD); + auto create_state = [this](const VertexAccessor &vertex, int64_t depth) { + return std::make_pair(vertex, upper_bound_set_ ? depth : 0); + }; // For the given (edge, direction, weight, depth) tuple checks if they // satisfy the "where" condition. if so, places them in the priority @@ -1935,73 +1938,118 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { } }; - // Check if upper bound exists - upper_bound_ = self_.upper_bound_ - ? EvaluateInt(&evaluator, self_.upper_bound_, "Max depth in all shortest paths expansion") - : std::numeric_limits::max(); + std::optional start_vertex; + auto *memory = context.evaluation_context.memory; + + auto create_path = [this, &frame, &memory]() { + auto ¤t_level = traversal_stack_.back(); + auto &edges_on_frame = frame[self_.common_.edge_symbol].ValueList(); + + // Clean out the current stack + if (current_level.empty()) { + if (!edges_on_frame.empty()) { + if (!self_.is_reverse_) + edges_on_frame.erase(edges_on_frame.end()); + else + edges_on_frame.erase(edges_on_frame.begin()); + } + traversal_stack_.pop_back(); + return false; + } + + auto [current_edge, current_edge_direction, current_weight] = current_level.back(); + current_level.pop_back(); + + // Edges order depends on direction of expansion + if (!self_.is_reverse_) + edges_on_frame.emplace_back(current_edge); + else + edges_on_frame.emplace(edges_on_frame.begin(), current_edge); + + auto next_vertex = current_edge_direction == EdgeAtom::Direction::IN ? current_edge.From() : current_edge.To(); + frame[self_.total_weight_.value()] = current_weight; + + if (next_edges_.find({next_vertex, traversal_stack_.size()}) != next_edges_.end()) { + auto next_vertex_edges = next_edges_[{next_vertex, traversal_stack_.size()}]; + traversal_stack_.emplace_back(std::move(next_vertex_edges)); + } else { + // Signal the end of iteration + utils::pmr::list empty(memory); + traversal_stack_.emplace_back(std::move(empty)); + } + + if ((current_weight > visited_cost_.at(next_vertex)).ValueBool()) return false; + + // Place destination node on the frame, handle existence flag + if (self_.common_.existing_node) { + const auto &node = frame[self_.common_.node_symbol]; + ExpectType(self_.common_.node_symbol, node, TypedValue::Type::Vertex); + if (node.ValueVertex() != next_vertex) return false; + } else { + frame[self_.common_.node_symbol] = next_vertex; + } + return true; + }; + + auto create_DFS_traversal_tree = [this, &context, &memory, &create_state, &expand_from_vertex]() { + while (!pq_.empty()) { + if (MustAbort(context)) throw HintedAbortError(); + + const auto [current_weight, current_depth, current_vertex, directed_edge] = pq_.top(); + pq_.pop(); + + const auto &[current_edge, direction, weight] = directed_edge; + auto current_state = create_state(current_vertex, current_depth); + + auto position = total_cost_.find(current_state); + if (position != total_cost_.end()) { + if ((position->second < current_weight).ValueBool()) continue; + } else { + total_cost_.emplace(current_state, current_weight); + if (current_depth < upper_bound_) { + expand_from_vertex(current_vertex, current_weight, current_depth); + } + } + + // Searching for a previous vertex in the expansion + auto prev_vertex = direction == EdgeAtom::Direction::IN ? current_edge.To() : current_edge.From(); + + // Update the parent + if (next_edges_.find({prev_vertex, current_depth - 1}) == next_edges_.end()) { + utils::pmr::list empty(memory); + next_edges_[{prev_vertex, current_depth - 1}] = std::move(empty); + } + next_edges_.at({prev_vertex, current_depth - 1}).emplace_back(directed_edge); + } + }; + + // upper_bound_set is used when storing visited edges, because with an upper bound we also consider suboptimal paths + // if they are shorter in depth + if (self_.upper_bound_) { + upper_bound_ = EvaluateInt(&evaluator, self_.upper_bound_, "Max depth in all shortest path expansion"); + upper_bound_set_ = true; + } else { + upper_bound_ = std::numeric_limits::max(); + upper_bound_set_ = false; + } // Check if upper bound is valid if (upper_bound_ < 1) { throw QueryRuntimeException("Maximum depth in all shortest paths expansion must be at least 1."); } - std::optional start_vertex; - auto *memory = context.evaluation_context.memory; - + // On first Pull run, traversal stack and priority queue are empty, so we start a pulling stream + // and create a DFS traversal tree (main part of algorithm). Then we return the first path + // created from the DFS traversal tree (basically a DFS algorithm). + // On each subsequent Pull run, paths are created from the traversal stack and returned. while (true) { // Check if there is an external error. if (MustAbort(context)) throw HintedAbortError(); - // If traversal stack if filled, the DFS traversal tree is created. Traverse the tree iteratively by - // preserving the traversal state on stack. + // The algorithm is run all at once by create_DFS_traversal_tree, after which we + // traverse the tree iteratively by preserving the traversal state on stack. while (!traversal_stack_.empty()) { - auto ¤t_level = traversal_stack_.back(); - auto &edges_on_frame = frame[self_.common_.edge_symbol].ValueList(); - - // Clean out the current stack - if (current_level.empty()) { - if (!edges_on_frame.empty()) { - if (!self_.is_reverse_) - edges_on_frame.erase(edges_on_frame.end()); - else - edges_on_frame.erase(edges_on_frame.begin()); - } - traversal_stack_.pop_back(); - continue; - } - - auto [current_edge, current_edge_direction, current_weight] = current_level.back(); - current_level.pop_back(); - - // Edges order depends on direction of expansion - if (!self_.is_reverse_) - edges_on_frame.emplace_back(current_edge); - else - edges_on_frame.emplace(edges_on_frame.begin(), current_edge); - - auto next_vertex = current_edge_direction == EdgeAtom::Direction::IN ? current_edge.From() : current_edge.To(); - frame[self_.total_weight_.value()] = current_weight; - - if (next_edges_.find({next_vertex, traversal_stack_.size()}) != next_edges_.end()) { - auto next_vertex_edges = next_edges_[{next_vertex, traversal_stack_.size()}]; - traversal_stack_.emplace_back(std::move(next_vertex_edges)); - } else { - // Signal the end of iteration - utils::pmr::list empty(memory); - traversal_stack_.emplace_back(std::move(empty)); - } - - if ((current_weight > visited_cost_.at(next_vertex)).ValueBool()) continue; - - // Place destination node on the frame, handle existence flag - if (self_.common_.existing_node) { - const auto &node = frame[self_.common_.node_symbol]; - ExpectType(self_.common_.node_symbol, node, TypedValue::Type::Vertex); - if (node.ValueVertex() != next_vertex) continue; - } else { - frame[self_.common_.node_symbol] = next_vertex; - } - return true; + if (create_path()) return true; } // If priority queue is empty start new pulling stream. @@ -2022,9 +2070,9 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { // Clear existing data structures. visited_cost_.clear(); - expanded_.clear(); next_edges_.clear(); traversal_stack_.clear(); + total_cost_.clear(); expand_from_vertex(*start_vertex, TypedValue(), 0); visited_cost_.emplace(*start_vertex, 0); @@ -2032,33 +2080,9 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { } // Create a DFS traversal tree from the start node - while (!pq_.empty()) { - if (MustAbort(context)) throw HintedAbortError(); - - const auto [current_weight, current_depth, current_vertex, directed_edge] = pq_.top(); - pq_.pop(); - - const auto &[current_edge, direction, weight] = directed_edge; - if (expanded_.contains(current_edge)) continue; - expanded_.emplace(current_edge); - - // Expand only if what we've just expanded is less than max depth. - if (current_depth < upper_bound_) { - expand_from_vertex(current_vertex, current_weight, current_depth); - } - - // Searching for a previous vertex in the expansion - auto prev_vertex = direction == EdgeAtom::Direction::IN ? current_edge.To() : current_edge.From(); - - // Update the parent - if (next_edges_.find({prev_vertex, current_depth - 1}) == next_edges_.end()) { - utils::pmr::list empty(memory); - next_edges_[{prev_vertex, current_depth - 1}] = std::move(empty); - } - - next_edges_.at({prev_vertex, current_depth - 1}).emplace_back(directed_edge); - } + create_DFS_traversal_tree(); + // DFS traversal tree is create, if (start_vertex && next_edges_.find({*start_vertex, 0}) != next_edges_.end()) { auto start_vertex_edges = next_edges_[{*start_vertex, 0}]; traversal_stack_.emplace_back(std::move(start_vertex_edges)); @@ -2071,9 +2095,9 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { void Reset() override { input_cursor_->Reset(); visited_cost_.clear(); - expanded_.clear(); next_edges_.clear(); traversal_stack_.clear(); + total_cost_.clear(); ClearQueue(); } @@ -2083,6 +2107,7 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { // Upper bound on the path length. int64_t upper_bound_{-1}; + bool upper_bound_set_{false}; struct AspStateHash { size_t operator()(const std::pair &key) const { @@ -2094,8 +2119,8 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { using NextEdgesState = std::pair; // Maps vertices to minimum weights they got in expansion. utils::pmr::unordered_map visited_cost_; - // Marking the expanded edges to prevent multiple visits. - utils::pmr::unordered_set expanded_; + // Maps vertices to weights they got in expansion. + utils::pmr::unordered_map total_cost_; // Maps the vertex with the potential expansion edge. utils::pmr::unordered_map, AspStateHash> next_edges_; // Stack indicating the traversal level. diff --git a/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature b/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature index 7f224a016..73fb9e75b 100644 --- a/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature +++ b/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature @@ -1,191 +1,205 @@ Feature: All Shortest Path - Scenario: Test match allShortest upper bound - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'}) - """ - When executing query: - """ - MATCH (n {a:'0'})-[le *allShortest 1 (e, n | e.w ) w]->(m) RETURN m.a - """ - Then the result should be: - | m.a | - | '1' | - | '3' | + Scenario: Test match allShortest upper bound + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'}) + """ + When executing query: + """ + MATCH (n {a:'0'})-[le *allShortest 1 (e, n | e.w ) w]->(m) RETURN m.a + """ + Then the result should be: + | m.a | + | '1' | + | '3' | - Scenario: Test match allShortest upper bound 2 - Given an empty graph - And having executed: - """ - CREATE (a {a:'0'})-[:r {w: 2}]->(b {a:'1'})-[:r {w: 3}]->(c {a:'2'}), - (a)-[:re {w: 2}]->(b), - (b)-[:re {w:3}]->(c), - ({a: '4'})<-[:r {w: 1}]-(a), - ({a: '5'})<-[:r {w: 1}]-(a), - (c)-[:r {w: 1}]->({a: '6'}), - (c)-[:r {w: 1}]->({a: '7'}) - """ - When executing query: - """ - MATCH path=(n {a:'0'})-[r *allShortest ..2 (e, n | 1 ) w]->(m {a:'2'}) RETURN COUNT(path) AS c - """ - Then the result should be: - | c | - | 4 | + Scenario: Test match allShortest upper bound 2 + Given an empty graph + And having executed: + """ + CREATE (a {a:'0'})-[:r {w: 2}]->(b {a:'1'})-[:r {w: 3}]->(c {a:'2'}), + (a)-[:re {w: 2}]->(b), + (b)-[:re {w:3}]->(c), + ({a: '4'})<-[:r {w: 1}]-(a), + ({a: '5'})<-[:r {w: 1}]-(a), + (c)-[:r {w: 1}]->({a: '6'}), + (c)-[:r {w: 1}]->({a: '7'}) + """ + When executing query: + """ + MATCH path=(n {a:'0'})-[r *allShortest ..2 (e, n | 1 ) w]->(m {a:'2'}) RETURN COUNT(path) AS c + """ + Then the result should be: + | c | + | 4 | - Scenario: Test match allShortest filtered - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'}) - """ - When executing query: - """ - MATCH (n {a:'0'})-[le *allShortest 1 (e, n | e.w ) w (e, n | n.a = '3')]->(m) RETURN m.a - """ - Then the result should be: - | m.a | - | '3' | + Scenario: Test match allShortest filtered + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'}) + """ + When executing query: + """ + MATCH (n {a:'0'})-[le *allShortest 1 (e, n | e.w ) w (e, n | n.a = '3')]->(m) RETURN m.a + """ + Then the result should be: + | m.a | + | '3' | - Scenario: Test match allShortest resulting edge list - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 4}]->({a:'3'}) - """ - When executing query: - """ - MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w - """ - Then the result should be: - | m.a | s | w | - | '1' | 1 | 1 | - | '2' | 2 | 3 | - | '3' | 1 | 4 | + Scenario: Test match allShortest resulting edge list + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 4}]->({a:'3'}) + """ + When executing query: + """ + MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w + """ + Then the result should be: + | m.a | s | w | + | '1' | 1 | 1 | + | '2' | 2 | 3 | + | '3' | 1 | 4 | - Scenario: Test match allShortest single edge type filtered - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) - """ - When executing query: - """ - MATCH ()-[le:r0 *allShortest 10 (e, n | e.w) w]->(m) - RETURN size(le) AS s, m.a - """ - Then the result should be: - | s | m.a | - | 1 | '1' | + Scenario: Test match allShortest single edge type filtered + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) + """ + When executing query: + """ + MATCH ()-[le:r0 *allShortest 10 (e, n | e.w) w]->(m) + RETURN size(le) AS s, m.a + """ + Then the result should be: + | s | m.a | + | 1 | '1' | - Scenario: Test match allShortest multiple edge types filtered - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r1 {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) - """ - When executing query: - """ - MATCH ()-[le :r0|:r1 *allShortest 10 (e, n | e.w) w]->(m) WHERE size(le) > 1 - RETURN size(le) AS s, (le[0]).w AS r0, (le[1]).w AS r1 - """ - Then the result should be: - | s | r0 | r1 | - | 2 | 1 | 2 | + Scenario: Test match allShortest multiple edge types filtered + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r1 {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) + """ + When executing query: + """ + MATCH ()-[le :r0|:r1 *allShortest 10 (e, n | e.w) w]->(m) WHERE size(le) > 1 + RETURN size(le) AS s, (le[0]).w AS r0, (le[1]).w AS r1 + """ + Then the result should be: + | s | r0 | r1 | + | 2 | 1 | 2 | - Scenario: Test match allShortest property filters - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) - """ - When executing query: - """ - MATCH ()-[le *allShortest 10 {w:1} (e, n | e.w ) total_weight]->(m) - RETURN size(le) AS s, (le[0]).w AS r0 - """ - Then the result should be: - | s | r0 | - | 1 | 1 | + Scenario: Test match allShortest property filters + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) + """ + When executing query: + """ + MATCH ()-[le *allShortest 10 {w:1} (e, n | e.w ) total_weight]->(m) + RETURN size(le) AS s, (le[0]).w AS r0 + """ + Then the result should be: + | s | r0 | + | 1 | 1 | - Scenario: Test match allShortest weight not a number - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: 'not a number'}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) - """ - When executing query: - """ - MATCH ()-[le *allShortest 10 (e, n | e.w ) total_weight]->(m) - RETURN le, total_weight - """ - Then an error should be raised + Scenario: Test match allShortest weight not a number + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: 'not a number'}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) + """ + When executing query: + """ + MATCH ()-[le *allShortest 10 (e, n | e.w ) total_weight]->(m) + RETURN le, total_weight + """ + Then an error should be raised - Scenario: Test match allShortest negative weight - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: -1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) - """ - When executing query: - """ - MATCH ()-[le *allShortest 10 (e, n | e.w ) total_weight]->(m) - RETURN le, total_weight - """ - Then an error should be raised + Scenario: Test match allShortest negative weight + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: -1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'}) + """ + When executing query: + """ + MATCH ()-[le *allShortest 10 (e, n | e.w ) total_weight]->(m) + RETURN le, total_weight + """ + Then an error should be raised - Scenario: Test match allShortest weight duration - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: DURATION('PT1S')}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'}) - """ - When executing query: - """ - MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w - """ - Then the result should be: - | m.a | s | w | - | '1' | 1 | PT1S | - | '2' | 2 | PT3S | - | '3' | 1 | PT4S | + Scenario: Test match allShortest weight duration + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: DURATION('PT1S')}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'}) + """ + When executing query: + """ + MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w + """ + Then the result should be: + | m.a | s | w | + | '1' | 1 | PT1S | + | '2' | 2 | PT3S | + | '3' | 1 | PT4S | - Scenario: Test match allShortest weight negative duration - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: DURATION({seconds: -1})}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'}) - """ - When executing query: - """ - MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w - """ - Then an error should be raised + Scenario: Test match allShortest weight negative duration + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: DURATION({seconds: -1})}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'}) + """ + When executing query: + """ + MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w + """ + Then an error should be raised - Scenario: Test match allShortest weight mixed numeric and duration as weights - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: 2}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'}) - """ - When executing query: - """ - MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w - """ - Then an error should be raised + Scenario: Test match allShortest weight mixed numeric and duration as weights + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: 2}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'}) + """ + When executing query: + """ + MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w + """ + Then an error should be raised - Scenario: Test allShortest return both paths of same length - Given an empty graph - And having executed: - """ - CREATE (n {a:'0'})-[:r {w: 2}]->({a:'1'})-[:r {w: 3}]->({a:'2'}), (n)-[:r {w: 5}]->({a:'2'}) - """ - When executing query: - """ - MATCH path=(n {a:'0'})-[r *allShortest (e, n | e.w ) w]->(m {a:'2'}) RETURN COUNT(path); - """ - Then the result should be: - | COUNT(path) | - | 2 | + Scenario: Test allShortest return both paths of same length + Given an empty graph + And having executed: + """ + CREATE (n {a:'0'})-[:r {w: 2}]->({a:'1'})-[:r {w: 3}]->({a:'2'}), (n)-[:r {w: 5}]->({a:'2'}) + """ + When executing query: + """ + MATCH path=(n {a:'0'})-[r *allShortest (e, n | e.w ) w]->(m {a:'2'}) RETURN COUNT(path); + """ + Then the result should be: + | COUNT(path) | + | 2 | + + Scenario: Test allShortest on different edge between two nodes + Given an empty graph + And having executed: + """ + CREATE (n:One), (o:Two), (m:Three), (n)-[:TYPE {cost: 0.3}]->(o), (o)-[:TYPE {cost: 40}]->(m), (o)-[:TYPE {cost: 20}]->(m) + """ + When executing query: + """ + MATCH p=(h:One)-[r*allshortest ..5 (e, v | e.cost) total_cost]->(k:Three) return total_cost; + """ + Then the result should be: + | total_cost | + | 20.3 |