diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp index f683494fa..2bec78b9c 100644 --- a/src/query/frontend/ast/ast.lcp +++ b/src/query/frontend/ast/ast.lcp @@ -1380,7 +1380,7 @@ cpp<# :documentation "Variable where the total weight for weighted shortest path will be stored.")) (:public (lcp:define-enum type - (single depth-first breadth-first weighted-shortest-path) + (single depth-first breadth-first weighted-shortest-path all-shortest-paths) (:serialize)) (lcp:define-enum direction (in out both) @@ -1432,6 +1432,7 @@ cpp<# case Type::DEPTH_FIRST: case Type::BREADTH_FIRST: case Type::WEIGHTED_SHORTEST_PATH: + case Type::ALL_SHORTEST_PATHS: return true; case Type::SINGLE: return false; diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index 002710f1e..7686223ed 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -1619,9 +1619,10 @@ antlrcpp::Any CypherMainVisitor::visitRelationshipPattern(MemgraphCypher::Relati auto relationshipLambdas = relationshipDetail->relationshipLambda(); if (variableExpansion) { - if (relationshipDetail->total_weight && edge->type_ != EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) + if (relationshipDetail->total_weight && edge->type_ != EdgeAtom::Type::WEIGHTED_SHORTEST_PATH && + edge->type_ != EdgeAtom::Type::ALL_SHORTEST_PATHS) throw SemanticException( - "Variable for total weight is allowed only with weighted shortest " + "Variable for total weight is allowed only with weighted and all shortest " "path expansion."); auto visit_lambda = [this](auto *lambda) { EdgeAtom::Lambda edge_lambda; @@ -1646,14 +1647,19 @@ antlrcpp::Any CypherMainVisitor::visitRelationshipPattern(MemgraphCypher::Relati throw SemanticException( "Lambda for calculating weights is mandatory with weighted " "shortest path expansion."); + else if (edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) + throw SemanticException( + "Lambda for calculating weights is mandatory with all " + "shortest paths expansion."); // In variable expansion inner variables are mandatory. anonymous_identifiers.push_back(&edge->filter_lambda_.inner_edge); anonymous_identifiers.push_back(&edge->filter_lambda_.inner_node); break; case 1: - if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) { - // For wShortest, the first (and required) lambda is used for weight - // calculation. + if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || + edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) { + // For wShortest and allShortest, the first (and required) lambda is + // used for weight calculation. edge->weight_lambda_ = visit_lambda(relationshipLambdas[0]); visit_total_weight(); // Add mandatory inner variables for filter lambda. @@ -1665,7 +1671,7 @@ antlrcpp::Any CypherMainVisitor::visitRelationshipPattern(MemgraphCypher::Relati } break; case 2: - if (edge->type_ != EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) + if (edge->type_ != EdgeAtom::Type::WEIGHTED_SHORTEST_PATH && edge->type_ != EdgeAtom::Type::ALL_SHORTEST_PATHS) throw SemanticException("Only one filter lambda can be supplied."); edge->weight_lambda_ = visit_lambda(relationshipLambdas[0]); visit_total_weight(); @@ -1724,6 +1730,8 @@ antlrcpp::Any CypherMainVisitor::visitVariableExpansion(MemgraphCypher::Variable edge_type = EdgeAtom::Type::BREADTH_FIRST; else if (!ctx->getTokens(MemgraphCypher::WSHORTEST).empty()) edge_type = EdgeAtom::Type::WEIGHTED_SHORTEST_PATH; + else if (!ctx->getTokens(MemgraphCypher::ALLSHORTEST).empty()) + edge_type = EdgeAtom::Type::ALL_SHORTEST_PATHS; Expression *lower = nullptr; Expression *upper = nullptr; @@ -1734,7 +1742,8 @@ antlrcpp::Any CypherMainVisitor::visitVariableExpansion(MemgraphCypher::Variable auto *bound = std::any_cast(ctx->expression()[0]->accept(this)); if (!dots_tokens.size()) { // Case -[*bound]- - if (edge_type != EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) lower = bound; + if (edge_type != EdgeAtom::Type::WEIGHTED_SHORTEST_PATH && edge_type != EdgeAtom::Type::ALL_SHORTEST_PATHS) + lower = bound; upper = bound; } else if (dots_tokens[0]->getSourceInterval().startsAfter(ctx->expression()[0]->getSourceInterval())) { // Case -[*bound..]- @@ -1748,8 +1757,8 @@ antlrcpp::Any CypherMainVisitor::visitVariableExpansion(MemgraphCypher::Variable lower = std::any_cast(ctx->expression()[0]->accept(this)); upper = std::any_cast(ctx->expression()[1]->accept(this)); } - if (lower && edge_type == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) - throw SemanticException("Lower bound is not allowed in weighted shortest path expansion."); + if (lower && (edge_type == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || edge_type == EdgeAtom::Type::ALL_SHORTEST_PATHS)) + throw SemanticException("Lower bound is not allowed in weighted or all shortest path expansion."); return std::make_tuple(edge_type, lower, upper); } diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4 index 6ce84db1a..68f7b0908 100644 --- a/src/query/frontend/opencypher/grammar/Cypher.g4 +++ b/src/query/frontend/opencypher/grammar/Cypher.g4 @@ -172,7 +172,7 @@ relationshipDetail : '[' ( name=variable )? ( relationshipTypes )? ( variableExp relationshipLambda: '(' traversed_edge=variable ',' traversed_node=variable '|' expression ')'; -variableExpansion : '*' (BFS | WSHORTEST)? ( expression )? ( '..' ( expression )? )? ; +variableExpansion : '*' (BFS | WSHORTEST | ALLSHORTEST)? ( expression )? ( '..' ( expression )? )? ; properties : mapLiteral | parameter @@ -381,6 +381,7 @@ cypherKeyword : ALL | WHERE | WITH | WSHORTEST + | ALLSHORTEST | XOR | YIELD ; diff --git a/src/query/frontend/opencypher/grammar/CypherLexer.g4 b/src/query/frontend/opencypher/grammar/CypherLexer.g4 index 1377fbc82..abf9aee13 100644 --- a/src/query/frontend/opencypher/grammar/CypherLexer.g4 +++ b/src/query/frontend/opencypher/grammar/CypherLexer.g4 @@ -139,6 +139,7 @@ WHEN : W H E N ; WHERE : W H E R E ; WITH : W I T H ; WSHORTEST : W S H O R T E S T ; +ALLSHORTEST : A L L S H O R T E S T ; XOR : X O R ; YIELD : Y I E L D ; diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index a4ae9da66..a8c5e5b10 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -46,6 +46,7 @@ #include "utils/likely.hpp" #include "utils/logging.hpp" #include "utils/memory.hpp" +#include "utils/pmr/list.hpp" #include "utils/pmr/unordered_map.hpp" #include "utils/pmr/unordered_set.hpp" #include "utils/pmr/vector.hpp" @@ -787,9 +788,9 @@ ExpandVariable::ExpandVariable(const std::shared_ptr &input, Sy weight_lambda_(weight_lambda), total_weight_(total_weight) { DMG_ASSERT(type_ == EdgeAtom::Type::DEPTH_FIRST || type_ == EdgeAtom::Type::BREADTH_FIRST || - type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH, - "ExpandVariable can only be used with breadth first, depth first or " - "weighted shortest path type"); + type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS, + "ExpandVariable can only be used with breadth first, depth first, " + "weighted shortest path or all shortest paths type"); DMG_ASSERT(!(type_ == EdgeAtom::Type::BREADTH_FIRST && is_reverse), "Breadth first expansion can't be reversed"); } @@ -1429,6 +1430,28 @@ class SingleSourceShortestPathCursor : public query::plan::Cursor { utils::pmr::vector> to_visit_next_; }; +namespace { + +void CheckWeightType(TypedValue current_weight, utils::MemoryResource *memory) { + if (!current_weight.IsNumeric() && !current_weight.IsDuration()) { + throw QueryRuntimeException("Calculated weight must be numeric or a Duration, got {}.", current_weight.type()); + } + + const auto is_valid_numeric = [&] { + return current_weight.IsNumeric() && (current_weight >= TypedValue(0, memory)).ValueBool(); + }; + + const auto is_valid_duration = [&] { + return current_weight.IsDuration() && (current_weight >= TypedValue(utils::Duration(0), memory)).ValueBool(); + }; + + if (!is_valid_numeric() && !is_valid_duration()) { + throw QueryRuntimeException("Calculated weight must be non-negative!"); + } +} + +} // namespace + class ExpandWeightedShortestPathCursor : public query::plan::Cursor { public: ExpandWeightedShortestPathCursor(const ExpandVariable &self, utils::MemoryResource *mem) @@ -1466,21 +1489,7 @@ class ExpandWeightedShortestPathCursor : public query::plan::Cursor { TypedValue current_weight = self_.weight_lambda_->expression->Accept(evaluator); - if (!current_weight.IsNumeric() && !current_weight.IsDuration()) { - throw QueryRuntimeException("Calculated weight must be numeric or a Duration, got {}.", current_weight.type()); - } - - const auto is_valid_numeric = [&] { - return current_weight.IsNumeric() && (current_weight >= TypedValue(0, memory)).ValueBool(); - }; - - const auto is_valid_duration = [&] { - return current_weight.IsDuration() && (current_weight >= TypedValue(utils::Duration(0), memory)).ValueBool(); - }; - - if (!is_valid_numeric() && !is_valid_duration()) { - throw QueryRuntimeException("Calculated weight must be non-negative!"); - } + CheckWeightType(current_weight, memory); auto next_state = create_state(vertex, depth); @@ -1690,6 +1699,302 @@ class ExpandWeightedShortestPathCursor : public query::plan::Cursor { } }; +class ExpandAllShortestPathsCursor : public query::plan::Cursor { + public: + ExpandAllShortestPathsCursor(const ExpandVariable &self, utils::MemoryResource *mem) + : self_(self), + input_cursor_(self_.input_->MakeCursor(mem)), + visited_cost_(mem), + expanded_(mem), + next_edges_(mem), + traversal_stack_(mem), + pq_(mem) {} + + bool Pull(Frame &frame, ExecutionContext &context) override { + SCOPED_PROFILE_OP("ExpandAllShortestPathsCursor"); + + ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor, + storage::View::OLD); + + // For the given (edge, direction, weight, depth) tuple checks if they + // satisfy the "where" condition. if so, places them in the priority + // queue. + auto expand_vertex = [this, &evaluator, &frame](const EdgeAccessor &edge, const EdgeAtom::Direction direction, + const TypedValue &total_weight, int64_t depth) { + auto *memory = evaluator.GetMemoryResource(); + + auto const &next_vertex = direction == EdgeAtom::Direction::IN ? edge.From() : edge.To(); + + // If filter expression exists, evaluate filter + if (self_.filter_lambda_.expression) { + frame[self_.filter_lambda_.inner_edge_symbol] = edge; + frame[self_.filter_lambda_.inner_node_symbol] = next_vertex; + + if (!EvaluateFilter(evaluator, self_.filter_lambda_.expression)) return; + } + + // Evaluate current weight + frame[self_.weight_lambda_->inner_edge_symbol] = edge; + frame[self_.weight_lambda_->inner_node_symbol] = next_vertex; + + TypedValue current_weight = self_.weight_lambda_->expression->Accept(evaluator); + + CheckWeightType(current_weight, memory); + + TypedValue next_weight = std::invoke([&] { + if (total_weight.IsNull()) { + return current_weight; + } + + ValidateWeightTypes(current_weight, total_weight); + + return TypedValue(current_weight, memory) + total_weight; + }); + + auto found_it = visited_cost_.find(next_vertex); + // Check if the vertex has already been processed. + if (found_it != visited_cost_.end()) { + auto weight = found_it->second; + + if (weight.IsNull() || (next_weight <= weight).ValueBool()) { + // Has been visited, but now found a shorter path + visited_cost_[next_vertex] = next_weight; + } else { + // Continue and do not expand if current weight is larger + return; + } + } else { + visited_cost_[next_vertex] = next_weight; + } + + DirectedEdge directed_edge = {edge, direction, next_weight}; + pq_.push({next_weight, depth + 1, next_vertex, directed_edge}); + }; + + // Populates the priority queue structure with expansions + // from the given vertex. skips expansions that don't satisfy + // the "where" condition. + auto expand_from_vertex = [this, &expand_vertex](const VertexAccessor &vertex, const TypedValue &weight, + int64_t depth) { + if (self_.common_.direction != EdgeAtom::Direction::IN) { + auto out_edges = UnwrapEdgesResult(vertex.OutEdges(storage::View::OLD, self_.common_.edge_types)); + for (const auto &edge : out_edges) { + expand_vertex(edge, EdgeAtom::Direction::OUT, weight, depth); + } + } + if (self_.common_.direction != EdgeAtom::Direction::OUT) { + auto in_edges = UnwrapEdgesResult(vertex.InEdges(storage::View::OLD, self_.common_.edge_types)); + for (const auto &edge : in_edges) { + expand_vertex(edge, EdgeAtom::Direction::IN, weight, depth); + } + } + }; + + // 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(); + + // 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; + + 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. + 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_.common_.node_symbol] = next_vertex; + 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; + return true; + } + + // If priority queue is empty start new pulling stream. + if (pq_.empty()) { + // Finish if there is nothing to pull + if (!input_cursor_->Pull(frame, context)) return false; + + const auto &vertex_value = frame[self_.input_symbol_]; + if (vertex_value.IsNull()) continue; + + start_vertex = vertex_value.ValueVertex(); + if (self_.common_.existing_node) { + const auto &node = frame[self_.common_.node_symbol]; + // Due to optional matching the existing node could be null. + // Skip expansion for such nodes. + if (node.IsNull()) continue; + } + + // Clear existing data structures. + visited_cost_.clear(); + expanded_.clear(); + next_edges_.clear(); + traversal_stack_.clear(); + + pq_.push({TypedValue(), 0, *start_vertex, std::nullopt}); + visited_cost_.emplace(*start_vertex, 0); + frame[self_.common_.edge_symbol] = TypedValue::TVector(memory); + } + + // Create a DFS traversal tree from the start node + while (!pq_.empty()) { + if (MustAbort(context)) throw HintedAbortError(); + + auto [current_weight, current_depth, current_vertex, maybe_directed_edge] = pq_.top(); + pq_.pop(); + + // Expand only if what we've just expanded is less than max depth. + if (current_depth < upper_bound_) { + if (maybe_directed_edge) { + auto &[current_edge, direction, weight] = *maybe_directed_edge; + if (expanded_.find(current_edge) != expanded_.end()) continue; + expanded_.emplace(current_edge); + } + expand_from_vertex(current_vertex, current_weight, current_depth); + } + + // if current vertex is not starting vertex, maybe_directed_edge will not be nullopt + if (maybe_directed_edge) { + auto &[current_edge, direction, weight] = *maybe_directed_edge; + // 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(*maybe_directed_edge); + } + } + + 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)); + } + } + } + + void Shutdown() override { input_cursor_->Shutdown(); } + + void Reset() override { + input_cursor_->Reset(); + visited_cost_.clear(); + expanded_.clear(); + next_edges_.clear(); + traversal_stack_.clear(); + ClearQueue(); + } + + private: + const ExpandVariable &self_; + const UniqueCursorPtr input_cursor_; + + // Upper bound on the path length. + int64_t upper_bound_{-1}; + + struct AspStateHash { + size_t operator()(const std::pair &key) const { + return utils::HashCombine{}(key.first, key.second); + } + }; + + using DirectedEdge = std::tuple; + 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 the vertex with the potential expansion edge. + utils::pmr::unordered_map, AspStateHash> next_edges_; + // Stack indicating the traversal level. + utils::pmr::list> traversal_stack_; + + static void ValidateWeightTypes(const TypedValue &lhs, const TypedValue &rhs) { + if (!((lhs.IsNumeric() && lhs.IsNumeric()) || (rhs.IsDuration() && rhs.IsDuration()))) { + throw QueryRuntimeException(utils::MessageWithLink( + "All weights should be of the same type, either numeric or a Duration. Please update the weight " + "expression or the filter expression.", + "https://memgr.ph/wsp")); + } + } + + // Priority queue comparator. Keep lowest weight on top of the queue. + class PriorityQueueComparator { + public: + bool operator()(const std::tuple> &lhs, + const std::tuple> &rhs) { + const auto &lhs_weight = std::get<0>(lhs); + const auto &rhs_weight = std::get<0>(rhs); + // Null defines minimum value for all types + if (lhs_weight.IsNull()) { + return false; + } + + if (rhs_weight.IsNull()) { + return true; + } + + ValidateWeightTypes(lhs_weight, rhs_weight); + return (lhs_weight > rhs_weight).ValueBool(); + } + }; + + // Priority queue - core element of the algorithm. + // Stores: {weight, depth, next vertex, edge and direction} + std::priority_queue>, + utils::pmr::vector>>, + PriorityQueueComparator> + pq_; + + void ClearQueue() { + while (!pq_.empty()) pq_.pop(); + } +}; + UniqueCursorPtr ExpandVariable::MakeCursor(utils::MemoryResource *mem) const { EventCounter::IncrementCounter(EventCounter::ExpandVariableOperator); @@ -1704,6 +2009,8 @@ UniqueCursorPtr ExpandVariable::MakeCursor(utils::MemoryResource *mem) const { return MakeUniqueCursorPtr(mem, *this, mem); case EdgeAtom::Type::WEIGHTED_SHORTEST_PATH: return MakeUniqueCursorPtr(mem, *this, mem); + case EdgeAtom::Type::ALL_SHORTEST_PATHS: + return MakeUniqueCursorPtr(mem, *this, mem); case EdgeAtom::Type::SINGLE: LOG_FATAL("ExpandVariable should not be planned for a single expansion!"); } diff --git a/src/query/plan/operator.lcp b/src/query/plan/operator.lcp index 1a3b4b43e..d7b531cfc 100644 --- a/src/query/plan/operator.lcp +++ b/src/query/plan/operator.lcp @@ -1082,6 +1082,7 @@ pulled.") // that should be inaccessible (private class function won't compile) friend class ExpandVariableCursor; friend class ExpandWeightedShortestPathCursor; + friend class ExpandAllShortestPathCursor; cpp<#) (:serialize (:slk)) (:clone)) diff --git a/src/query/plan/preprocess.cpp b/src/query/plan/preprocess.cpp index cd0ebd965..5f90a7b85 100644 --- a/src/query/plan/preprocess.cpp +++ b/src/query/plan/preprocess.cpp @@ -65,7 +65,7 @@ std::vector NormalizePatterns(const SymbolTable &symbol_table, const // Remove symbols which are bound by lambda arguments. collector.symbols_.erase(symbol_table.at(*edge->filter_lambda_.inner_edge)); collector.symbols_.erase(symbol_table.at(*edge->filter_lambda_.inner_node)); - if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) { + if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) { collector.symbols_.erase(symbol_table.at(*edge->weight_lambda_.inner_edge)); collector.symbols_.erase(symbol_table.at(*edge->weight_lambda_.inner_node)); } diff --git a/src/query/plan/pretty_print.cpp b/src/query/plan/pretty_print.cpp index b44b8986c..17644068a 100644 --- a/src/query/plan/pretty_print.cpp +++ b/src/query/plan/pretty_print.cpp @@ -120,6 +120,9 @@ bool PlanPrinter::PreVisit(query::plan::ExpandVariable &op) { case Type::WEIGHTED_SHORTEST_PATH: *out_ << "WeightedShortestPath"; break; + case Type::ALL_SHORTEST_PATHS: + *out_ << "AllShortestPaths"; + break; case Type::SINGLE: LOG_FATAL("Unexpected ExpandVariable::type_"); } @@ -308,6 +311,8 @@ std::string ToString(EdgeAtom::Type type) { return "dfs"; case EdgeAtom::Type::WEIGHTED_SHORTEST_PATH: return "wsp"; + case EdgeAtom::Type::ALL_SHORTEST_PATHS: + return "asp"; case EdgeAtom::Type::SINGLE: return "single"; } @@ -548,7 +553,7 @@ bool PlanToJsonVisitor::PreVisit(ExpandVariable &op) { self["filter_lambda"] = op.filter_lambda_.expression ? ToJson(op.filter_lambda_.expression) : json(); - if (op.type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) { + if (op.type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || op.type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) { self["weight_lambda"] = ToJson(op.weight_lambda_->expression); self["total_weight_symbol"] = ToJson(*op.total_weight_); } diff --git a/src/query/plan/rule_based_planner.hpp b/src/query/plan/rule_based_planner.hpp index 8854392e6..563768ea0 100644 --- a/src/query/plan/rule_based_planner.hpp +++ b/src/query/plan/rule_based_planner.hpp @@ -418,7 +418,7 @@ class RuleBasedPlanner { std::optional weight_lambda; std::optional total_weight; - if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) { + if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) { weight_lambda.emplace(ExpansionLambda{symbol_table.at(*edge->weight_lambda_.inner_edge), symbol_table.at(*edge->weight_lambda_.inner_node), edge->weight_lambda_.expression}); diff --git a/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature b/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature new file mode 100644 index 000000000..8a8cd76cc --- /dev/null +++ b/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature @@ -0,0 +1,171 @@ +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 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 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 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 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 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 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 | diff --git a/tests/mgbench/datasets.py b/tests/mgbench/datasets.py index dbaaa2de9..c68fcac34 100644 --- a/tests/mgbench/datasets.py +++ b/tests/mgbench/datasets.py @@ -45,13 +45,10 @@ class Dataset: variant = self.DEFAULT_VARIANT if variant not in self.VARIANTS: raise ValueError("Invalid test variant!") - if (self.FILES and variant not in self.FILES) and \ - (self.URLS and variant not in self.URLS): - raise ValueError("The variant doesn't have a defined URL or " - "file path!") + if (self.FILES and variant not in self.FILES) and (self.URLS and variant not in self.URLS): + raise ValueError("The variant doesn't have a defined URL or " "file path!") if variant not in self.SIZES: - raise ValueError("The variant doesn't have a defined dataset " - "size!") + raise ValueError("The variant doesn't have a defined dataset " "size!") self._variant = variant if self.FILES is not None: self._file = self.FILES.get(variant, None) @@ -63,8 +60,7 @@ class Dataset: self._url = None self._size = self.SIZES[variant] if "vertices" not in self._size or "edges" not in self._size: - raise ValueError("The size defined for this variant doesn't " - "have the number of vertices and/or edges!") + raise ValueError("The size defined for this variant doesn't " "have the number of vertices and/or edges!") self._num_vertices = self._size["vertices"] self._num_edges = self._size["edges"] @@ -76,8 +72,7 @@ class Dataset: cached_input, exists = directory.get_file("dataset.cypher") if not exists: print("Downloading dataset file:", self._url) - downloaded_file = helpers.download_file( - self._url, directory.get_path()) + downloaded_file = helpers.download_file(self._url, directory.get_path()) print("Unpacking and caching file:", downloaded_file) helpers.unpack_and_move_file(downloaded_file, cached_input) print("Using cached dataset file:", cached_input) @@ -137,18 +132,20 @@ class Pokec(Dataset): # Arango benchmarks def benchmark__arango__single_vertex_read(self): - return ("MATCH (n:User {id : $id}) RETURN n", - {"id": self._get_random_vertex()}) + return ("MATCH (n:User {id : $id}) RETURN n", {"id": self._get_random_vertex()}) def benchmark__arango__single_vertex_write(self): - return ("CREATE (n:UserTemp {id : $id}) RETURN n", - {"id": random.randint(1, self._num_vertices * 10)}) + return ( + "CREATE (n:UserTemp {id : $id}) RETURN n", + {"id": random.randint(1, self._num_vertices * 10)}, + ) def benchmark__arango__single_edge_write(self): vertex_from, vertex_to = self._get_random_from_to() - return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " - "CREATE (n)-[e:Temp]->(m) RETURN e", - {"from": vertex_from, "to": vertex_to}) + return ( + "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " "CREATE (n)-[e:Temp]->(m) RETURN e", + {"from": vertex_from, "to": vertex_to}, + ) def benchmark__arango__aggregate(self): return ("MATCH (n:User) RETURN n.age, COUNT(*)", {}) @@ -157,92 +154,112 @@ class Pokec(Dataset): return ("MATCH (n:User) WHERE n.age >= 18 RETURN n.age, COUNT(*)", {}) def benchmark__arango__expansion_1(self): - return ("MATCH (s:User {id: $id})-->(n:User) " - "RETURN n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->(n:User) " "RETURN n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_1_with_filter(self): - return ("MATCH (s:User {id: $id})-->(n:User) " - "WHERE n.age >= 18 " - "RETURN n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->(n:User) " "WHERE n.age >= 18 " "RETURN n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_2(self): - return ("MATCH (s:User {id: $id})-->()-->(n:User) " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->(n:User) " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_2_with_filter(self): - return ("MATCH (s:User {id: $id})-->()-->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_3(self): - return ("MATCH (s:User {id: $id})-->()-->()-->(n:User) " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->()-->(n:User) " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_3_with_filter(self): - return ("MATCH (s:User {id: $id})-->()-->()-->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_4(self): - return ("MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_4_with_filter(self): - return ("MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__neighbours_2(self): - return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__neighbours_2_with_filter(self): - return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__neighbours_2_with_data(self): - return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " - "RETURN DISTINCT n.id, n", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "RETURN DISTINCT n.id, n", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__neighbours_2_with_data_and_filter(self): - return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id, n", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id, n", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__shortest_path(self): vertex_from, vertex_to = self._get_random_from_to() - return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " - "MATCH p=(n)-[*bfs..15]->(m) " - "RETURN extract(n in nodes(p) | n.id) AS path", - {"from": vertex_from, "to": vertex_to}) + return ( + "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " + "MATCH p=(n)-[*bfs..15]->(m) " + "RETURN extract(n in nodes(p) | n.id) AS path", + {"from": vertex_from, "to": vertex_to}, + ) def benchmark__arango__shortest_path_with_filter(self): vertex_from, vertex_to = self._get_random_from_to() - return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " - "MATCH p=(n)-[*bfs..15 (e, n | n.age >= 18)]->(m) " - "RETURN extract(n in nodes(p) | n.id) AS path", - {"from": vertex_from, "to": vertex_to}) + return ( + "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " + "MATCH p=(n)-[*bfs..15 (e, n | n.age >= 18)]->(m) " + "RETURN extract(n in nodes(p) | n.id) AS path", + {"from": vertex_from, "to": vertex_to}, + ) + + def benchmark__arango__allshortest_paths(self): + vertex_from, vertex_to = self._get_random_from_to() + return ( + "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " + "MATCH p=(n)-[*allshortest 2 (r, n | 1) total_weight]->(m) " + "RETURN extract(n in nodes(p) | n.id) AS path", + {"from": vertex_from, "to": vertex_to}, + ) # Our benchmark queries def benchmark__create__edge(self): vertex_from, vertex_to = self._get_random_from_to() - return ("MATCH (a:User {id: $from}), (b:User {id: $to}) " - "CREATE (a)-[:TempEdge]->(b)", - {"from": vertex_from, "to": vertex_to}) + return ( + "MATCH (a:User {id: $from}), (b:User {id: $to}) " "CREATE (a)-[:TempEdge]->(b)", + {"from": vertex_from, "to": vertex_to}, + ) def benchmark__create__pattern(self): return ("CREATE ()-[:TempEdge]->()", {}) @@ -251,9 +268,12 @@ class Pokec(Dataset): return ("CREATE ()", {}) def benchmark__create__vertex_big(self): - return ("CREATE (:L1:L2:L3:L4:L5:L6:L7 {p1: true, p2: 42, " - "p3: \"Here is some text that is not extremely short\", " - "p4:\"Short text\", p5: 234.434, p6: 11.11, p7: false})", {}) + return ( + "CREATE (:L1:L2:L3:L4:L5:L6:L7 {p1: true, p2: 42, " + 'p3: "Here is some text that is not extremely short", ' + 'p4:"Short text", p5: 234.434, p6: 11.11, p7: false})', + {}, + ) def benchmark__aggregation__count(self): return ("MATCH (n) RETURN count(n), count(n.age)", {}) @@ -262,29 +282,31 @@ class Pokec(Dataset): return ("MATCH (n) RETURN min(n.age), max(n.age), avg(n.age)", {}) def benchmark__match__pattern_cycle(self): - return ("MATCH (n:User {id: $id})-[e1]->(m)-[e2]->(n) " - "RETURN e1, m, e2", - {"id": self._get_random_vertex()}) + return ( + "MATCH (n:User {id: $id})-[e1]->(m)-[e2]->(n) " "RETURN e1, m, e2", + {"id": self._get_random_vertex()}, + ) def benchmark__match__pattern_long(self): - return ("MATCH (n1:User {id: $id})-[e1]->(n2)-[e2]->" - "(n3)-[e3]->(n4)<-[e4]-(n5) " - "RETURN n5 LIMIT 1", - {"id": self._get_random_vertex()}) + return ( + "MATCH (n1:User {id: $id})-[e1]->(n2)-[e2]->" "(n3)-[e3]->(n4)<-[e4]-(n5) " "RETURN n5 LIMIT 1", + {"id": self._get_random_vertex()}, + ) def benchmark__match__pattern_short(self): - return ("MATCH (n:User {id: $id})-[e]->(m) " - "RETURN m LIMIT 1", - {"id": self._get_random_vertex()}) + return ( + "MATCH (n:User {id: $id})-[e]->(m) " "RETURN m LIMIT 1", + {"id": self._get_random_vertex()}, + ) def benchmark__match__vertex_on_label_property(self): - return ("MATCH (n:User) WITH n WHERE n.id = $id RETURN n", - {"id": self._get_random_vertex()}) + return ( + "MATCH (n:User) WITH n WHERE n.id = $id RETURN n", + {"id": self._get_random_vertex()}, + ) def benchmark__match__vertex_on_label_property_index(self): - return ("MATCH (n:User {id: $id}) RETURN n", - {"id": self._get_random_vertex()}) + return ("MATCH (n:User {id: $id}) RETURN n", {"id": self._get_random_vertex()}) def benchmark__match__vertex_on_property(self): - return ("MATCH (n {id: $id}) RETURN n", - {"id": self._get_random_vertex()}) + return ("MATCH (n {id: $id}) RETURN n", {"id": self._get_random_vertex()}) diff --git a/tests/mgbench/runners.py b/tests/mgbench/runners.py index 891a7cddd..b6b727ed8 100644 --- a/tests/mgbench/runners.py +++ b/tests/mgbench/runners.py @@ -40,8 +40,7 @@ def _convert_args_to_flags(*args, **kwargs): def _get_usage(pid): total_cpu = 0 with open("/proc/{}/stat".format(pid)) as f: - total_cpu = (sum(map(int, f.read().split(")")[1].split()[11:15])) / - os.sysconf(os.sysconf_names["SC_CLK_TCK"])) + total_cpu = sum(map(int, f.read().split(")")[1].split()[11:15])) / os.sysconf(os.sysconf_names["SC_CLK_TCK"]) peak_rss = 0 with open("/proc/{}/status".format(pid)) as f: for row in f: @@ -60,10 +59,8 @@ class Memgraph: atexit.register(self._cleanup) # Determine Memgraph version - ret = subprocess.run([memgraph_binary, "--version"], - stdout=subprocess.PIPE, check=True) - version = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", - ret.stdout.decode("utf-8")).group(0) + ret = subprocess.run([memgraph_binary, "--version"], stdout=subprocess.PIPE, check=True) + version = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", ret.stdout.decode("utf-8")).group(0) self._memgraph_version = tuple(map(int, version.split("."))) def __del__(self): @@ -79,8 +76,7 @@ class Memgraph: if self._memgraph_version >= (0, 50, 0): kwargs["storage_properties_on_edges"] = self._properties_on_edges else: - assert self._properties_on_edges, \ - "Older versions of Memgraph can't disable properties on edges!" + assert self._properties_on_edges, "Older versions of Memgraph can't disable properties on edges!" return _convert_args_to_flags(self._memgraph_binary, **kwargs) def _start(self, **kwargs): @@ -94,8 +90,7 @@ class Memgraph: raise Exception("The database process died prematurely!") wait_for_server(7687) ret = self._proc_mg.poll() - assert ret is None, "The database process died prematurely " \ - "({})!".format(ret) + assert ret is None, "The database process died prematurely " "({})!".format(ret) def _cleanup(self): if self._proc_mg is None: @@ -121,8 +116,7 @@ class Memgraph: def stop(self): ret, usage = self._cleanup() - assert ret == 0, "The database process exited with a non-zero " \ - "status ({})!".format(ret) + assert ret == 0, "The database process exited with a non-zero " "status ({})!".format(ret) return usage @@ -135,8 +129,7 @@ class Client: return _convert_args_to_flags(self._client_binary, **kwargs) def execute(self, queries=None, file_path=None, num_workers=1): - if (queries is None and file_path is None) or \ - (queries is not None and file_path is not None): + if (queries is None and file_path is None) or (queries is not None and file_path is not None): raise ValueError("Either queries or input_path must be specified!") # TODO: check `file_path.endswith(".json")` to support advanced @@ -151,8 +144,8 @@ class Client: json.dump(query, f) f.write("\n") - args = self._get_args(input=file_path, num_workers=num_workers, - queries_json=queries_json) + args = self._get_args(input=file_path, num_workers=num_workers, queries_json=queries_json) ret = subprocess.run(args, stdout=subprocess.PIPE, check=True) data = ret.stdout.decode("utf-8").strip().split("\n") + data = [x for x in data if not x.startswith("[")] return list(map(json.loads, data)) diff --git a/tests/unit/query_plan_match_filter_return.cpp b/tests/unit/query_plan_match_filter_return.cpp index 5d885b1bb..87af0771a 100644 --- a/tests/unit/query_plan_match_filter_return.cpp +++ b/tests/unit/query_plan_match_filter_return.cpp @@ -1221,6 +1221,359 @@ TEST_F(QueryPlanExpandWeightedShortestPath, NegativeUpperBound) { EXPECT_THROW(ExpandWShortest(EdgeAtom::Direction::BOTH, -1, LITERAL(true)), QueryRuntimeException); } +/** A test fixture for all shortest paths expansion */ +class QueryPlanExpandAllShortestPaths : public testing::Test { + public: + struct ResultType { + std::vector path; + memgraph::query::VertexAccessor vertex; + double total_weight; + }; + + protected: + memgraph::storage::Storage db; + memgraph::storage::Storage::Accessor storage_dba{db.Access()}; + memgraph::query::DbAccessor dba{&storage_dba}; + std::pair prop = PROPERTY_PAIR("property"); + memgraph::storage::EdgeTypeId edge_type = dba.NameToEdgeType("edge_type"); + + // make 5 vertices because we'll need to compare against them exactly + // v[0] has `prop` with the value 0 + std::vector v; + + // make some edges too, in a map (from, to) vertex indices + std::unordered_map, memgraph::query::EdgeAccessor> e; + + AstStorage storage; + SymbolTable symbol_table; + + // inner edge and vertex symbols + Symbol filter_edge = symbol_table.CreateSymbol("f_edge", true); + Symbol filter_node = symbol_table.CreateSymbol("f_node", true); + + Symbol weight_edge = symbol_table.CreateSymbol("w_edge", true); + Symbol weight_node = symbol_table.CreateSymbol("w_node", true); + + Symbol total_weight = symbol_table.CreateSymbol("total_weight", true); + + void SetUp() { + for (int i = 0; i < 5; i++) { + v.push_back(dba.InsertVertex()); + ASSERT_TRUE(v.back().SetProperty(prop.second, memgraph::storage::PropertyValue(i)).HasValue()); + } + + auto add_edge = [&](int from, int to, double weight) { + auto edge = dba.InsertEdge(&v[from], &v[to], edge_type); + ASSERT_TRUE(edge->SetProperty(prop.second, memgraph::storage::PropertyValue(weight)).HasValue()); + e.emplace(std::make_pair(from, to), *edge); + }; + + add_edge(0, 1, 5); + add_edge(1, 4, 5); + add_edge(0, 2, 3); + add_edge(2, 3, 3); + add_edge(3, 4, 3); + add_edge(4, 0, 12); + + dba.AdvanceCommand(); + } + + // defines and performs an all shortest paths expansion with the given + // params returns a vector of pairs. each pair is (vector-of-edges, + // vertex) + auto ExpandAllShortest(EdgeAtom::Direction direction, std::optional max_depth, Expression *where, + std::optional node_id = 0, ScanAllTuple *existing_node_input = nullptr) { + // scan the nodes optionally filtering on property value + auto n = MakeScanAll(storage, symbol_table, "n", existing_node_input ? existing_node_input->op_ : nullptr); + auto last_op = n.op_; + if (node_id) { + last_op = std::make_shared(last_op, EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id))); + } + + auto ident_e = IDENT("e"); + ident_e->MapTo(weight_edge); + + // expand allshortest + auto node_sym = existing_node_input ? existing_node_input->sym_ : symbol_table.CreateSymbol("node", true); + auto edge_list_sym = symbol_table.CreateSymbol("edgelist_", true); + auto filter_lambda = last_op = std::make_shared( + last_op, n.sym_, node_sym, edge_list_sym, EdgeAtom::Type::ALL_SHORTEST_PATHS, direction, + std::vector{}, false, nullptr, max_depth ? LITERAL(max_depth.value()) : nullptr, + existing_node_input != nullptr, ExpansionLambda{filter_edge, filter_node, where}, + ExpansionLambda{weight_edge, weight_node, PROPERTY_LOOKUP(ident_e, prop)}, total_weight); + + Frame frame(symbol_table.max_position()); + auto cursor = last_op->MakeCursor(memgraph::utils::NewDeleteResource()); + std::vector results; + auto context = MakeContext(storage, symbol_table, &dba); + while (cursor->Pull(frame, context)) { + results.push_back(ResultType{std::vector(), frame[node_sym].ValueVertex(), + frame[total_weight].ValueDouble()}); + for (const TypedValue &edge : frame[edge_list_sym].ValueList()) + results.back().path.emplace_back(edge.ValueEdge()); + } + + return results; + } + + template + auto GetProp(const TAccessor &accessor) { + return accessor.GetProperty(memgraph::storage::View::OLD, prop.second)->ValueInt(); + } + + template + auto GetDoubleProp(const TAccessor &accessor) { + return accessor.GetProperty(memgraph::storage::View::OLD, prop.second)->ValueDouble(); + } + + Expression *PropNe(Symbol symbol, int value) { + auto ident = IDENT("inner_element"); + ident->MapTo(symbol); + return NEQ(PROPERTY_LOOKUP(ident, prop), LITERAL(value)); + } +}; + +bool compareResultType(const QueryPlanExpandAllShortestPaths::ResultType &a, + const QueryPlanExpandAllShortestPaths::ResultType &b) { + return a.total_weight < b.total_weight; +} + +// Testing all shortest paths on this graph: +// +// 5 5 +// /-->--[1]-->--\ +// / \ +// / 12 \ 2 +// [0]--------<--------[4]------->-------[5] +// \ / (on some tests only) +// \ / +// \->[2]->-[3]->/ +// 3 3 3 + +TEST_F(QueryPlanExpandAllShortestPaths, Basic) { + auto results = ExpandAllShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true)); + sort(results.begin(), results.end(), compareResultType); + + ASSERT_EQ(results.size(), 4); + + // check end nodes + EXPECT_EQ(GetProp(results[0].vertex), 2); + EXPECT_EQ(GetProp(results[1].vertex), 1); + EXPECT_EQ(GetProp(results[2].vertex), 3); + EXPECT_EQ(GetProp(results[3].vertex), 4); + + // check paths and total weights + EXPECT_EQ(results[0].path.size(), 1); + EXPECT_EQ(GetDoubleProp(results[0].path[0]), 3); + EXPECT_EQ(results[0].total_weight, 3); + + EXPECT_EQ(results[1].path.size(), 1); + EXPECT_EQ(GetDoubleProp(results[1].path[0]), 5); + EXPECT_EQ(results[1].total_weight, 5); + + EXPECT_EQ(results[2].path.size(), 2); + EXPECT_EQ(GetDoubleProp(results[2].path[0]), 3); + EXPECT_EQ(GetDoubleProp(results[2].path[1]), 3); + EXPECT_EQ(results[2].total_weight, 6); + + EXPECT_EQ(results[3].path.size(), 3); + EXPECT_EQ(GetDoubleProp(results[3].path[0]), 3); + EXPECT_EQ(GetDoubleProp(results[3].path[1]), 3); + EXPECT_EQ(GetDoubleProp(results[3].path[2]), 3); + EXPECT_EQ(results[3].total_weight, 9); +} + +TEST_F(QueryPlanExpandAllShortestPaths, EdgeDirection) { + { + auto results = ExpandAllShortest(EdgeAtom::Direction::OUT, 1000, LITERAL(true)); + sort(results.begin(), results.end(), compareResultType); + ASSERT_EQ(results.size(), 4); + EXPECT_EQ(GetProp(results[0].vertex), 2); + EXPECT_EQ(results[0].total_weight, 3); + EXPECT_EQ(GetProp(results[1].vertex), 1); + EXPECT_EQ(results[1].total_weight, 5); + EXPECT_EQ(GetProp(results[2].vertex), 3); + EXPECT_EQ(results[2].total_weight, 6); + EXPECT_EQ(GetProp(results[3].vertex), 4); + EXPECT_EQ(results[3].total_weight, 9); + } + { + auto results = ExpandAllShortest(EdgeAtom::Direction::IN, 1000, LITERAL(true)); + sort(results.begin(), results.end(), compareResultType); + ASSERT_EQ(results.size(), 4); + EXPECT_EQ(GetProp(results[0].vertex), 4); + EXPECT_EQ(results[0].total_weight, 12); + EXPECT_EQ(GetProp(results[1].vertex), 3); + EXPECT_EQ(results[1].total_weight, 15); + EXPECT_EQ(GetProp(results[2].vertex), 1); + EXPECT_EQ(results[2].total_weight, 17); + EXPECT_EQ(GetProp(results[3].vertex), 2); + EXPECT_EQ(results[3].total_weight, 18); + } +} + +TEST_F(QueryPlanExpandAllShortestPaths, Where) { + { + auto results = ExpandAllShortest(EdgeAtom::Direction::BOTH, 1000, PropNe(filter_node, 2)); + ASSERT_EQ(results.size(), 3); + EXPECT_EQ(GetProp(results[0].vertex), 1); + EXPECT_EQ(results[0].total_weight, 5); + EXPECT_EQ(GetProp(results[1].vertex), 4); + EXPECT_EQ(results[1].total_weight, 10); + EXPECT_EQ(GetProp(results[2].vertex), 3); + EXPECT_EQ(results[2].total_weight, 13); + } + { + auto results = ExpandAllShortest(EdgeAtom::Direction::BOTH, 1000, PropNe(filter_node, 1)); + ASSERT_EQ(results.size(), 3); + EXPECT_EQ(GetProp(results[0].vertex), 2); + EXPECT_EQ(results[0].total_weight, 3); + EXPECT_EQ(GetProp(results[1].vertex), 3); + EXPECT_EQ(results[1].total_weight, 6); + EXPECT_EQ(GetProp(results[2].vertex), 4); + EXPECT_EQ(results[2].total_weight, 9); + } +} + +TEST_F(QueryPlanExpandAllShortestPaths, UpperBound) { + { + auto results = ExpandAllShortest(EdgeAtom::Direction::BOTH, std::nullopt, LITERAL(true)); + std::sort(results.begin(), results.end(), compareResultType); + ASSERT_EQ(results.size(), 4); + EXPECT_EQ(GetProp(results[0].vertex), 2); + EXPECT_EQ(results[0].total_weight, 3); + EXPECT_EQ(GetProp(results[1].vertex), 1); + EXPECT_EQ(results[1].total_weight, 5); + EXPECT_EQ(GetProp(results[2].vertex), 3); + EXPECT_EQ(results[2].total_weight, 6); + EXPECT_EQ(GetProp(results[3].vertex), 4); + EXPECT_EQ(results[3].total_weight, 9); + } + { + auto results = ExpandAllShortest(EdgeAtom::Direction::BOTH, 2, LITERAL(true)); + std::sort(results.begin(), results.end(), compareResultType); + ASSERT_EQ(results.size(), 4); + EXPECT_EQ(GetProp(results[0].vertex), 2); + EXPECT_EQ(results[0].total_weight, 3); + EXPECT_EQ(GetProp(results[1].vertex), 1); + EXPECT_EQ(results[1].total_weight, 5); + EXPECT_EQ(GetProp(results[2].vertex), 3); + EXPECT_EQ(results[2].total_weight, 6); + EXPECT_EQ(GetProp(results[3].vertex), 4); + EXPECT_EQ(results[3].total_weight, 10); + } + { + auto results = ExpandAllShortest(EdgeAtom::Direction::BOTH, 1, LITERAL(true)); + std::sort(results.begin(), results.end(), compareResultType); + ASSERT_EQ(results.size(), 3); + EXPECT_EQ(GetProp(results[0].vertex), 2); + EXPECT_EQ(results[0].total_weight, 3); + EXPECT_EQ(GetProp(results[1].vertex), 1); + EXPECT_EQ(results[1].total_weight, 5); + EXPECT_EQ(GetProp(results[2].vertex), 4); + EXPECT_EQ(results[2].total_weight, 12); + } + { + auto new_vertex = dba.InsertVertex(); + ASSERT_TRUE(new_vertex.SetProperty(prop.second, memgraph::storage::PropertyValue(5)).HasValue()); + auto edge = dba.InsertEdge(&v[4], &new_vertex, edge_type); + ASSERT_TRUE(edge.HasValue()); + ASSERT_TRUE(edge->SetProperty(prop.second, memgraph::storage::PropertyValue(2)).HasValue()); + dba.AdvanceCommand(); + + auto results = ExpandAllShortest(EdgeAtom::Direction::BOTH, 3, LITERAL(true)); + std::sort(results.begin(), results.end(), compareResultType); + ASSERT_EQ(results.size(), 5); + EXPECT_EQ(GetProp(results[0].vertex), 2); + EXPECT_EQ(results[0].total_weight, 3); + EXPECT_EQ(GetProp(results[1].vertex), 1); + EXPECT_EQ(results[1].total_weight, 5); + EXPECT_EQ(GetProp(results[2].vertex), 3); + EXPECT_EQ(results[2].total_weight, 6); + EXPECT_EQ(GetProp(results[3].vertex), 4); + EXPECT_EQ(results[3].total_weight, 9); + EXPECT_EQ(GetProp(results[4].vertex), 5); + EXPECT_EQ(results[4].total_weight, 12); + } +} + +TEST_F(QueryPlanExpandAllShortestPaths, NonNumericWeight) { + auto new_vertex = dba.InsertVertex(); + ASSERT_TRUE(new_vertex.SetProperty(prop.second, memgraph::storage::PropertyValue(5)).HasValue()); + auto edge = dba.InsertEdge(&v[4], &new_vertex, edge_type); + ASSERT_TRUE(edge.HasValue()); + ASSERT_TRUE(edge->SetProperty(prop.second, memgraph::storage::PropertyValue("not a number")).HasValue()); + dba.AdvanceCommand(); + EXPECT_THROW(ExpandAllShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true)), QueryRuntimeException); +} + +TEST_F(QueryPlanExpandAllShortestPaths, NegativeWeight) { + auto new_vertex = dba.InsertVertex(); + ASSERT_TRUE(new_vertex.SetProperty(prop.second, memgraph::storage::PropertyValue(5)).HasValue()); + auto edge = dba.InsertEdge(&v[4], &new_vertex, edge_type); + ASSERT_TRUE(edge.HasValue()); + ASSERT_TRUE(edge->SetProperty(prop.second, memgraph::storage::PropertyValue(-10)).HasValue()); // negative weight + dba.AdvanceCommand(); + EXPECT_THROW(ExpandAllShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true)), QueryRuntimeException); +} + +TEST_F(QueryPlanExpandAllShortestPaths, NegativeUpperBound) { + EXPECT_THROW(ExpandAllShortest(EdgeAtom::Direction::BOTH, -1, LITERAL(true)), QueryRuntimeException); +} + +// MultiplePaths testing on this graph: +// 5 5 +// [0]-->--[1]--->---[6] +// | \ / +// \/ 3 5 >-[4]-> +// | / 1 +// [2]-->--[3]-> +// 3 3 + +TEST_F(QueryPlanExpandAllShortestPaths, MultiplePaths) { + auto new_vertex = dba.InsertVertex(); + ASSERT_TRUE(new_vertex.SetProperty(prop.second, memgraph::storage::PropertyValue(6)).HasValue()); + + auto edge = dba.InsertEdge(&v[4], &new_vertex, edge_type); + ASSERT_TRUE(edge.HasValue()); + ASSERT_TRUE(edge->SetProperty(prop.second, memgraph::storage::PropertyValue(1)).HasValue()); + dba.AdvanceCommand(); + + auto edge2 = dba.InsertEdge(&v[1], &new_vertex, edge_type); + ASSERT_TRUE(edge2.HasValue()); + ASSERT_TRUE(edge2->SetProperty(prop.second, memgraph::storage::PropertyValue(5)).HasValue()); + dba.AdvanceCommand(); + + auto results = ExpandAllShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true)); + std::sort(results.begin(), results.end(), compareResultType); + ASSERT_EQ(results.size(), 6); + EXPECT_EQ(GetProp(results[4].vertex), 6); + EXPECT_EQ(results[4].total_weight, 10); + EXPECT_EQ(GetProp(results[5].vertex), 6); + EXPECT_EQ(results[5].total_weight, 10); +} + +// Uses graph from Basic test, with double edge 2->-3 and 3->-4 +TEST_F(QueryPlanExpandAllShortestPaths, MultiEdge) { + auto edge = dba.InsertEdge(&v[2], &v[3], edge_type); + ASSERT_TRUE(edge.HasValue()); + ASSERT_TRUE(edge->SetProperty(prop.second, memgraph::storage::PropertyValue(3)).HasValue()); + dba.AdvanceCommand(); + + auto edge2 = dba.InsertEdge(&v[3], &v[4], edge_type); + ASSERT_TRUE(edge2.HasValue()); + ASSERT_TRUE(edge2->SetProperty(prop.second, memgraph::storage::PropertyValue(3)).HasValue()); + dba.AdvanceCommand(); + + auto results = ExpandAllShortest(EdgeAtom::Direction::OUT, 1000, LITERAL(true)); + std::sort(results.begin(), results.end(), compareResultType); + ASSERT_EQ(results.size(), 8); + EXPECT_EQ(GetProp(results[6].vertex), 4); + EXPECT_EQ(results[4].total_weight, 9); + EXPECT_EQ(GetProp(results[7].vertex), 4); + EXPECT_EQ(results[5].total_weight, 9); +} + TEST(QueryPlan, ExpandOptional) { memgraph::storage::Storage db; auto storage_dba = db.Access();