diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index bb09ece2d..92a45bbc5 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -157,7 +157,7 @@ add_subdirectory(path_module) add_subdirectory(node_module) add_subdirectory(neighbors_module) add_subdirectory(refactor_module) -add_subdirectory(schema_module) add_subdirectory(algo_module) + add_cugraph_subdirectory(cugraph_module) diff --git a/cpp/algo_module/algorithm/algo.hpp b/cpp/algo_module/algorithm/algo.hpp index 53ed67680..c65e99104 100644 --- a/cpp/algo_module/algorithm/algo.hpp +++ b/cpp/algo_module/algorithm/algo.hpp @@ -40,8 +40,6 @@ constexpr const std::string_view kAllSimplePathsArg4 = "max_length"; constexpr const std::string_view kResultAllSimplePaths = "path"; /* cover constants */ -constexpr std::string_view kProcedureCover = "cover"; -constexpr std::string_view kCoverArg1 = "nodes"; constexpr std::string_view kCoverRet1 = "rel"; void AllSimplePaths(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); diff --git a/cpp/map_module/algorithm/map.cpp b/cpp/map_module/algorithm/map.cpp index 328978bdd..f232b10ca 100644 --- a/cpp/map_module/algorithm/map.cpp +++ b/cpp/map_module/algorithm/map.cpp @@ -181,9 +181,8 @@ void Map::Merge(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp const auto map2 = arguments[1].ValueMap(); mgp::Map merged_map = mgp::Map(std::move(map2)); - for (const auto element : map1) { - if (merged_map.At(element.key).IsNull()) { + if (!merged_map.KeyExists(element.key)) { merged_map.Insert(element.key, element.value); } } diff --git a/cpp/memgraph b/cpp/memgraph index b719f0744..2bfa148db 160000 --- a/cpp/memgraph +++ b/cpp/memgraph @@ -1 +1 @@ -Subproject commit b719f0744f38d63f23521593cf392461311ca790 +Subproject commit 2bfa148db2daa410175087b2518d8154ce132ce9 diff --git a/cpp/node_module/algorithm/node.cpp b/cpp/node_module/algorithm/node.cpp index 3187d2c9d..67ad3332f 100644 --- a/cpp/node_module/algorithm/node.cpp +++ b/cpp/node_module/algorithm/node.cpp @@ -177,3 +177,55 @@ void Node::RelationshipTypes(mgp_list *args, mgp_graph *memgraph_graph, mgp_resu return; } } + +void Node::DegreeIn(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory) { + mgp::MemoryDispatcherGuard guard{memory}; + const auto arguments = mgp::List(args); + auto result = mgp::Result(res); + try { + const auto node = arguments[0].ValueNode(); + const auto type = arguments[1].ValueString(); + if (type.size() == 0) { + result.SetValue((int64_t)node.InDegree()); + return; + } + int64_t degree = 0; + for (const auto rel : node.InRelationships()) { + if (rel.Type() == type) { + degree += 1; + } + } + result.SetValue(degree); + + + } catch (const std::exception &e) { + result.SetErrorMessage(e.what()); + return; + } +} + +void Node::DegreeOut(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory) { + mgp::MemoryDispatcherGuard guard{memory}; + const auto arguments = mgp::List(args); + auto result = mgp::Result(res); + try { + const auto node = arguments[0].ValueNode(); + const auto type = arguments[1].ValueString(); + if (type.size() == 0) { + result.SetValue((int64_t)node.OutDegree()); + return; + } + int64_t degree = 0; + for (const auto rel : node.OutRelationships()) { + if (rel.Type() == type) { + degree += 1; + } + } + result.SetValue(degree); + + + } catch (const std::exception &e) { + result.SetErrorMessage(e.what()); + return; + } +} diff --git a/cpp/node_module/algorithm/node.hpp b/cpp/node_module/algorithm/node.hpp index 1f9baaa8a..c03b4b00e 100644 --- a/cpp/node_module/algorithm/node.hpp +++ b/cpp/node_module/algorithm/node.hpp @@ -25,6 +25,16 @@ constexpr std::string_view kRelationshipTypesArg1 = "node"; constexpr std::string_view kRelationshipTypesArg2 = "types"; constexpr std::string_view kResultRelationshipTypes = "relationship_types"; +/* degree_in constants */ +constexpr std::string_view kFunctionDegreeIn = "degree_in"; +constexpr std::string_view kDegreeInArg1 = "node"; +constexpr std::string_view kDegreeInArg2 = "type"; + +/* degree_out constants */ +constexpr std::string_view kFunctionDegreeOut = "degree_out"; +constexpr std::string_view kDegreeOutArg1 = "node"; +constexpr std::string_view kDegreeOutArg2 = "type"; + void RelationshipsExist(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); bool RelationshipExist(const mgp::Node &node, std::string &rel_type); @@ -35,4 +45,8 @@ void RelationshipExists(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *r void RelationshipTypes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); +void DegreeIn(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory); + +void DegreeOut(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory); + } // namespace Node diff --git a/cpp/node_module/node_module.cpp b/cpp/node_module/node_module.cpp index 35fa39b4a..3ca023707 100644 --- a/cpp/node_module/node_module.cpp +++ b/cpp/node_module/node_module.cpp @@ -4,7 +4,9 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) { try { - mgp::MemoryDispatcherGuard guard{memory};; + + mgp::MemoryDispatcherGuard guard{memory}; + AddProcedure(Node::RelationshipsExist, std::string(Node::kProcedureRelationshipsExist).c_str(), mgp::ProcedureType::Read, {mgp::Parameter(std::string(Node::kArgumentNodesRelationshipsExist).c_str(), mgp::Type::Node), @@ -25,6 +27,16 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *mem mgp::Parameter(Node::kRelationshipTypesArg2, {mgp::Type::List, mgp::Type::String}, mgp::Value(mgp::List{}))}, {mgp::Return(Node::kResultRelationshipTypes, {mgp::Type::List, mgp::Type::String})}, module, memory); + mgp::AddFunction(Node::DegreeIn, Node::kFunctionDegreeIn, + {mgp::Parameter(Node::kDegreeInArg1, mgp::Type::Node), + mgp::Parameter(Node::kDegreeInArg2, mgp::Type::String, "")}, + module, memory); + + mgp::AddFunction(Node::DegreeOut, Node::kFunctionDegreeOut, + {mgp::Parameter(Node::kDegreeOutArg1, mgp::Type::Node), + mgp::Parameter(Node::kDegreeOutArg2, mgp::Type::String, "")}, + module, memory); + } catch (const std::exception &e) { return 1; } diff --git a/cpp/path_module/algorithm/path.cpp b/cpp/path_module/algorithm/path.cpp index c5a1371c8..a1af9e87d 100644 --- a/cpp/path_module/algorithm/path.cpp +++ b/cpp/path_module/algorithm/path.cpp @@ -1,11 +1,265 @@ #include "path.hpp" -#include - #include "mgp.hpp" +Path::PathHelper::PathHelper(const mgp::List &labels, const mgp::List &relationships, int64_t min_hops, + int64_t max_hops) { + ParseLabels(labels); + FilterLabelBoolStatus(); + ParseRelationships(relationships); + config_.min_hops = min_hops; + config_.max_hops = max_hops; +} + +Path::PathHelper::PathHelper(const mgp::Map &config) { + auto same_type_or_null = [](const mgp::Type type, const mgp::Type wanted_type) { + return type == wanted_type || type == mgp::Type::Null; + }; + + if (!same_type_or_null(config.At("minHops").Type(), mgp::Type::Int) || + !same_type_or_null(config.At("maxHops").Type(), mgp::Type::Int) || + !same_type_or_null(config.At("relationshipFilter").Type(), mgp::Type::List) || + !same_type_or_null(config.At("labelFilter").Type(), mgp::Type::List) || + !same_type_or_null(config.At("filterStartNode").Type(), mgp::Type::Bool) || + !same_type_or_null(config.At("beginSequenceAtStart").Type(), mgp::Type::Bool) || + !same_type_or_null(config.At("bfs").Type(), mgp::Type::Bool)) { + throw mgp::ValueException( + "The config parameter needs to be a map with keys and values in line with the documentation."); + } + + auto value = config.At("maxHops"); + if (!value.IsNull()) { + config_.max_hops = value.ValueInt(); + } + value = config.At("minHops"); + if (!value.IsNull()) { + config_.min_hops = value.ValueInt(); + } + + value = config.At("relationshipFilter"); + if (!value.IsNull()) { + ParseRelationships(value.ValueList()); + } else { + ParseRelationships(mgp::List()); + } + + value = config.At("labelFilter"); + if (!value.IsNull()) { + ParseLabels(value.ValueList()); + } else { + ParseLabels(mgp::List()); + } + FilterLabelBoolStatus(); + + value = config.At("filterStartNode"); + config_.filter_start_node = value.IsNull() ? true : value.ValueBool(); + + value = config.At("beginSequenceAtStart"); + config_.begin_sequence_at_start = value.IsNull() ? true : value.ValueBool(); + + value = config.At("bfs"); + config_.bfs = value.IsNull() ? false : value.ValueBool(); +} + +Path::RelDirection Path::PathHelper::GetDirection(const std::string &rel_type) const { + auto it = config_.relationship_sets.find(rel_type); + if (it == config_.relationship_sets.end()) { + return RelDirection::kNone; + } + return it->second; +} + +Path::LabelBools Path::PathHelper::GetLabelBools(const mgp::Node &node) const { + LabelBools label_bools; + for (const auto &label : node.Labels()) { + FilterLabel(label, label_bools); + } + return label_bools; +} + +bool Path::PathHelper::AreLabelsValid(const LabelBools &label_bools) const { + return !label_bools.blacklisted && + ((label_bools.end_node && config_.label_bools_status.end_node_activated) || label_bools.terminated || + (!config_.label_bools_status.termination_activated && !config_.label_bools_status.end_node_activated && + Whitelisted(label_bools.whitelisted))); +} + +bool Path::PathHelper::ContinueExpanding(const LabelBools &label_bools, size_t path_size) const { + return (static_cast(path_size) <= config_.max_hops && !label_bools.blacklisted && !label_bools.terminated && + (label_bools.end_node || Whitelisted(label_bools.whitelisted))); +} + +bool Path::PathHelper::PathSizeOk(const int64_t path_size) const { + return (path_size <= config_.max_hops) && (path_size >= config_.min_hops); +} + +bool Path::PathHelper::PathTooBig(const int64_t path_size) const { return path_size > config_.max_hops; } + +bool Path::PathHelper::Whitelisted(const bool whitelisted) const { + return (config_.label_bools_status.whitelist_empty || whitelisted); +} + +void Path::PathHelper::FilterLabelBoolStatus() { + config_.label_bools_status.end_node_activated = !config_.label_sets.end_list.empty(); + config_.label_bools_status.whitelist_empty = config_.label_sets.whitelist.empty(); + config_.label_bools_status.termination_activated = !config_.label_sets.termination_list.empty(); +} + +/*function to set appropriate parameters for filtering*/ +void Path::PathHelper::FilterLabel(std::string_view label, LabelBools &label_bools) const { + if (config_.label_sets.blacklist.contains(label)) { + label_bools.blacklisted = true; + } + + if (config_.label_sets.termination_list.contains(label)) { + label_bools.terminated = true; + } + + if (config_.label_sets.end_list.contains(label)) { + label_bools.end_node = true; + } + + if (config_.label_sets.whitelist.contains(label)) { + label_bools.whitelisted = true; + } +} + +// Function that takes input list of labels, and sorts them into appropriate category +// sets were used so when filtering is done, its done in O(1) +void Path::PathHelper::ParseLabels(const mgp::List &list_of_labels) { + for (const auto &label : list_of_labels) { + std::string_view label_string = label.ValueString(); + const char first_elem = label_string.front(); + switch (first_elem) { + case '-': + label_string.remove_prefix(1); + config_.label_sets.blacklist.insert(label_string); + break; + case '>': + label_string.remove_prefix(1); + config_.label_sets.end_list.insert(label_string); + break; + case '+': + label_string.remove_prefix(1); + config_.label_sets.whitelist.insert(label_string); + break; + case '/': + label_string.remove_prefix(1); + config_.label_sets.termination_list.insert(label_string); + break; + default: + config_.label_sets.whitelist.insert(label_string); + break; + } + } +} + +// Function that takes input list of relationships, and sorts them into appropriate categories +// sets were also used to reduce complexity +void Path::PathHelper::ParseRelationships(const mgp::List &list_of_relationships) { + if (list_of_relationships.Size() == + 0) { // if no relationships were passed as arguments, all relationships are allowed + config_.any_outgoing = true; + config_.any_incoming = true; + return; + } + + for (const auto &rel : list_of_relationships) { + std::string rel_type{std::string(rel.ValueString())}; + bool starts_with = rel_type.starts_with('<'); + bool ends_with = rel_type.ends_with('>'); + + if (rel_type.size() == 1) { + if (starts_with) { + config_.any_incoming = true; + } else if (ends_with) { + config_.any_outgoing = true; + } else { + config_.relationship_sets[rel_type] = RelDirection::kAny; + } + continue; + } + + if (starts_with && ends_with) { // + config_.relationship_sets[rel_type.substr(1, rel_type.size() - 2)] = RelDirection::kBoth; + } else if (starts_with) { // + config_.relationship_sets[rel_type.substr(0, rel_type.size() - 1)] = RelDirection::kOutgoing; + } else { // type + config_.relationship_sets[rel_type] = RelDirection::kAny; + } + } +} + +void Path::Elements(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory) { + mgp::MemoryDispatcherGuard guard(memory); + const auto arguments = mgp::List(args); + auto result = mgp::Result(res); + + try { + const auto path{arguments[0].ValuePath()}; + size_t path_length = path.Length(); + mgp::List split_path(path_length * 2 + 1); + for (int i = 0; i < path_length; ++i) { + split_path.Append(mgp::Value(path.GetNodeAt(i))); + split_path.Append(mgp::Value(path.GetRelationshipAt(i))); + } + split_path.Append(mgp::Value(path.GetNodeAt(path.Length()))); + result.SetValue(std::move(split_path)); + + } catch (const std::exception &e) { + result.SetErrorMessage(e.what()); + } +} + +void Path::Combine(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory) { + mgp::MemoryDispatcherGuard guard(memory); + const auto arguments = mgp::List(args); + auto result = mgp::Result(res); + + try { + auto path1{arguments[0].ValuePath()}; + const auto path2{arguments[1].ValuePath()}; + + for (int i = 0; i < path2.Length(); ++i) { + // Expand will throw an exception if it can't connect + path1.Expand(path2.GetRelationshipAt(i)); + } + + result.SetValue(std::move(path1)); + + } catch (const std::exception &e) { + result.SetErrorMessage(e.what()); + } +} + +void Path::Slice(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory) { + mgp::MemoryDispatcherGuard guard(memory); + const auto arguments = mgp::List(args); + auto result = mgp::Result(res); + + try { + const auto path{arguments[0].ValuePath()}; + const auto offset{arguments[1].ValueInt()}; + const auto length{arguments[2].ValueInt()}; + + mgp::Path new_path{path.GetNodeAt(offset)}; + size_t old_path_length = path.Length(); + size_t max_iteration = std::min((length == -1 ? old_path_length : offset + length), old_path_length); + for (int i = static_cast(offset); i < max_iteration; ++i) { + new_path.Expand(path.GetRelationshipAt(i)); + } + + result.SetValue(std::move(new_path)); + + } catch (const std::exception &e) { + result.SetErrorMessage(e.what()); + } +} + void Path::Create(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { - mgp::MemoryDispatcherGuard guard{memory};; + mgp::MemoryDispatcherGuard guard{memory}; const auto arguments = mgp::List(args); const auto record_factory = mgp::RecordFactory(result); try { @@ -58,391 +312,221 @@ void Path::Create(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, } } -bool Path::PathSizeOk(const int64_t path_size, const int64_t &max_hops, const int64_t &min_hops) { - return (path_size + 1 <= max_hops) && (path_size + 1 >= min_hops); +void Path::PathExpand::ExpandPath(mgp::Path &path, const mgp::Relationship &relationship, int64_t path_size) { + path.Expand(relationship); + path_data_.visited_.insert(relationship.Id().AsInt()); + DFS(path, path_size + 1); + path_data_.visited_.erase(relationship.Id().AsInt()); + path.Pop(); } -bool Path::Whitelisted(const bool &whitelisted, const bool &whitelist_empty) { - return (whitelisted || whitelist_empty); -} -bool Path::ShouldExpand(const LabelBools &labelBools, const LabelBoolsStatus &labelStatus) { - return !labelBools.blacklisted && ((labelBools.end_node && labelStatus.end_node_activated) || labelBools.terminated || - (!labelStatus.termination_activated && !labelStatus.end_node_activated && - Whitelisted(labelBools.whitelisted, labelStatus.whitelist_empty))); -} -void Path::FilterLabelBoolStatus(const LabelSets &labelSets, LabelBoolsStatus &labelStatus) { - if (labelSets.end_list.size() != 0) { // end node is activated, which means only paths ending with it can be saved as - // result, but they can be expanded further - labelStatus.end_node_activated = true; - } - if (labelSets.whitelist.size() == 0) { // whitelist is empty, which means all nodes are whitelisted - labelStatus.whitelist_empty = true; - } - - if (labelSets.termination_list.size() != 0) { - labelStatus.termination_activated = true; // there is a termination node, so only paths ending with it are allowed - } -} +void Path::PathExpand::ExpandFromRelationships(mgp::Path &path, mgp::Relationships relationships, bool outgoing, + int64_t path_size, + std::set> &seen) { + for (const auto relationship : relationships) { + auto type = std::string(relationship.Type()); + auto wanted_direction = path_data_.helper_.GetDirection(type); -bool Path::RelationshipAllowed(const std::string &rel_type, const RelationshipSets &relationshipSets, - bool &any_outgoing, bool &any_incoming, bool outgoing) { - if (outgoing) { // for outgoing rels - if (!any_outgoing && (relationshipSets.outgoing_rel.find(rel_type) == relationshipSets.outgoing_rel.end()) && - (relationshipSets.any_rel.find(rel_type) == - relationshipSets.any_rel.end())) { // check if relationship is allowed or all relationships are allowed - return false; + if ((wanted_direction == RelDirection::kNone && !path_data_.helper_.AnyDirected(outgoing)) || + path_data_.visited_.contains(relationship.Id().AsInt())) { + continue; } - } else { // incoming rels - if (!any_incoming && (relationshipSets.incoming_rel.find(rel_type) == relationshipSets.incoming_rel.end()) && - (relationshipSets.any_rel.find(rel_type) == relationshipSets.any_rel.end())) { // check if rel allowed - return false; + RelDirection curr_direction = outgoing ? RelDirection::kOutgoing : RelDirection::kIncoming; + + if (wanted_direction == RelDirection::kAny || curr_direction == wanted_direction || + path_data_.helper_.AnyDirected(outgoing)) { + ExpandPath(path, relationship, path_size); + } else if (wanted_direction == RelDirection::kBoth) { + if (outgoing && seen.contains({type, relationship.To().Id().AsInt()})) { + ExpandPath(path, relationship, path_size); + } else { + seen.insert({type, relationship.From().Id().AsInt()}); + } } } - return true; } -/*function to set appropriate parameters for filtering*/ -void Path::FilterLabel(const std::string_view label, const LabelSets &labelSets, LabelBools &labelBools) { - if (labelSets.blacklist.find(label) != labelSets.blacklist.end()) { // if label is blacklisted - labelBools.blacklisted = true; - } - if (labelSets.termination_list.find(label) != labelSets.termination_list.end()) { // if label is termination label - labelBools.terminated = true; - } +/*function used for traversal and filtering*/ +void Path::PathExpand::DFS(mgp::Path &path, int64_t path_size) { + const mgp::Node node{path.GetNodeAt(path_size)}; - if (labelSets.end_list.find(label) != labelSets.end_list.end()) { // if label is end label - labelBools.end_node = true; + LabelBools label_bools = path_data_.helper_.GetLabelBools(node); + if (path_data_.helper_.PathSizeOk(path_size) && path_data_.helper_.AreLabelsValid(label_bools)) { + auto record = path_data_.record_factory_.NewRecord(); + record.Insert(std::string(kResultExpand).c_str(), path); } - if (labelSets.whitelist.find(label) != labelSets.whitelist.end()) { // if label is whitelisted - labelBools.whitelisted = true; + if (!path_data_.helper_.ContinueExpanding(label_bools, path_size + 1)) { + return; } -} -/*function that takes input list of labels, and sorts them into appropriate category -sets were used so when filtering is done, its done in O(1)*/ -void Path::ParseLabels(const mgp::List &list_of_labels, LabelSets &labelSets) { - for (const auto label : list_of_labels) { - std::string_view label_string = label.ValueString(); - const char first_elem = label_string.front(); - switch (first_elem) { - case '-': - label_string.remove_prefix(1); - labelSets.blacklist.insert(label_string); - break; - case '>': - label_string.remove_prefix(1); - labelSets.end_list.insert(label_string); - break; - case '+': - label_string.remove_prefix(1); - labelSets.whitelist.insert(label_string); - break; - case '/': - label_string.remove_prefix(1); - labelSets.termination_list.insert(label_string); - break; - default: - labelSets.whitelist.insert(label_string); - break; - } - } + std::set> seen; + this->ExpandFromRelationships(path, node.InRelationships(), false, path_size, seen); + this->ExpandFromRelationships(path, node.OutRelationships(), true, path_size, seen); } -/*function that takes input list of relationships, and sorts them into appropriate categories -sets were also used to reduce complexity*/ -void Path::ParseRelationships(const mgp::List &list_of_relationships, RelationshipSets &relationshipSets, - bool &any_outgoing, bool &any_incoming) { - if (list_of_relationships.Size() == - 0) { // if no relationships were passed as arguments, all relationships are allowed - any_outgoing = true; - any_incoming = true; - return; - } - for (const auto rel : list_of_relationships) { - std::string rel_type = std::string(rel.ValueString()); - const size_t size = rel_type.size(); - const char first_elem = rel_type[0]; - const char last_elem = rel_type[size - 1]; - if (first_elem == '<' && last_elem == '>') { - throw mgp::ValueException("Wrong relationship format => is not allowed!"); - } else if (first_elem == '<' && size == 1) { - any_incoming = true; // all incoming relatiomships are allowed - } else if (first_elem == '<' && size != 1) { - relationshipSets.incoming_rel.insert(rel_type.erase(0, 1)); // only specified incoming relationships are allowed - } else if (last_elem == '>' && size == 1) { - any_outgoing = true; // all outgoing relationships are allowed - - } else if (last_elem == '>' && size != 1) { - rel_type.pop_back(); - relationshipSets.outgoing_rel.insert(rel_type); // only specifed outgoing relationships are allowed - } else { // if not specified, a relationship goes both ways - relationshipSets.any_rel.insert(rel_type); - } - } +void Path::PathExpand::StartAlgorithm(const mgp::Node node) { + mgp::Path path = mgp::Path(node); + DFS(path, 0); } -void Path::DfsByDirection(mgp::Path &path, std::unordered_set &relationships_set, - const mgp::RecordFactory &record_factory, int64_t path_size, const int64_t min_hops, - const int64_t max_hops, const LabelSets &labelSets, const LabelBoolsStatus &labelStatus, - const RelationshipSets &relationshipSets, bool &any_outgoing, bool &any_incoming, - bool outgoing) { - const mgp::Node &node = path.GetNodeAt(path_size); // get latest node in path, and expand on it - - mgp::Relationships rels = outgoing ? node.OutRelationships() : node.InRelationships(); - for (auto rel : rels) { - // go through every relationship of the node and expand to the other node of the relationship - if (relationships_set.find(rel) != - relationships_set.end()) { // relationships_set contains all relationships already visited in this path, and - // the usage of this if loop is to evade cycles - continue; - } - mgp::Path cpy = mgp::Path(path); - const std::string rel_type = std::string(rel.Type()); - bool rel_allowed = outgoing ? RelationshipAllowed(rel_type, relationshipSets, any_outgoing, any_incoming, true) - : RelationshipAllowed(rel_type, relationshipSets, any_outgoing, any_incoming, false); - if (!rel_allowed) { // if relationship not allowed, go to next one - continue; - } - cpy.Expand(rel); // expand the path with this relationships - std::unordered_set relationships_set_cpy = relationships_set; - relationships_set_cpy.insert(rel); // insert the relationship into visited relationships - - /*this part is for label filtering*/ - LabelBools labelBools; - mgp::Labels labels = outgoing ? rel.To().Labels() : rel.From().Labels(); - for (auto label : labels) { // set booleans to their value for the label of the finish node - FilterLabel(label, labelSets, labelBools); - } - - if (PathSizeOk(path_size, max_hops, min_hops) && ShouldExpand(labelBools, labelStatus)) { - auto record = record_factory.NewRecord(); - record.Insert(std::string(std::string(kResultExpand).c_str()).c_str(), cpy); - } - if (path_size + 1 < max_hops && !labelBools.blacklisted && !labelBools.terminated && - (labelBools.end_node || Whitelisted(labelBools.whitelisted, labelStatus.whitelist_empty))) { - PathDFS(cpy, relationships_set_cpy, record_factory, path_size + 1, min_hops, max_hops, labelSets, labelStatus, - relationshipSets, any_outgoing, any_incoming); - } +void Path::PathExpand::Parse(const mgp::Value &value) { + if (value.IsNode()) { + path_data_.start_nodes_.insert((value.ValueNode())); + } else if (value.IsInt()) { + path_data_.start_nodes_.insert((path_data_.graph_.GetNodeById(mgp::Id::FromInt(value.ValueInt())))); + } else { + throw mgp::ValueException("Invalid start type. Expected Node, Int, List[Node, Int]"); } } -/*function used for traversal and filtering*/ -void Path::PathDFS(mgp::Path path, std::unordered_set &relationships_set, - const mgp::RecordFactory &record_factory, int64_t path_size, const int64_t min_hops, - const int64_t max_hops, const LabelSets &labelSets, const LabelBoolsStatus &labelStatus, - const RelationshipSets &relationshipSets, bool &any_outgoing, bool &any_incoming) { - DfsByDirection(path, relationships_set, record_factory, path_size, min_hops, max_hops, labelSets, labelStatus, - relationshipSets, any_outgoing, any_incoming, true); - DfsByDirection(path, relationships_set, record_factory, path_size, min_hops, max_hops, labelSets, labelStatus, - relationshipSets, any_outgoing, any_incoming, false); -} - -void Path::StartFunction(const mgp::Node &node, const mgp::RecordFactory &record_factory, int64_t path_size, - const int64_t min_hops, const int64_t max_hops, const LabelSets &labelSets, - const LabelBoolsStatus &labelStatus, const RelationshipSets &relationshipSets, - bool &any_outgoing, bool &any_incoming) { - mgp::Path path = mgp::Path(node); - std::unordered_set relationships_set; - PathDFS(path, relationships_set, record_factory, 0, min_hops, max_hops, labelSets, labelStatus, relationshipSets, - any_outgoing, any_incoming); +void Path::PathExpand::RunAlgorithm() { + for (const auto &node : path_data_.start_nodes_) { + StartAlgorithm(node); + } } void Path::Expand(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { - mgp::MemoryDispatcherGuard guard{memory};; + mgp::MemoryDispatcherGuard guard{memory}; const auto arguments = mgp::List(args); const auto record_factory = mgp::RecordFactory(result); try { - mgp::Graph graph = mgp::Graph(memgraph_graph); + auto graph = mgp::Graph(memgraph_graph); const mgp::Value start_value = arguments[0]; - mgp::List relationships = arguments[1].ValueList(); - const mgp::List labels = arguments[2].ValueList(); - int64_t min_hops = arguments[3].ValueInt(); - int64_t max_hops = arguments[4].ValueInt(); - - /*filter label part*/ - LabelSets labelSets; - LabelBoolsStatus labelStatus; - ParseLabels(labels, labelSets); - FilterLabelBoolStatus(labelSets, labelStatus); - /*end filter label part*/ - - /*filter relationships part*/ - RelationshipSets relationshipSets; - bool any_outgoing = false; - bool any_incoming = false; - ParseRelationships(relationships, relationshipSets, any_outgoing, any_incoming); - /*end filter relationships part*/ - - if (start_value.IsNode()) { - StartFunction(start_value.ValueNode(), record_factory, 0, min_hops, max_hops, labelSets, labelStatus, - relationshipSets, any_outgoing, any_incoming); - } else if (start_value.IsInt()) { - StartFunction(graph.GetNodeById(mgp::Id::FromInt(start_value.ValueInt())), record_factory, 0, min_hops, max_hops, - labelSets, labelStatus, relationshipSets, any_outgoing, any_incoming); - } else if (start_value.IsList()) { - for (const auto value : start_value.ValueList()) { - if (value.IsNode()) { - StartFunction(value.ValueNode(), record_factory, 0, min_hops, max_hops, labelSets, labelStatus, - relationshipSets, any_outgoing, any_incoming); - - } else if (value.IsInt()) { - StartFunction(graph.GetNodeById(mgp::Id::FromInt(value.ValueInt())), record_factory, 0, min_hops, max_hops, - labelSets, labelStatus, relationshipSets, any_outgoing, any_incoming); - } else { - throw mgp::ValueException("Invalid start type. Expected Node, Int, List[Node, Int]"); - } - } + mgp::List relationships{arguments[1].ValueList()}; + const mgp::List labels{arguments[2].ValueList()}; + int64_t min_hops{arguments[3].ValueInt()}; + int64_t max_hops{arguments[4].ValueInt()}; + + PathExpand path_expand{PathData(PathHelper{labels, relationships, min_hops, max_hops}, record_factory, graph)}; + + if (!start_value.IsList()) { + path_expand.Parse(start_value); } else { - throw mgp::ValueException("Invalid start type. Expected Node, Int, List[Node, Int]"); + for (const auto &list_item : start_value.ValueList()) { + path_expand.Parse(list_item); + } } + path_expand.RunAlgorithm(); + } catch (const std::exception &e) { record_factory.SetErrorMessage(e.what()); return; } } -void GetStartNodes(const mgp::Value element, const mgp::Graph &graph, std::unordered_set &start_nodes) { - if (!(element.IsNode() || element.IsInt())) { +void Path::PathSubgraph::Parse(const mgp::Value &value) { + if (!(value.IsNode() || value.IsInt())) { throw mgp::ValueException("The first argument needs to be a node, an integer ID, or a list thereof."); } - if (element.IsNode()) { - start_nodes.insert(element.ValueNode()); + if (value.IsNode()) { + path_data_.start_nodes_.insert(value.ValueNode()); return; } - start_nodes.insert(graph.GetNodeById(mgp::Id::FromInt(element.ValueInt()))); + path_data_.start_nodes_.insert(path_data_.graph_.GetNodeById(mgp::Id::FromInt(value.ValueInt()))); } -bool RelFilterAllows(const mgp::Map &config, std::string_view type, bool ingoing) { - mgp::List list_of_types = config.At("relationshipFilter").ValueList(); - if (list_of_types.Size() == 0) { - return true; - } - for (const auto element : list_of_types) { - auto string_rel_type = element.ValueString(); - if (string_rel_type.front() == '<' && ingoing) { - string_rel_type.remove_prefix(1); - } - if (string_rel_type.back() == '>' && !ingoing) { - string_rel_type.remove_suffix(1); - } - if (string_rel_type == type || string_rel_type.size() == 0) { - return true; +void Path::PathSubgraph::ExpandFromRelationships(const std::pair &pair, + const mgp::Relationships relationships, bool outgoing, + std::queue> &queue, + std::set> &seen) { + for (const auto relationship : relationships) { + auto next_node = outgoing ? relationship.To() : relationship.From(); + auto type = std::string(relationship.Type()); + auto wanted_direction = path_data_.helper_.GetDirection(type); + + if (path_data_.helper_.IsNotStartOrSupportsStartRel(pair.second == 0)) { + if ((wanted_direction == RelDirection::kNone && !path_data_.helper_.AnyDirected(outgoing)) || + path_data_.visited_.contains(next_node.Id().AsInt())) { + continue; + } } - } - return false; -} -bool IsLabelListed(const mgp::Node node, std::unordered_set &set) { - for (const auto label : node.Labels()) { - if (set.contains(label)) { - return true; + RelDirection curr_direction = outgoing ? RelDirection::kOutgoing : RelDirection::kIncoming; + + if (wanted_direction == RelDirection::kAny || curr_direction == wanted_direction || + path_data_.helper_.AnyDirected(outgoing)) { + path_data_.visited_.insert(next_node.Id().AsInt()); + queue.push({next_node, pair.second + 1}); + } else if (wanted_direction == RelDirection::kBoth) { + if (outgoing && seen.contains({type, relationship.To().Id().AsInt()})) { + path_data_.visited_.insert(next_node.Id().AsInt()); + queue.push({next_node, pair.second + 1}); + to_be_returned_nodes_.AppendExtend(mgp::Value{next_node}); + } else { + seen.insert({type, relationship.From().Id().AsInt()}); + } } } - return false; } -void Path::VisitNode(const mgp::Node node, std::map &visited_nodes, bool is_start, - const mgp::Map &config, int64_t hop_count, Path::LabelSets &labelFilterSets, - mgp::List &to_be_returned_nodes) { - if (config.At("maxLevel").ValueInt() != -1 && hop_count > config.At("maxLevel").ValueInt()) { - return; - } - if (config.At("filterStartNode").ValueBool() || !is_start) { - if ((IsLabelListed(node, labelFilterSets.blacklist)) || - (!labelFilterSets.whitelist.empty() && !IsLabelListed(node, labelFilterSets.whitelist) && - !IsLabelListed(node, labelFilterSets.end_list) && !IsLabelListed(node, labelFilterSets.termination_list))) { - return; - } - } - try { - if (visited_nodes.at(node) <= hop_count) { - return; +void Path::PathSubgraph::TryInsertNode(const mgp::Node &node, int64_t hop_count, LabelBools &label_bools) { + if (path_data_.helper_.IsNotStartOrSupportsStartNode(hop_count == 0)) { + if (path_data_.helper_.AreLabelsValid(label_bools)) { + to_be_returned_nodes_.AppendExtend(mgp::Value(node)); } - } catch (const std::out_of_range &e) { - // it's okay, the node is not in visited nodes map - if (!is_start || config.At("minLevel").ValueInt() != 1) { - if ((labelFilterSets.end_list.empty() && labelFilterSets.termination_list.empty()) || - IsLabelListed(node, labelFilterSets.end_list) || IsLabelListed(node, labelFilterSets.termination_list)) { - to_be_returned_nodes.AppendExtend(mgp::Value(node)); - } - } - } - visited_nodes.insert({node, hop_count}); - if (IsLabelListed(node, labelFilterSets.termination_list)) { return; } - for (const auto in_rel : node.InRelationships()) { - if (RelFilterAllows(config, in_rel.Type(), true)) { - VisitNode(in_rel.From(), visited_nodes, false, config, hop_count + 1, labelFilterSets, to_be_returned_nodes); - } - } - for (const auto out_rel : node.OutRelationships()) { - if (RelFilterAllows(config, out_rel.Type(), false)) { - VisitNode(out_rel.To(), visited_nodes, false, config, hop_count + 1, labelFilterSets, to_be_returned_nodes); - } + + if (!path_data_.visited_.contains(node.Id().AsInt())) { + to_be_returned_nodes_.AppendExtend(mgp::Value(node)); } } -void SetConfig(mgp::Map &config) { - auto value = config.At("maxLevel"); - if (value.IsNull()) { - config.Insert("maxLevel", mgp::Value(int64_t(-1))); - } - value = config.At("relationshipFilter"); - if (value.IsNull()) { - config.Insert("relationshipFilter", mgp::Value(mgp::List())); - } - value = config.At("labelFilter"); - if (value.IsNull()) { - config.Insert("labelFilter", mgp::Value(mgp::List())); - } - value = config.At("filterStartNode"); - if (value.IsNull()) { - config.Insert("filterStartNode", mgp::Value(false)); - } - value = config.At("minLevel"); - if (value.IsNull()) { - config.Insert("minLevel", mgp::Value(int64_t(0))); +mgp::List Path::PathSubgraph::BFS() { + std::queue> queue; + std::unordered_set visited; + + for (const auto &node : path_data_.start_nodes_) { + queue.push({node, 0}); + visited.insert(node.Id().AsInt()); } - if (!(config.At("minLevel").IsInt() && config.At("maxLevel").IsInt() && config.At("relationshipFilter").IsList() && - config.At("labelFilter").IsList() && config.At("filterStartNode").IsBool()) || - (config.At("minLevel").ValueInt() != 0 && config.At("minLevel").ValueInt() != 1)) { - throw mgp::ValueException( - "The config parameter needs to be a map with keys and values in line with the documentation."); + + while (!queue.empty()) { + auto pair = queue.front(); + queue.pop(); + + if (path_data_.helper_.PathTooBig(pair.second)) { + continue; + } + + LabelBools label_bools = path_data_.helper_.GetLabelBools(pair.first); + TryInsertNode(pair.first, pair.second, label_bools); + if (!path_data_.helper_.ContinueExpanding(label_bools, pair.second + 1)) { + continue; + } + + std::set> seen; + this->ExpandFromRelationships(pair, pair.first.InRelationships(), false, queue, seen); + this->ExpandFromRelationships(pair, pair.first.OutRelationships(), true, queue, seen); } + + return to_be_returned_nodes_; } void Path::SubgraphNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { - mgp::MemoryDispatcherGuard guard{memory};; + mgp::MemoryDispatcherGuard guard{memory}; const auto arguments = mgp::List(args); const auto graph = mgp::Graph(memgraph_graph); const auto record_factory = mgp::RecordFactory(result); try { auto config = arguments[1].ValueMap(); - SetConfig(config); + PathSubgraph path_subgraph{PathData(PathHelper{config}, record_factory, graph)}; - std::unordered_set start_nodes; - if (arguments[0].IsList()) { - for (const auto element : arguments[0].ValueList()) { - GetStartNodes(element, graph, start_nodes); - } + auto start_value = arguments[0]; + if (!start_value.IsList()) { + path_subgraph.Parse(start_value); } else { - GetStartNodes(arguments[0], graph, start_nodes); + for (const auto &list_item : start_value.ValueList()) { + path_subgraph.Parse(list_item); + } } - LabelSets labelFilterSets; - ParseLabels(config.At("labelFilter").ValueList(), labelFilterSets); + auto to_be_returned_nodes = path_subgraph.BFS(); - std::map visited_nodes; - mgp::List to_be_returned_nodes; - for (const auto node : start_nodes) { - VisitNode(node, visited_nodes, true, config, 0, labelFilterSets, to_be_returned_nodes); - } - - for (auto node : to_be_returned_nodes) { + for (const auto &node : to_be_returned_nodes) { auto record = record_factory.NewRecord(); record.Insert(std::string(kResultSubgraphNodes).c_str(), node); } @@ -454,36 +538,30 @@ void Path::SubgraphNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result * } void Path::SubgraphAll(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { - mgp::MemoryDispatcherGuard guard{memory};; + mgp::MemoryDispatcherGuard guard{memory}; const auto arguments = mgp::List(args); const auto graph = mgp::Graph(memgraph_graph); const auto record_factory = mgp::RecordFactory(result); try { auto config = arguments[1].ValueMap(); - SetConfig(config); + PathSubgraph path_subgraph{PathData(PathHelper{config}, record_factory, graph)}; - std::unordered_set start_nodes; - if (arguments[0].IsList()) { - for (const auto element : arguments[0].ValueList()) { - GetStartNodes(element, graph, start_nodes); - } + auto start_value = arguments[0]; + if (!start_value.IsList()) { + path_subgraph.Parse(start_value); } else { - GetStartNodes(arguments[0], graph, start_nodes); + for (const auto &list_item : start_value.ValueList()) { + path_subgraph.Parse(list_item); + } } - LabelSets labelFilterSets; - ParseLabels(config.At("labelFilter").ValueList(), labelFilterSets); - - std::map visited_nodes; - mgp::List to_be_returned_nodes; - for (const auto node : start_nodes) { - VisitNode(node, visited_nodes, true, config, 0, labelFilterSets, to_be_returned_nodes); - } + const auto to_be_returned_nodes = path_subgraph.BFS(); std::unordered_set to_be_returned_nodes_searchable; - for (auto node : to_be_returned_nodes) { - to_be_returned_nodes_searchable.insert(node.ValueNode()); - } + + std::transform(to_be_returned_nodes.begin(), to_be_returned_nodes.end(), + std::inserter(to_be_returned_nodes_searchable, to_be_returned_nodes_searchable.begin()), + [](const mgp::Value &node) { return node.ValueNode(); }); mgp::List to_be_returned_rels; for (auto node : to_be_returned_nodes) { diff --git a/cpp/path_module/algorithm/path.hpp b/cpp/path_module/algorithm/path.hpp index 0e3ca7d1d..c724a43e1 100644 --- a/cpp/path_module/algorithm/path.hpp +++ b/cpp/path_module/algorithm/path.hpp @@ -2,11 +2,27 @@ #include +#include #include #include namespace Path { +/* elements constants */ +constexpr const std::string_view kProcedureElements = "elements"; +constexpr const std::string_view kElementsArg1 = "path"; + +/* combine constants */ +constexpr const std::string_view kProcedureCombine = "combine"; +constexpr const std::string_view kCombineArg1 = "first"; +constexpr const std::string_view kCombineArg2 = "second"; + +/* slice constants */ +constexpr const std::string_view kProcedureSlice = "slice"; +constexpr const std::string_view kSliceArg1 = "path"; +constexpr const std::string_view kSliceArg2 = "offset"; +constexpr const std::string_view kSliceArg3 = "length"; + /* create constants */ constexpr const std::string_view kProcedureCreate = "create"; constexpr const std::string_view kCreateArg1 = "start_node"; @@ -44,64 +60,131 @@ struct LabelSets { }; struct LabelBools { + // no node in the path will be blacklisted bool blacklisted = false; + // returned paths end with a termination node but don't continue to be expanded further, + // takes precedence over end nodes bool terminated = false; + // returned paths end with an end node but continue to be expanded further bool end_node = false; + // all nodes in the path will be whitelisted (except end and termination nodes) + // end and termination nodes don't have to respect whitelists and blacklists bool whitelisted = false; }; struct LabelBoolsStatus { + // true if there is an end node -> only paths ending with it can be saved as result, + // but they can be expanded further bool end_node_activated = false; + // true if no whitelist is given -> all nodes are whitelisted bool whitelist_empty = false; + // true if there is a termination node -> only paths ending with it are allowed bool termination_activated = false; }; -struct RelationshipSets { - std::unordered_set outgoing_rel; - std::unordered_set incoming_rel; - std::unordered_set any_rel; +enum class RelDirection { kNone = -1, kAny = 0, kIncoming = 1, kOutgoing = 2, kBoth = 3 }; + +struct Config { + LabelBoolsStatus label_bools_status; + std::unordered_map relationship_sets; + LabelSets label_sets; + int64_t min_hops = 0; + int64_t max_hops = std::numeric_limits::max(); + bool any_incoming = false; + bool any_outgoing = false; + bool filter_start_node = true; + bool begin_sequence_at_start = true; + bool bfs = false; }; -void Create(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); +class PathHelper { + public: + explicit PathHelper(const mgp::List &labels, const mgp::List &relationships, int64_t min_hops, int64_t max_hops); + explicit PathHelper(const mgp::Map &config); -void FilterLabelBoolStatus(const LabelSets &labelSets, LabelBoolsStatus &labelStatus); + RelDirection GetDirection(const std::string &rel_type) const; + LabelBools GetLabelBools(const mgp::Node &node) const; -bool ShouldExpand(const LabelBools &labelBools, const LabelBoolsStatus &labelStatus); + bool AnyDirected(bool outgoing) const { return outgoing ? config_.any_outgoing : config_.any_incoming; } + bool IsNotStartOrSupportsStartNode(bool is_start) const { return (config_.filter_start_node || !is_start); } + bool IsNotStartOrSupportsStartRel(bool is_start) const { return (config_.begin_sequence_at_start || !is_start); } -void Expand(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); + bool AreLabelsValid(const LabelBools &label_bools) const; + bool ContinueExpanding(const LabelBools &label_bools, size_t path_size) const; -void FilterLabel(const std::string_view label, const LabelSets &labelFilters, LabelBools &labelBools); + bool PathSizeOk(int64_t path_size) const; + bool PathTooBig(int64_t path_size) const; + bool Whitelisted(bool whitelisted) const; -void ParseLabels(const mgp::List &list_of_labels, LabelSets &labelSets); + // methods for parsing config + void FilterLabelBoolStatus(); + void FilterLabel(std::string_view label, LabelBools &label_bools) const; + void ParseLabels(const mgp::List &list_of_labels); + void ParseRelationships(const mgp::List &list_of_relationships); -void ParseRelationships(const mgp::List &list_of_relationships, RelationshipSets &relationshipSets, bool &any_outgoing, - bool &any_incoming); -bool PathSizeOk(const int64_t path_size, const int64_t &max_hops, const int64_t &min_hops); + private: + Config config_; +}; -bool RelationshipAllowed(const std::string &rel_type, const RelationshipSets &relationshipSets, bool &any_outgoing, - bool &any_incoming, bool outgoing); +class PathData { + public: + friend class PathExpand; + friend class PathSubgraph; -bool Whitelisted(const bool &whitelisted, const bool &whitelist_empty); + explicit PathData(PathHelper &&helper, const mgp::RecordFactory &record_factory, const mgp::Graph &graph) + : helper_(std::move(helper)), record_factory_(record_factory), graph_(graph) {} -void PathDFS(mgp::Path path, std::unordered_set &relationships_set, - const mgp::RecordFactory &record_factory, int64_t path_size, const int64_t min_hops, - const int64_t max_hops, const LabelSets &labelFilters, const LabelBoolsStatus &labelStatus, - const RelationshipSets &relationshipSets, bool &any_outgoing, bool &any_incoming); + PathHelper helper_; + const mgp::RecordFactory &record_factory_; + const mgp::Graph &graph_; + std::unordered_set visited_; + std::unordered_set start_nodes_; +}; + +class PathExpand { + public: + explicit PathExpand(PathData &&path_data) : path_data_(std::move(path_data)) {} + + void ExpandPath(mgp::Path &path, const mgp::Relationship &relationship, int64_t path_size); + void ExpandFromRelationships(mgp::Path &path, mgp::Relationships relationships, bool outgoing, int64_t path_size, + std::set> &seen); + void StartAlgorithm(mgp::Node node); + void Parse(const mgp::Value &value); + void DFS(mgp::Path &path, int64_t path_size); + void RunAlgorithm(); + + private: + PathData path_data_; +}; -void DfsByDirection(mgp::Path &path, std::unordered_set &relationships_set, - const mgp::RecordFactory &record_factory, int64_t path_size, const int64_t min_hops, - const int64_t max_hops, const LabelSets &labelFilters, const LabelBoolsStatus &labelStatus, - const RelationshipSets &relationshipSets, bool &any_outgoing, bool &any_incoming, bool outgoing); +class PathSubgraph { + public: + explicit PathSubgraph(PathData &&path_data) : path_data_(std::move(path_data)) {} -void StartFunction(const mgp::Node &node, const mgp::RecordFactory &record_factory, int64_t path_size, - const int64_t min_hops, const int64_t max_hops, const LabelSets &labelSets, - const LabelBoolsStatus &labelStatus, const RelationshipSets &relationshipSets, bool &any_outgoing, - bool &any_incoming); + void ExpandFromRelationships(const std::pair &pair, mgp::Relationships relationships, + bool outgoing, std::queue> &queue, + std::set> &seen); + void Parse(const mgp::Value &value); + void TryInsertNode(const mgp::Node &node, int64_t hop_count, LabelBools &label_bools); + mgp::List BFS(); + + private: + PathData path_data_; + mgp::List to_be_returned_nodes_; +}; + +void Elements(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory); + +void Combine(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory); + +void Slice(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory); + +void Create(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); + +void Expand(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); void SubgraphNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); + void SubgraphAll(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); -void VisitNode(const mgp::Node node, std::map &visited_nodes, bool is_start, - const mgp::Map &config, int64_t hop_count, Path::LabelSets &labelFilterSets, - mgp::List &to_be_returned_nodes); } // namespace Path diff --git a/cpp/path_module/path_module.cpp b/cpp/path_module/path_module.cpp index 237743df7..eb0de2e31 100644 --- a/cpp/path_module/path_module.cpp +++ b/cpp/path_module/path_module.cpp @@ -4,7 +4,22 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) { try { - mgp::MemoryDispatcherGuard guard{memory};; + mgp::MemoryDispatcherGuard guard{memory}; + + mgp::AddFunction(Path::Elements, Path::kProcedureElements, {mgp::Parameter(Path::kElementsArg1, mgp::Type::Path)}, + module, memory); + + mgp::AddFunction( + Path::Combine, Path::kProcedureCombine, + {mgp::Parameter(Path::kCombineArg1, mgp::Type::Path), mgp::Parameter(Path::kCombineArg2, mgp::Type::Path)}, + module, memory); + + mgp::AddFunction(Path::Slice, Path::kProcedureSlice, + {mgp::Parameter(Path::kSliceArg1, mgp::Type::Path), + mgp::Parameter(Path::kSliceArg2, mgp::Type::Int, static_cast(0)), + mgp::Parameter(Path::kSliceArg3, mgp::Type::Int, static_cast(-1))}, + module, memory); + AddProcedure( Path::Expand, std::string(Path::kProcedureExpand).c_str(), mgp::ProcedureType::Read, {mgp::Parameter(std::string(Path::kArgumentStartExpand).c_str(), mgp::Type::Any), diff --git a/cpp/schema_module/CMakeLists.txt b/cpp/schema_module/CMakeLists.txt deleted file mode 100644 index 960bfe0a1..000000000 --- a/cpp/schema_module/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -set(schema_module_src - schema_module.cpp - algorithm/schema.cpp) - -add_query_module(schema 1 "${schema_module_src}") diff --git a/cpp/schema_module/algorithm/schema.cpp b/cpp/schema_module/algorithm/schema.cpp deleted file mode 100644 index 107e08932..000000000 --- a/cpp/schema_module/algorithm/schema.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include "schema.hpp" - -/*we have << operator for type in Cpp API, but in it we return somewhat different strings than I would like in this -module, so I implemented a small function here*/ -std::string Schema::TypeOf(const mgp::Type &type) { - switch (type) { - case mgp::Type::Null: - return "Null"; - case mgp::Type::Bool: - return "Bool"; - case mgp::Type::Int: - return "Int"; - case mgp::Type::Double: - return "Double"; - case mgp::Type::String: - return "String"; - case mgp::Type::List: - return "List[Any]"; - case mgp::Type::Map: - return "Map[Any]"; - case mgp::Type::Node: - return "Vertex"; - case mgp::Type::Relationship: - return "Edge"; - case mgp::Type::Path: - return "Path"; - case mgp::Type::Date: - return "Date"; - case mgp::Type::LocalTime: - return "LocalTime"; - case mgp::Type::LocalDateTime: - return "LocalDateTime"; - case mgp::Type::Duration: - return "Duration"; - default: - throw mgp::ValueException("Unsupported type"); - } -} -template -void Schema::ProcessPropertiesNode(mgp::Record &record, const std::string &type, const mgp::List &labels, const std::string &propertyName, - const T &propertyType, const bool &mandatory) { - record.Insert(std::string(kReturnNodeType).c_str(), type); - record.Insert(std::string(kReturnLabels).c_str(), labels); - record.Insert(std::string(kReturnPropertyName).c_str(), propertyName); - record.Insert(std::string(kReturnPropertyType).c_str(), propertyType); - record.Insert(std::string(kReturnMandatory).c_str(), mandatory); -} - -template -void Schema::ProcessPropertiesRel(mgp::Record &record, const std::string_view &type, const std::string &propertyName, - const T &propertyType, const bool &mandatory) { - record.Insert(std::string(kReturnRelType).c_str(), type); - record.Insert(std::string(kReturnPropertyName).c_str(), propertyName); - record.Insert(std::string(kReturnPropertyType).c_str(), propertyType); - record.Insert(std::string(kReturnMandatory).c_str(), mandatory); -} - -void Schema::NodeTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { - mgp::MemoryDispatcherGuard guard{memory};; - const auto record_factory = mgp::RecordFactory(result); - try { - const mgp::Graph graph = mgp::Graph(memgraph_graph); - for (auto node : graph.Nodes()) { - std::string type = ""; - mgp::List labels = mgp::List(); - for (auto label : node.Labels()) { - labels.AppendExtend(mgp::Value(label)); - type += ":`" + std::string(label) + "`"; - } - - - if (node.Properties().size() == 0) { - auto record = record_factory.NewRecord(); - ProcessPropertiesNode(record, type, labels, "", "", false); - continue; - } - - for (auto &[key, prop] : node.Properties()) { - auto property_type = mgp::List(); - auto record = record_factory.NewRecord(); - property_type.AppendExtend(mgp::Value(TypeOf(prop.Type()))); - ProcessPropertiesNode(record, type, labels, key, property_type, true); - } - } - - } catch (const std::exception &e) { - record_factory.SetErrorMessage(e.what()); - return; - } -} - -void Schema::RelTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { - mgp::MemoryDispatcherGuard guard{memory};; - const auto record_factory = mgp::RecordFactory(result); - try { - const mgp::Graph graph = mgp::Graph(memgraph_graph); - - for (auto rel : graph.Relationships()) { - std::string type = ":`" + std::string(rel.Type()) + "`"; - if (rel.Properties().size() == 0) { - auto record = record_factory.NewRecord(); - ProcessPropertiesRel(record, type, "","", false); - continue; - } - - for (auto &[key, prop] : rel.Properties()) { - auto property_type = mgp::List(); - auto record = record_factory.NewRecord(); - property_type.AppendExtend(mgp::Value(TypeOf(prop.Type()))); - ProcessPropertiesRel(record, type, key, property_type, true); - } - } - - } catch (const std::exception &e) { - record_factory.SetErrorMessage(e.what()); - return; - } -} diff --git a/cpp/schema_module/algorithm/schema.hpp b/cpp/schema_module/algorithm/schema.hpp deleted file mode 100644 index ce1d3b31b..000000000 --- a/cpp/schema_module/algorithm/schema.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include - -namespace Schema { - -/*NodeTypeProperties and RelTypeProperties constants*/ -constexpr std::string_view kReturnNodeType = "nodeType"; -constexpr std::string_view kProcedureNodeType = "node_type_properties"; -constexpr std::string_view kProcedureRelType = "rel_type_properties"; -constexpr std::string_view kReturnLabels = "nodeLabels"; -constexpr std::string_view kReturnRelType = "relType"; -constexpr std::string_view kReturnPropertyName = "propertyName"; -constexpr std::string_view kReturnPropertyType = "propertyTypes"; -constexpr std::string_view kReturnMandatory = "mandatory"; - -std::string TypeOf(const mgp::Type &type); - -template -void ProcessPropertiesNode(mgp::Record &record, const std::string &type,const mgp::List &labels, const std::string &propertyName, - const T &propertyType, const bool &mandatory); - -template -void ProcessPropertiesRel(mgp::Record &record, const std::string_view &type, const std::string &propertyName, - const T &propertyType, const bool &mandatory); - - -void NodeTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); -void RelTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); -} // namespace Schema diff --git a/cpp/schema_module/schema_module.cpp b/cpp/schema_module/schema_module.cpp deleted file mode 100644 index 28de5b99c..000000000 --- a/cpp/schema_module/schema_module.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include - -#include "algorithm/schema.hpp" - -extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) { - try { - mgp::MemoryDispatcherGuard guard{memory};; - - AddProcedure(Schema::NodeTypeProperties, std::string(Schema::kProcedureNodeType).c_str(), mgp::ProcedureType::Read, - {}, - {mgp::Return(std::string(Schema::kReturnNodeType).c_str(), mgp::Type::String), - mgp::Return(std::string(Schema::kReturnLabels).c_str(), {mgp::Type::List, mgp::Type::String}), - mgp::Return(std::string(Schema::kReturnPropertyName).c_str(), mgp::Type::String), - mgp::Return(std::string(Schema::kReturnPropertyType).c_str(), mgp::Type::Any), - mgp::Return(std::string(Schema::kReturnMandatory).c_str(), mgp::Type::Bool)}, - module, memory); - - AddProcedure(Schema::RelTypeProperties, std::string(Schema::kProcedureRelType).c_str(), mgp::ProcedureType::Read, - {}, - {mgp::Return(std::string(Schema::kReturnRelType).c_str(), mgp::Type::String), - mgp::Return(std::string(Schema::kReturnPropertyName).c_str(), mgp::Type::String), - mgp::Return(std::string(Schema::kReturnPropertyType).c_str(), mgp::Type::Any), - mgp::Return(std::string(Schema::kReturnMandatory).c_str(), mgp::Type::Bool)}, - module, memory); - - } catch (const std::exception &e) { - return 1; - } - - return 0; -} - -extern "C" int mgp_shutdown_module() { return 0; } diff --git a/e2e/convert_test/test_str2object1/file1 b/e2e/convert_test/test_str2object1/file1 deleted file mode 100644 index c6dc46576..000000000 --- a/e2e/convert_test/test_str2object1/file1 +++ /dev/null @@ -1,4 +0,0 @@ -"_id","_labels","a","b","prop","did_it","_start","_end","_type" -"0",":Dog","3","[""1"", ""2"", 3]","2","","","","" -"1",":Human","","[[""1"",{""key"": 5}]]","","yes","","","" -"5",":Cat","","{""prop"": {""prop2"": {""key"": 10}}}","","","0","1","loves" diff --git a/e2e/convert_test/test_str2object1/input.cyp b/e2e/convert_test/test_str2object1/input.cyp deleted file mode 100644 index e69de29bb..000000000 diff --git a/e2e/convert_test/test_str2object1/test.yml b/e2e/convert_test/test_str2object1/test.yml deleted file mode 100644 index 938976cbf..000000000 --- a/e2e/convert_test/test_str2object1/test.yml +++ /dev/null @@ -1,8 +0,0 @@ -query: > - LOAD CSV FROM "/_file1" WITH header AS row - RETURN convert.str2object(row.b) AS list; - -output: - - list: [ "1","2", 3] - - list: [["1",{"key": 5}]] - - list: {"prop": {"prop2": {"key": 10}}} diff --git a/e2e/node_test/test_degree_in1/input.cyp b/e2e/node_test/test_degree_in1/input.cyp new file mode 100644 index 000000000..3afd74f02 --- /dev/null +++ b/e2e/node_test/test_degree_in1/input.cyp @@ -0,0 +1 @@ +CREATE (d:Dog),(h:Human),(d)-[l:LOVES]->(h),(d)-[o:OWNED_BY]->(h); diff --git a/e2e/node_test/test_degree_in1/test.yml b/e2e/node_test/test_degree_in1/test.yml new file mode 100644 index 000000000..6bfbe5181 --- /dev/null +++ b/e2e/node_test/test_degree_in1/test.yml @@ -0,0 +1,5 @@ +query: > + MATCH (h:Human) RETURN node.degree_in(h, "LOVES") AS result; + +output: + - result: 1 diff --git a/e2e/node_test/test_degree_in2/input.cyp b/e2e/node_test/test_degree_in2/input.cyp new file mode 100644 index 000000000..3afd74f02 --- /dev/null +++ b/e2e/node_test/test_degree_in2/input.cyp @@ -0,0 +1 @@ +CREATE (d:Dog),(h:Human),(d)-[l:LOVES]->(h),(d)-[o:OWNED_BY]->(h); diff --git a/e2e/node_test/test_degree_in2/test.yml b/e2e/node_test/test_degree_in2/test.yml new file mode 100644 index 000000000..fb2dfeba3 --- /dev/null +++ b/e2e/node_test/test_degree_in2/test.yml @@ -0,0 +1,5 @@ +query: > + MATCH (h:Human) RETURN node.degree_in(h) AS result; + +output: + - result: 2 diff --git a/e2e/node_test/test_degree_in3/input.cyp b/e2e/node_test/test_degree_in3/input.cyp new file mode 100644 index 000000000..c0ed30ce8 --- /dev/null +++ b/e2e/node_test/test_degree_in3/input.cyp @@ -0,0 +1 @@ +MERGE (d:Dog {name: "Rex"}) WITH d UNWIND range(0, 1000) AS id MERGE (h:Human {name: "Humie" + id}) MERGE (h)-[l:LOVES]->(d); diff --git a/e2e/node_test/test_degree_in3/test.yml b/e2e/node_test/test_degree_in3/test.yml new file mode 100644 index 000000000..17dfea6b9 --- /dev/null +++ b/e2e/node_test/test_degree_in3/test.yml @@ -0,0 +1,5 @@ +query: > + MATCH (d:Dog) RETURN node.degree_in(d) AS result; + +output: + - result: 1001 diff --git a/e2e/node_test/test_degree_out1/input.cyp b/e2e/node_test/test_degree_out1/input.cyp new file mode 100644 index 000000000..3afd74f02 --- /dev/null +++ b/e2e/node_test/test_degree_out1/input.cyp @@ -0,0 +1 @@ +CREATE (d:Dog),(h:Human),(d)-[l:LOVES]->(h),(d)-[o:OWNED_BY]->(h); diff --git a/e2e/node_test/test_degree_out1/test.yml b/e2e/node_test/test_degree_out1/test.yml new file mode 100644 index 000000000..a98ecf5f9 --- /dev/null +++ b/e2e/node_test/test_degree_out1/test.yml @@ -0,0 +1,5 @@ +query: > + MATCH (d:Dog) RETURN node.degree_out(d, "LOVES") AS result; + +output: + - result: 1 diff --git a/e2e/node_test/test_degree_out2/input.cyp b/e2e/node_test/test_degree_out2/input.cyp new file mode 100644 index 000000000..3afd74f02 --- /dev/null +++ b/e2e/node_test/test_degree_out2/input.cyp @@ -0,0 +1 @@ +CREATE (d:Dog),(h:Human),(d)-[l:LOVES]->(h),(d)-[o:OWNED_BY]->(h); diff --git a/e2e/node_test/test_degree_out2/test.yml b/e2e/node_test/test_degree_out2/test.yml new file mode 100644 index 000000000..1ec1e4c4d --- /dev/null +++ b/e2e/node_test/test_degree_out2/test.yml @@ -0,0 +1,5 @@ +query: > + MATCH (d:Dog) RETURN node.degree_out(d) AS result; + +output: + - result: 2 diff --git a/e2e/node_test/test_degree_out3/input.cyp b/e2e/node_test/test_degree_out3/input.cyp new file mode 100644 index 000000000..65e216d59 --- /dev/null +++ b/e2e/node_test/test_degree_out3/input.cyp @@ -0,0 +1 @@ +MERGE (d:Dog {name: "Rex"}) WITH d UNWIND range(0, 1000) AS id MERGE (h:Human {name: "Humie" + id}) MERGE (d)-[l:LOVES]->(h); diff --git a/e2e/node_test/test_degree_out3/test.yml b/e2e/node_test/test_degree_out3/test.yml new file mode 100644 index 000000000..4d19cdba1 --- /dev/null +++ b/e2e/node_test/test_degree_out3/test.yml @@ -0,0 +1,5 @@ +query: > + MATCH (d:Dog) RETURN node.degree_out(d) AS result; + +output: + - result: 1001 diff --git a/e2e/path_test/test_elements_1/input.cyp b/e2e/path_test/test_elements_1/input.cyp new file mode 100644 index 000000000..2ebb0e8e5 --- /dev/null +++ b/e2e/path_test/test_elements_1/input.cyp @@ -0,0 +1 @@ +CREATE (:Node {id: 1})-[:CONNECTED {id: 1}]->(:Node {id: 2})-[:CONNECTED {id: 2}]->(:Node {id: 3})-[:CONNECTED {id: 3}]->(:Node {id: 4})-[:CONNECTED {id: 4}]->(:Node {id: 5}); diff --git a/e2e/path_test/test_elements_1/test.yml b/e2e/path_test/test_elements_1/test.yml new file mode 100644 index 000000000..d2a84083c --- /dev/null +++ b/e2e/path_test/test_elements_1/test.yml @@ -0,0 +1,37 @@ +query: > + MATCH path = (:Node {id: 1})-[:CONNECTED*4]->(:Node {id: 5}) RETURN + path.elements(path) AS result; +output: + - result: + - labels: + - Node + properties: + id: 1 + - label: CONNECTED + properties: + id: 1 + - labels: + - Node + properties: + id: 2 + - label: CONNECTED + properties: + id: 2 + - labels: + - Node + properties: + id: 3 + - label: CONNECTED + properties: + id: 3 + - labels: + - Node + properties: + id: 4 + - label: CONNECTED + properties: + id: 4 + - labels: + - Node + properties: + id: 5 diff --git a/e2e/path_test/test_elements_2/input.cyp b/e2e/path_test/test_elements_2/input.cyp new file mode 100644 index 000000000..01f3f5f8e --- /dev/null +++ b/e2e/path_test/test_elements_2/input.cyp @@ -0,0 +1 @@ +CREATE (:Node {id: 1})<-[:CONNECTED {id: 1}]-(:Node {id: 2})-[:CONNECTED {id: 2}]->(:Node {id: 3})<-[:CONNECTED {id: 3}]-(:Node {id: 4})-[:CONNECTED {id: 4}]->(:Node {id: 5}); diff --git a/e2e/path_test/test_elements_2/test.yml b/e2e/path_test/test_elements_2/test.yml new file mode 100644 index 000000000..1a2b5336b --- /dev/null +++ b/e2e/path_test/test_elements_2/test.yml @@ -0,0 +1,37 @@ +query: > + MATCH path = (:Node {id: 1})<-[:CONNECTED*4]->(:Node {id: 5}) RETURN + path.elements(path) AS result; +output: + - result: + - labels: + - Node + properties: + id: 1 + - label: CONNECTED + properties: + id: 1 + - labels: + - Node + properties: + id: 2 + - label: CONNECTED + properties: + id: 2 + - labels: + - Node + properties: + id: 3 + - label: CONNECTED + properties: + id: 3 + - labels: + - Node + properties: + id: 4 + - label: CONNECTED + properties: + id: 4 + - labels: + - Node + properties: + id: 5 diff --git a/e2e/path_test/test_expand1/test.yml b/e2e/path_test/test_expand1/test.yml index d0868a07b..345a0e85d 100644 --- a/e2e/path_test/test_expand1/test.yml +++ b/e2e/path_test/test_expand1/test.yml @@ -1,7 +1,17 @@ query: > - MATCH (d:Dog) - CALL path.expand(d,["HUNTS>"],[],0,3) YIELD result RETURN result; - - + MATCH (d:Dog) CALL path.expand(d,["HUNTS>"],[],1,3) YIELD result RETURN + result; output: - - result: {'nodes': [{'labels': ['Dog'],'properties': {'name': 'Rex'}},{'labels': ['Cat'],'properties': {'name': 'Tom'}}],'relationships': [{'label': 'HUNTS','properties': {}}]} + - result: + nodes: + - labels: + - Dog + properties: + name: Rex + - labels: + - Cat + properties: + name: Tom + relationships: + - label: HUNTS + properties: {} diff --git a/e2e/path_test/test_expand2/test.yml b/e2e/path_test/test_expand2/test.yml index 09531bcdc..24822e83e 100644 --- a/e2e/path_test/test_expand2/test.yml +++ b/e2e/path_test/test_expand2/test.yml @@ -1,9 +1,54 @@ query: > - MATCH (d:Dog) - CALL path.expand(d,[],["-Human","/Cat"],0,3) YIELD result RETURN result; - - + MATCH (d:Dog) CALL path.expand(d,[],["-Human","/Cat"],0,3) YIELD result RETURN + result; output: - - result: {'nodes': [{'labels': ['Dog'],'properties': {'name': 'Rex'}},{'labels': ['Cat'],'properties': {'name': 'Tom'}}],'relationships': [{'label': 'HUNTS','properties': {}}]} - - result: {'nodes': [{'labels': ['Dog'],'properties': {'name': 'Rex'}},{'labels': ['Cat'],'properties': {'name': 'Rinko'}}],'relationships': [{'label': 'BEST_FRIENDS','properties': {}}]} - - result: {'nodes': [{'labels': ['Dog'], 'properties': {'name': 'Rex'}}, {'labels': ['Zadar'], 'properties': {}}, {'labels': ['Mouse'], 'properties': {'name': 'Squiggles'}}, {'labels': ['Cat'],'properties': {'name': 'Tom'}}],'relationships': [{'label': 'LIVES', 'properties': {}}, {'label': 'RUNS_THROUGH','properties': {}},{'label': 'CATCHES','properties': {}}]} + - result: + nodes: + - labels: + - Dog + properties: + name: Rex + - labels: + - Cat + properties: + name: Tom + relationships: + - label: HUNTS + properties: {} + - result: + nodes: + - labels: + - Dog + properties: + name: Rex + - labels: + - Cat + properties: + name: Rinko + relationships: + - label: BEST_FRIENDS + properties: {} + - result: + nodes: + - labels: + - Dog + properties: + name: Rex + - labels: + - Zadar + properties: {} + - labels: + - Mouse + properties: + name: Squiggles + - labels: + - Cat + properties: + name: Tom + relationships: + - label: LIVES + properties: {} + - label: RUNS_THROUGH + properties: {} + - label: CATCHES + properties: {} diff --git a/e2e/path_test/test_expand3/test.yml b/e2e/path_test/test_expand3/test.yml index 906cbfafb..55e52c68f 100644 --- a/e2e/path_test/test_expand3/test.yml +++ b/e2e/path_test/test_expand3/test.yml @@ -1,7 +1,28 @@ query: > - MATCH (d:Dog) - CALL path.expand(d,[">","Zadar","/Human"],3,3) YIELD result RETURN result; - - + MATCH (d:Dog) CALL path.expand(d,[">","Zadar","/Human"],3,3) YIELD + result RETURN result; output: - - result: {'nodes': [{'labels': ['Dog'],'properties': {'name': 'Rex'}},{'labels': ['Cat'],'properties': {'name': 'Tom'}},{'labels': ['Mouse'],'properties': {'name': 'Squiggles'}},{'labels': ['Zadar'],'properties': {}}],'relationships': [{'label': 'HUNTS','properties': {}},{'label': 'CATCHES','properties': {}},{'label': 'RUNS_THROUGH', 'properties': {}}]} + - result: + nodes: + - labels: + - Dog + properties: + name: Rex + - labels: + - Cat + properties: + name: Tom + - labels: + - Mouse + properties: + name: Squiggles + - labels: + - Zadar + properties: {} + relationships: + - label: HUNTS + properties: {} + - label: CATCHES + properties: {} + - label: RUNS_THROUGH + properties: {} diff --git a/e2e/path_test/test_expand4/test.yml b/e2e/path_test/test_expand4/test.yml index d7ac17a95..70240e149 100644 --- a/e2e/path_test/test_expand4/test.yml +++ b/e2e/path_test/test_expand4/test.yml @@ -1,7 +1,5 @@ query: > - MATCH (d:Dog), (h:Human) - CALL path.expand([d,id(h)],[],[],1,10) YIELD result RETURN count(result) AS count; - - + MATCH (d:Dog), (h:Human) CALL path.expand([d,id(h)],[],[],1,10) YIELD result + RETURN count(result) AS count; output: - - count: 142 + - count: 142 diff --git a/e2e/path_test/test_subgraph_all_1/test.yml b/e2e/path_test/test_subgraph_all_1/test.yml index 3dbf02f48..dd7a5e6b3 100644 --- a/e2e/path_test/test_subgraph_all_1/test.yml +++ b/e2e/path_test/test_subgraph_all_1/test.yml @@ -9,6 +9,10 @@ query: > output: - nodes: + - labels: + - Operations + properties: + name: Jill - labels: - Research properties: @@ -17,10 +21,6 @@ output: - Operations properties: name: Steve - - labels: - - Operations - properties: - name: Jill rels: - label: FOLLOWS properties: {} diff --git a/e2e/path_test/test_subgraph_all_2/test.yml b/e2e/path_test/test_subgraph_all_2/test.yml index 769ce629d..8953f1f72 100644 --- a/e2e/path_test/test_subgraph_all_2/test.yml +++ b/e2e/path_test/test_subgraph_all_2/test.yml @@ -2,8 +2,8 @@ query: > MATCH (p:Strategy {name: "Amber"}) CALL path.subgraph_all(p, { relationshipFilter: [">"], - minLevel: 0, - maxLevel: 2 + minHops: 0, + maxHops: 2 }) YIELD nodes, rels RETURN nodes, rels; @@ -22,6 +22,10 @@ output: - Operations properties: name: Jill + - labels: + - Support + properties: + name: Jackson - labels: - Research properties: @@ -34,10 +38,6 @@ output: - Analytics properties: name: Zack - - labels: - - Support - properties: - name: Jackson - labels: - Support properties: @@ -57,10 +57,10 @@ output: properties: {} - label: KNOWS properties: {} - - label: FOLLOWS - properties: {} - label: KNOWS properties: {} + - label: FOLLOWS + properties: {} - label: KNOWS properties: {} - label: FOLLOWS diff --git a/e2e/path_test/test_subgraph_all_3/test.yml b/e2e/path_test/test_subgraph_all_3/test.yml index 3c2314384..e0b768077 100644 --- a/e2e/path_test/test_subgraph_all_3/test.yml +++ b/e2e/path_test/test_subgraph_all_3/test.yml @@ -17,15 +17,15 @@ output: - labels: - Node properties: - name: E + name: C - labels: - Node properties: - name: D + name: E - labels: - Node properties: - name: C + name: D rels: - label: CONNECTED_TO properties: {} diff --git a/e2e/path_test/test_subgraph_nodes_1/test.yml b/e2e/path_test/test_subgraph_nodes_1/test.yml index ccb8719bf..c12f07633 100644 --- a/e2e/path_test/test_subgraph_nodes_1/test.yml +++ b/e2e/path_test/test_subgraph_nodes_1/test.yml @@ -5,7 +5,7 @@ query: > labelFilter: ["+Strategy", "/Operations", ">Research", "-Support", "Analytics"] }) YIELD nodes - RETURN nodes; + RETURN nodes ORDER BY id(nodes) ASC output: - nodes: @@ -17,9 +17,9 @@ output: labels: - Operations properties: - name: Steve + name: Jill - nodes: labels: - Operations properties: - name: Jill + name: Steve diff --git a/e2e/path_test/test_subgraph_nodes_2/test.yml b/e2e/path_test/test_subgraph_nodes_2/test.yml index 5c19cdd5d..be72d00b1 100644 --- a/e2e/path_test/test_subgraph_nodes_2/test.yml +++ b/e2e/path_test/test_subgraph_nodes_2/test.yml @@ -2,43 +2,43 @@ query: > MATCH (p:Strategy {name: "Amber"}) CALL path.subgraph_nodes(p, { relationshipFilter: [">"], - minLevel: 0, - maxLevel: 2 + minHops: 0, + maxHops: 2 }) YIELD nodes - RETURN nodes; + RETURN nodes ORDER BY id(nodes); output: - nodes: labels: - - Strategy + - Research properties: - name: Amber + name: Matt - nodes: labels: - - Strategy + - Analytics properties: - name: Jacob + name: Patricia - nodes: labels: - - Operations + - Analytics properties: - name: Jill + name: Zack - nodes: labels: - - Research + - Operations properties: - name: Matt + name: Jill - nodes: labels: - - Analytics + - Strategy properties: - name: Patricia + name: Amber - nodes: labels: - - Analytics + - Strategy properties: - name: Zack + name: Jacob - nodes: labels: - Support diff --git a/e2e/path_test/test_subgraph_nodes_3/test.yml b/e2e/path_test/test_subgraph_nodes_3/test.yml index f4714ec9e..cf544bff1 100644 --- a/e2e/path_test/test_subgraph_nodes_3/test.yml +++ b/e2e/path_test/test_subgraph_nodes_3/test.yml @@ -2,7 +2,7 @@ query: > MATCH (a:Node {name: "A"}) CALL path.subgraph_nodes(a, {relationshipFilter: ['CONNECTED_TO>']}) YIELD nodes - RETURN nodes + RETURN nodes ORDER BY id(nodes) ASC; output: - nodes: @@ -19,7 +19,7 @@ output: labels: - Node properties: - name: E + name: C - nodes: labels: - Node @@ -29,4 +29,4 @@ output: labels: - Node properties: - name: C + name: E diff --git a/e2e/schema_test/test_node_type_properties1/input.cyp b/e2e/schema_test/test_node_type_properties1/input.cyp deleted file mode 100644 index 949f01eca..000000000 --- a/e2e/schema_test/test_node_type_properties1/input.cyp +++ /dev/null @@ -1 +0,0 @@ -CREATE (d:Dog {name: "Rex", owner: "Carl"})-[l:LOVES]->(a:Activity {name: "Running", location: "Zadar"}); diff --git a/e2e/schema_test/test_node_type_properties1/test.yml b/e2e/schema_test/test_node_type_properties1/test.yml deleted file mode 100644 index 1b42de473..000000000 --- a/e2e/schema_test/test_node_type_properties1/test.yml +++ /dev/null @@ -1,31 +0,0 @@ -query: > - CALL schema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory - RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory - ORDER BY propertyName, nodeLabels[0]; - -output: - - - nodeType: ":`Activity`" - nodeLabels: ["Activity"] - propertyName: location - propertyTypes: ["String"] - mandatory: true - - - nodeType: ":`Activity`" - nodeLabels: ["Activity"] - propertyName: name - propertyTypes: ["String"] - mandatory: true - - - nodeType: ":`Dog`" - nodeLabels: ["Dog"] - propertyName: name - propertyTypes: ["String"] - mandatory: true - - - nodeType: ":`Dog`" - nodeLabels: ["Dog"] - propertyName: owner - propertyTypes: ["String"] - mandatory: true - diff --git a/e2e/schema_test/test_node_type_properties2/input.cyp b/e2e/schema_test/test_node_type_properties2/input.cyp deleted file mode 100644 index d20df7ae0..000000000 --- a/e2e/schema_test/test_node_type_properties2/input.cyp +++ /dev/null @@ -1 +0,0 @@ -CREATE (n:NonMandatory)-[r:RELATIONSHIP]->(m:Mandatory {property: ["2",3 ,4]}); diff --git a/e2e/schema_test/test_node_type_properties2/test.yml b/e2e/schema_test/test_node_type_properties2/test.yml deleted file mode 100644 index cb4034acd..000000000 --- a/e2e/schema_test/test_node_type_properties2/test.yml +++ /dev/null @@ -1,18 +0,0 @@ -query: > - CALL schema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory - RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory - ORDER BY propertyName, nodeLabels[0]; - -output: - - - nodeType: ":`NonMandatory`" - nodeLabels: ["NonMandatory"] - propertyName: '' - propertyTypes: '' - mandatory: false - - - nodeType: ":`Mandatory`" - nodeLabels: ["Mandatory"] - propertyName: property - propertyTypes: ["List[Any]"] - mandatory: true diff --git a/e2e/schema_test/test_node_type_properties3/input.cyp b/e2e/schema_test/test_node_type_properties3/input.cyp deleted file mode 100644 index 5b5aa2a36..000000000 --- a/e2e/schema_test/test_node_type_properties3/input.cyp +++ /dev/null @@ -1 +0,0 @@ -CREATE (n:NonMandatory:SecondLabel)-[r:RELATIONSHIP]->(m:Mandatory {property: ["2",3 ,4]}); diff --git a/e2e/schema_test/test_node_type_properties3/test.yml b/e2e/schema_test/test_node_type_properties3/test.yml deleted file mode 100644 index 1e5861182..000000000 --- a/e2e/schema_test/test_node_type_properties3/test.yml +++ /dev/null @@ -1,14 +0,0 @@ -query: > - CALL schema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory - WITH nodeType, nodeLabels, propertyName, propertyTypes , mandatory - WHERE propertyName = "" - RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory - -output: - - - nodeType: ":`NonMandatory`:`SecondLabel`" - nodeLabels: ["NonMandatory", "SecondLabel"] - propertyName: '' - propertyTypes: '' - mandatory: false - diff --git a/e2e/schema_test/test_rel_type_properties1/input.cyp b/e2e/schema_test/test_rel_type_properties1/input.cyp deleted file mode 100644 index 949f01eca..000000000 --- a/e2e/schema_test/test_rel_type_properties1/input.cyp +++ /dev/null @@ -1 +0,0 @@ -CREATE (d:Dog {name: "Rex", owner: "Carl"})-[l:LOVES]->(a:Activity {name: "Running", location: "Zadar"}); diff --git a/e2e/schema_test/test_rel_type_properties1/test.yml b/e2e/schema_test/test_rel_type_properties1/test.yml deleted file mode 100644 index e67f19054..000000000 --- a/e2e/schema_test/test_rel_type_properties1/test.yml +++ /dev/null @@ -1,8 +0,0 @@ -query: > - CALL schema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory; - -output: - - relType: ":`LOVES`" - propertyName: "" - propertyTypes: "" - mandatory: false diff --git a/e2e/schema_test/test_rel_type_properties2/input.cyp b/e2e/schema_test/test_rel_type_properties2/input.cyp deleted file mode 100644 index 122e1b04b..000000000 --- a/e2e/schema_test/test_rel_type_properties2/input.cyp +++ /dev/null @@ -1 +0,0 @@ -CREATE (d:Dog)-[c:CATCHES {property: [1,2,3], property2: {key: "value"}}]->(b:Ball)-[f:FALLS_FROM {speed_in_km: 100}]->(ba:Balcony); diff --git a/e2e/schema_test/test_rel_type_properties2/test.yml b/e2e/schema_test/test_rel_type_properties2/test.yml deleted file mode 100644 index 26b15f0ed..000000000 --- a/e2e/schema_test/test_rel_type_properties2/test.yml +++ /dev/null @@ -1,19 +0,0 @@ -query: > - CALL schema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory - ORDER BY propertyName, relType; - -output: - - relType: ":`CATCHES`" - propertyName: property - propertyTypes: ["List[Any]"] - mandatory: true - - - relType: ":`CATCHES`" - propertyName: property2 - propertyTypes: ["Map[Any]"] - mandatory: true - - - relType: ":`FALLS_FROM`" - propertyName: speed_in_km - propertyTypes: ["Int"] - mandatory: true diff --git a/e2e/schema_test/test_rel_type_properties3/input.cyp b/e2e/schema_test/test_rel_type_properties3/input.cyp deleted file mode 100644 index cdce0b8e1..000000000 --- a/e2e/schema_test/test_rel_type_properties3/input.cyp +++ /dev/null @@ -1,2 +0,0 @@ -CREATE (d:Dog)-[c:CATCHES {property: [1,2,3], property2: {key: "value"}}]->(b:Ball)-[f:FALLS_FROM {speed_in_km: 100}]->(ba:Balcony); -CREATE (b:Bird)-[f:FLIES]->(s:Sky); diff --git a/e2e/schema_test/test_rel_type_properties3/test.yml b/e2e/schema_test/test_rel_type_properties3/test.yml deleted file mode 100644 index e7365f186..000000000 --- a/e2e/schema_test/test_rel_type_properties3/test.yml +++ /dev/null @@ -1,24 +0,0 @@ -query: > - CALL schema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory - ORDER BY propertyName, relType; -output: - - - relType: ":`FLIES`" - propertyName: "" - propertyTypes: "" - mandatory: false - - - relType: ":`CATCHES`" - propertyName: property - propertyTypes: ["List[Any]"] - mandatory: true - - - relType: ":`CATCHES`" - propertyName: property2 - propertyTypes: ["Map[Any]"] - mandatory: true - - - relType: ":`FALLS_FROM`" - propertyName: speed_in_km - propertyTypes: ["Int"] - mandatory: true diff --git a/e2e_correctness/conftest.py b/e2e_correctness/conftest.py index 11f3aae60..79533a42f 100644 --- a/e2e_correctness/conftest.py +++ b/e2e_correctness/conftest.py @@ -1,5 +1,3 @@ - - def pytest_addoption(parser): parser.addoption("--memgraph-port", type=int, action="store") parser.addoption("--neo4j-port", type=int, action="store") diff --git a/e2e_correctness/path_test/test_combine_1/config.yml b/e2e_correctness/path_test/test_combine_1/config.yml new file mode 100644 index 000000000..3c7888e88 --- /dev/null +++ b/e2e_correctness/path_test/test_combine_1/config.yml @@ -0,0 +1,2 @@ +path_option: > + True diff --git a/e2e_correctness/path_test/test_combine_1/input.cyp b/e2e_correctness/path_test/test_combine_1/input.cyp new file mode 100644 index 000000000..09d857655 --- /dev/null +++ b/e2e_correctness/path_test/test_combine_1/input.cyp @@ -0,0 +1 @@ +CREATE (:Node {name: 'Node 1', id:1})-[:CONNECTED {id:1}]->(:Node {name: 'Node 2', id:2})-[:CONNECTED {id:2}]->(:Node {name: 'Node 3', id:3})-[:CONNECTED {id:3}]->(:Node {name: 'Node 4', id:4})-[:CONNECTED {id:4}]->(:Node {name: 'Node 5', id:5})-[:CONNECTED {id:5}]->(:Node {name: 'Node 6', id:6}); diff --git a/e2e_correctness/path_test/test_combine_1/test.yml b/e2e_correctness/path_test/test_combine_1/test.yml new file mode 100644 index 000000000..ea84a6bfb --- /dev/null +++ b/e2e_correctness/path_test/test_combine_1/test.yml @@ -0,0 +1,10 @@ +memgraph_query: > + MATCH (node1:Node {name: 'Node 1'}), (node4:Node {name: 'Node 4'}), (node6:Node {name: 'Node 6'}) + MATCH path1 = (node1)-[:CONNECTED*3]->(node4) + MATCH path2 = (node4)-[:CONNECTED*2]->(node6) + RETURN path.combine(path1, path2) AS result; +neo4j_query: > + MATCH (node1:Node {name: 'Node 1'}), (node4:Node {name: 'Node 4'}), (node6:Node {name: 'Node 6'}) + MATCH path1 = (node1)-[:CONNECTED*3]->(node4) + MATCH path2 = (node4)-[:CONNECTED*2]->(node6) + RETURN apoc.path.combine(path1, path2) AS result; diff --git a/e2e_correctness/path_test/test_combine_2/config.yml b/e2e_correctness/path_test/test_combine_2/config.yml new file mode 100644 index 000000000..3c7888e88 --- /dev/null +++ b/e2e_correctness/path_test/test_combine_2/config.yml @@ -0,0 +1,2 @@ +path_option: > + True diff --git a/e2e_correctness/path_test/test_combine_2/input.cyp b/e2e_correctness/path_test/test_combine_2/input.cyp new file mode 100644 index 000000000..179230911 --- /dev/null +++ b/e2e_correctness/path_test/test_combine_2/input.cyp @@ -0,0 +1 @@ +CREATE (:Node {name: 'Node 1', id:1})<-[:CONNECTED {id:1}]-(:Node {name: 'Node 2', id:2})-[:CONNECTED {id:2}]->(:Node {name: 'Node 3', id:3})-[:CONNECTED {id:3}]->(:Node {name: 'Node 4', id:4})<-[:CONNECTED {id:4}]-(:Node {name: 'Node 5', id:5})-[:CONNECTED {id:5}]->(:Node {name: 'Node 6', id:6}); diff --git a/e2e_correctness/path_test/test_combine_2/test.yml b/e2e_correctness/path_test/test_combine_2/test.yml new file mode 100644 index 000000000..ea84a6bfb --- /dev/null +++ b/e2e_correctness/path_test/test_combine_2/test.yml @@ -0,0 +1,10 @@ +memgraph_query: > + MATCH (node1:Node {name: 'Node 1'}), (node4:Node {name: 'Node 4'}), (node6:Node {name: 'Node 6'}) + MATCH path1 = (node1)-[:CONNECTED*3]->(node4) + MATCH path2 = (node4)-[:CONNECTED*2]->(node6) + RETURN path.combine(path1, path2) AS result; +neo4j_query: > + MATCH (node1:Node {name: 'Node 1'}), (node4:Node {name: 'Node 4'}), (node6:Node {name: 'Node 6'}) + MATCH path1 = (node1)-[:CONNECTED*3]->(node4) + MATCH path2 = (node4)-[:CONNECTED*2]->(node6) + RETURN apoc.path.combine(path1, path2) AS result; diff --git a/e2e_correctness/path_test/test_path_expand1/config.yml b/e2e_correctness/path_test/test_path_expand1/config.yml new file mode 100644 index 000000000..173fc413a --- /dev/null +++ b/e2e_correctness/path_test/test_path_expand1/config.yml @@ -0,0 +1,2 @@ +path_option: > + True diff --git a/e2e_correctness/path_test/test_path_expand1/input.cyp b/e2e_correctness/path_test/test_path_expand1/input.cyp new file mode 100644 index 000000000..8d7b1ed1c --- /dev/null +++ b/e2e_correctness/path_test/test_path_expand1/input.cyp @@ -0,0 +1,7 @@ +CREATE (d:Dog {name: "Rex", id: 0})-[h:HUNTS {id: 0}]->(c:Cat {name: "Tom", id: 1})-[catc:CATCHES {id:1}]->(m:Mouse {name: "Squiggles", id: 2}); +MATCH (d:Dog) CREATE (d)-[bff:BEST_FRIENDS {id:2}]->(c:Cat {name: "Rinko", id: 3}); +MATCH (c:Cat {name: "Tom"}) CREATE (h:Human {name: "Matija", id: 4})-[o:OWNS {id:3}]->(c); +MATCH (d:Dog {name: "Rex"}),(h:Human {name:"Matija"}) CREATE (h)-[o:PLAYS_WITH {id:4}]->(d); +MATCH (d:Dog {name: "Rex"}) CREATE (d)-[l:LIVES {id:5}]->(z:Zadar {id: 5}); +MATCH (m:Mouse {name: "Squiggles"}), (z:Zadar) CREATE (m)-[r:RUNS_THROUGH {id:6}]->(z); +MATCH (z:Zadar) CREATE (h:Human {name: "Dena", id: 6})-[g:GOES_TO {id:7}]->(z); diff --git a/e2e_correctness/path_test/test_path_expand1/test.yml b/e2e_correctness/path_test/test_path_expand1/test.yml new file mode 100644 index 000000000..c0260c510 --- /dev/null +++ b/e2e_correctness/path_test/test_path_expand1/test.yml @@ -0,0 +1,8 @@ +memgraph_query: > + MATCH (d:Dog), (h:Human) + CALL path.expand([d,id(h)],[],[],1,10) YIELD result RETURN result; + + +neo4j_query: > + MATCH (d:Dog), (h:Human) + CALL apoc.path.expand([d,h],"","",1,10) YIELD path RETURN path; diff --git a/e2e_correctness/path_test/test_slice_1/config.yml b/e2e_correctness/path_test/test_slice_1/config.yml new file mode 100644 index 000000000..3c7888e88 --- /dev/null +++ b/e2e_correctness/path_test/test_slice_1/config.yml @@ -0,0 +1,2 @@ +path_option: > + True diff --git a/e2e_correctness/path_test/test_slice_1/input.cyp b/e2e_correctness/path_test/test_slice_1/input.cyp new file mode 100644 index 000000000..2ebb0e8e5 --- /dev/null +++ b/e2e_correctness/path_test/test_slice_1/input.cyp @@ -0,0 +1 @@ +CREATE (:Node {id: 1})-[:CONNECTED {id: 1}]->(:Node {id: 2})-[:CONNECTED {id: 2}]->(:Node {id: 3})-[:CONNECTED {id: 3}]->(:Node {id: 4})-[:CONNECTED {id: 4}]->(:Node {id: 5}); diff --git a/e2e_correctness/path_test/test_slice_1/test.yml b/e2e_correctness/path_test/test_slice_1/test.yml new file mode 100644 index 000000000..8ac153e29 --- /dev/null +++ b/e2e_correctness/path_test/test_slice_1/test.yml @@ -0,0 +1,6 @@ +memgraph_query: > + MATCH path = (:Node {id: 1})-[:CONNECTED*4]->(:Node {id: 5}) + RETURN path.slice(path, 1, -1) AS result; +neo4j_query: > + MATCH path = (:Node {id: 1})-[:CONNECTED*4]->(:Node {id: 5}) + RETURN apoc.path.slice(path, 1, -1) AS result; diff --git a/e2e_correctness/path_test/test_slice_2/config.yml b/e2e_correctness/path_test/test_slice_2/config.yml new file mode 100644 index 000000000..3c7888e88 --- /dev/null +++ b/e2e_correctness/path_test/test_slice_2/config.yml @@ -0,0 +1,2 @@ +path_option: > + True diff --git a/e2e_correctness/path_test/test_slice_2/input.cyp b/e2e_correctness/path_test/test_slice_2/input.cyp new file mode 100644 index 000000000..2ebb0e8e5 --- /dev/null +++ b/e2e_correctness/path_test/test_slice_2/input.cyp @@ -0,0 +1 @@ +CREATE (:Node {id: 1})-[:CONNECTED {id: 1}]->(:Node {id: 2})-[:CONNECTED {id: 2}]->(:Node {id: 3})-[:CONNECTED {id: 3}]->(:Node {id: 4})-[:CONNECTED {id: 4}]->(:Node {id: 5}); diff --git a/e2e_correctness/path_test/test_slice_2/test.yml b/e2e_correctness/path_test/test_slice_2/test.yml new file mode 100644 index 000000000..91c4c0ab5 --- /dev/null +++ b/e2e_correctness/path_test/test_slice_2/test.yml @@ -0,0 +1,6 @@ +memgraph_query: > + MATCH path = (:Node {id: 1})-[:CONNECTED*4]->(:Node {id: 5}) + RETURN path.slice(path, 2, 2) AS result; +neo4j_query: > + MATCH path = (:Node {id: 1})-[:CONNECTED*4]->(:Node {id: 5}) + RETURN apoc.path.slice(path, 2, 2) AS result; diff --git a/e2e_correctness/path_test/test_slice_3/config.yml b/e2e_correctness/path_test/test_slice_3/config.yml new file mode 100644 index 000000000..3c7888e88 --- /dev/null +++ b/e2e_correctness/path_test/test_slice_3/config.yml @@ -0,0 +1,2 @@ +path_option: > + True diff --git a/e2e_correctness/path_test/test_slice_3/input.cyp b/e2e_correctness/path_test/test_slice_3/input.cyp new file mode 100644 index 000000000..2ebb0e8e5 --- /dev/null +++ b/e2e_correctness/path_test/test_slice_3/input.cyp @@ -0,0 +1 @@ +CREATE (:Node {id: 1})-[:CONNECTED {id: 1}]->(:Node {id: 2})-[:CONNECTED {id: 2}]->(:Node {id: 3})-[:CONNECTED {id: 3}]->(:Node {id: 4})-[:CONNECTED {id: 4}]->(:Node {id: 5}); diff --git a/e2e_correctness/path_test/test_slice_3/test.yml b/e2e_correctness/path_test/test_slice_3/test.yml new file mode 100644 index 000000000..4b7537851 --- /dev/null +++ b/e2e_correctness/path_test/test_slice_3/test.yml @@ -0,0 +1,6 @@ +memgraph_query: > + MATCH path = (:Node {id: 1})-[:CONNECTED*4]->(:Node {id: 5}) + RETURN path.slice(path, 1, 123) AS result; +neo4j_query: > + MATCH path = (:Node {id: 1})-[:CONNECTED*4]->(:Node {id: 5}) + RETURN apoc.path.slice(path, 1, 123) AS result; diff --git a/e2e_correctness/query_neo_mem.py b/e2e_correctness/query_neo_mem.py index 0650600ad..7bab351a9 100644 --- a/e2e_correctness/query_neo_mem.py +++ b/e2e_correctness/query_neo_mem.py @@ -74,7 +74,11 @@ def __eq__(self, other): class Edge: def __init__( - self, from_vertex: int, to_vertex: int, label: str, properties: Dict[str, Any] + self, + from_vertex: int, + to_vertex: int, + label: str, + properties: Dict[str, Any], ): self._from_vertex = from_vertex self._to_vertex = to_vertex @@ -297,3 +301,83 @@ def neo4j_get_graph(neo4j_driver: neo4j.BoltDriver) -> Graph: json_data = get_neo4j_data_json(neo4j_driver) logger.debug("Building the graph from Neo4j JSON data") return create_graph_neo4j_json(json_data) + + +# additions for path testing +def sort_dict(dict): + keys = list(dict.keys()) + keys.sort() + sorted_dict = {i: dict[i] for i in keys} + return sorted_dict + + +def execute_query_neo4j(driver: neo4j.BoltDriver, query: str) -> list: + with driver.session() as session: + query = neo4j.Query(query) + results = session.run(query).value() + return results + + +def path_to_string_neo4j(path): #type should be neo4j.graph.path but it doesnt recognize it in the definition + path_string_list = ["PATH: "] + + n = len(path.nodes) + + for i in range(0, n): + node = path.nodes[i] + node_labels = list(node.labels) + node_labels.sort() + sorted_dict = sort_dict(node._properties) + if "id" in sorted_dict: + sorted_dict.pop("id") + node_props = str(sorted_dict) + path_string_list.append(f"(id:{str(node.get('id'))} labels: {str(node_labels)} {node_props})-") + + if i == n - 1: + path_string = "".join(path_string_list) + return path_string[:-1] + + relationship = path.relationships[i] + sorted_dict_rel = sort_dict(relationship._properties) + if "id" in sorted_dict_rel: + sorted_dict_rel.pop("id") + rel_props = str(sorted_dict_rel) + path_string_list.append(f"[id:{str(relationship.get('id'))} type: {relationship.type} {str(rel_props)}]-") + +def parse_neo4j(results: list) -> List[str]: + paths = [path_to_string_neo4j(res) for res in results] + paths.sort() + return paths + + +def path_to_string_mem(path: gqlalchemy.Path) -> str: + path_string_list = ["PATH: "] + + n = len(path._nodes) + + for i in range(0, n): + node = path._nodes[i] + node_labels = list(node._labels) + node_labels.sort() + sorted_dict = sort_dict(node._properties) + if "id" in sorted_dict: + sorted_dict.pop("id") + node_props = str(sorted_dict) + path_string_list.append(f"(id:{str(node._properties.get('id'))} labels: {str(node_labels)} {str(node_props)})-") + + if i == n - 1: + path_string = "".join(path_string_list) + return path_string[:-1] + + relationship = path._relationships[i] + sorted_dict_rel = sort_dict(relationship._properties) + if "id" in sorted_dict_rel: + sorted_dict_rel.pop("id") + rel_props = str(sorted_dict_rel) + path_string_list.append(f"[id:{str(relationship._properties.get('id'))} type: {relationship._type} {str(rel_props)}]-") + + +def parse_mem(results:list) -> List[str]: + paths = [path_to_string_mem(result["result"]) for result in results] + paths.sort() + return paths diff --git a/e2e_correctness/test_modules.py b/e2e_correctness/test_modules.py index 9357ed24b..9bf223c73 100644 --- a/e2e_correctness/test_modules.py +++ b/e2e_correctness/test_modules.py @@ -6,6 +6,7 @@ import neo4j import pytest import yaml +import os from gqlalchemy import Memgraph @@ -25,6 +26,11 @@ neo4j_get_graph, run_memgraph_query, run_neo4j_query, + execute_query_neo4j, + path_to_string_neo4j, + parse_neo4j, + path_to_string_mem, + parse_mem, ) logging.basicConfig(format="%(asctime)-15s [%(levelname)s]: %(message)s") @@ -49,6 +55,8 @@ class TestConstants: TEST_FILE = "test.yml" MEMGRAPH_QUERY = "memgraph_query" NEO4J_QUERY = "neo4j_query" + CONFIG_FILE = "config.yml" + class ConfigConstants: @@ -79,7 +87,9 @@ def get_all_tests(): if not test_or_group_dir.is_dir(): continue - if test_or_group_dir.name.endswith(TestConstants.TEST_GROUP_DIR_SUFFIX): + if test_or_group_dir.name.endswith( + TestConstants.TEST_GROUP_DIR_SUFFIX + ): for test_dir in test_or_group_dir.iterdir(): if not test_dir.is_dir(): continue @@ -143,11 +153,15 @@ def _graphs_equal(memgraph_graph: Graph, neo4j_graph: Graph) -> bool: return True -def _run_test(test_dir: str, memgraph_db: Memgraph, neo4j_driver: neo4j.BoltDriver): +def _run_test( + test_dir: str, memgraph_db: Memgraph, neo4j_driver: neo4j.BoltDriver +) -> None: """ Run input queries on Memgraph and Neo4j and compare graphs after running test query """ - input_cyphers = test_dir.joinpath(TestConstants.INPUT_FILE).open("r").readlines() + input_cyphers = ( + test_dir.joinpath(TestConstants.INPUT_FILE).open("r").readlines() + ) mg_execute_cyphers(input_cyphers, memgraph_db) logger.info(f"Imported data into Memgraph from {input_cyphers}") neo4j_execute_cyphers(input_cyphers, neo4j_driver) @@ -162,7 +176,9 @@ def _run_test(test_dir: str, memgraph_db: Memgraph, neo4j_driver: neo4j.BoltDriv run_memgraph_query(test_dict[TestConstants.MEMGRAPH_QUERY], memgraph_db) logger.info("Done") - logger.info(f"Running query against Neo4j: {test_dict[TestConstants.NEO4J_QUERY]}") + logger.info( + f"Running query against Neo4j: {test_dict[TestConstants.NEO4J_QUERY]}" + ) run_neo4j_query(test_dict[TestConstants.NEO4J_QUERY], neo4j_driver) logger.info("Done") @@ -174,11 +190,57 @@ def _run_test(test_dir: str, memgraph_db: Memgraph, neo4j_driver: neo4j.BoltDriv ), "The graphs are not equal, check the logs for more details" +def _run_path_test( + test_dir: str, memgraph_db: Memgraph, neo4j_driver: neo4j.BoltDriver +) -> None: + """ + Run input queries on Memgraph and Neo4j and compare path results after running test query + """ + input_cyphers = ( + test_dir.joinpath(TestConstants.INPUT_FILE).open("r").readlines() + ) + logger.info(f"Importing data from {input_cyphers}") + mg_execute_cyphers(input_cyphers, memgraph_db) + logger.info("Imported data into Memgraph") + neo4j_execute_cyphers(input_cyphers, neo4j_driver) + logger.info("Imported data into Neo4j") + + test_dict = _load_yaml(test_dir.joinpath(TestConstants.TEST_FILE)) + logger.info(f"Test dict {test_dict}") + logger.info( + f"Running query against Memgraph: {test_dict[TestConstants.MEMGRAPH_QUERY]}" + ) + memgraph_results = memgraph_db.execute_and_fetch( + test_dict[TestConstants.MEMGRAPH_QUERY] + ) + memgraph_paths = parse_mem(memgraph_results) + logger.info("Done") + + logger.info( + f"Running query against Neo4j: {test_dict[TestConstants.NEO4J_QUERY]}" + ) + neo4j_results = execute_query_neo4j( + neo4j_driver, test_dict[TestConstants.NEO4J_QUERY] + ) + neo4j_paths = parse_neo4j(neo4j_results) + logger.info("Done") + + assert memgraph_paths == neo4j_paths + +def check_path_option(test_dir): + config_path = test_dir.joinpath(TestConstants.CONFIG_FILE) + if(os.path.exists(config_path)): + config_dict = _load_yaml(config_path) + if "path_option" in config_dict: + option = config_dict["path_option"].strip() + return ( option == "True") + return False @pytest.fixture(scope="session") def memgraph_port(pytestconfig): return pytestconfig.getoption("--memgraph-port") + @pytest.fixture(scope="session", autouse=True) def memgraph_db(memgraph_port): memgraph_db = create_memgraph_db(memgraph_port) @@ -191,6 +253,7 @@ def memgraph_db(memgraph_port): def neo4j_port(pytestconfig): return pytestconfig.getoption("--neo4j-port") + @pytest.fixture(scope="session", autouse=True) def neo4j_driver(neo4j_port): neo4j_driver = create_neo4j_driver(neo4j_port) @@ -201,7 +264,9 @@ def neo4j_driver(neo4j_port): @pytest.mark.parametrize("test_dir", tests) def test_end2end( - test_dir: Path, memgraph_db: Memgraph, neo4j_driver: neo4j.BoltDriver + test_dir: Path, + memgraph_db: Memgraph, + neo4j_driver: neo4j.BoltDriver, ): logger.debug("Dropping the Memgraph and Neo4j databases.") @@ -209,7 +274,11 @@ def test_end2end( clean_neo4j_db(neo4j_driver) if test_dir.name.startswith(TestConstants.TEST_SUBDIR_PREFIX): - _run_test(test_dir, memgraph_db, neo4j_driver) + + if check_path_option(test_dir): + _run_path_test(test_dir, memgraph_db, neo4j_driver) + else: + _run_test(test_dir, memgraph_db, neo4j_driver) else: logger.info(f"Skipping directory: {test_dir.name}") diff --git a/python/convert.py b/python/convert.py deleted file mode 100644 index 6db93c8c6..000000000 --- a/python/convert.py +++ /dev/null @@ -1,9 +0,0 @@ -import mgp -from json import loads - - -@mgp.function -def str2object(string: str) -> mgp.Any: - if string: - return loads(string) - return None diff --git a/python/mgps.py b/python/mgps.py deleted file mode 100644 index 14b18d7d4..000000000 --- a/python/mgps.py +++ /dev/null @@ -1,8 +0,0 @@ -import mgp - - -@mgp.read_proc -def components( - context: mgp.ProcCtx, -) -> mgp.Record(versions=list, edition=str, name=str): - return mgp.Record(versions=["5.9.0"], edition="community", name="Memgraph") diff --git a/test_e2e_correctness.py b/test_e2e_correctness.py index 10ba141a7..ff1c52c81 100644 --- a/test_e2e_correctness.py +++ b/test_e2e_correctness.py @@ -12,18 +12,26 @@ class ConfigConstants: NEO4J_PORT = 7688 MEMGRAPH_PORT = 7687 + def parse_arguments(): - parser = argparse.ArgumentParser( - description="Test MAGE E2E correctness." - ) + parser = argparse.ArgumentParser(description="Test MAGE E2E correctness.") parser.add_argument( - "-k", help="Filter what tests you want to run", type=str, required=False + "-k", + help="Filter what tests you want to run", + type=str, + required=False, ) parser.add_argument( - "--memgraph-port", help="Set the port that Memgraph is listening on", type=int, required=False + "--memgraph-port", + help="Set the port that Memgraph is listening on", + type=int, + required=False, ) parser.add_argument( - "--neo4j-port", help="Set the port that Neo4j is listening on", type=int, required=False + "--neo4j-port", + help="Set the port that Neo4j is listening on", + type=int, + required=False, ) args = parser.parse_args() return args @@ -34,18 +42,21 @@ def parse_arguments(): ################################################# -def main(test_filter: str = None, - memgraph_port:str = str(ConfigConstants.MEMGRAPH_PORT), - neo4j_port: str = str(ConfigConstants.NEO4J_PORT)): +def main( + test_filter: str = None, + memgraph_port: str = str(ConfigConstants.MEMGRAPH_PORT), + neo4j_port: str = str(ConfigConstants.NEO4J_PORT), +): os.environ["PYTHONPATH"] = E2E_CORRECTNESS_DIRECTORY os.chdir(E2E_CORRECTNESS_DIRECTORY) command = ["python3", "-m", "pytest", ".", "-vv"] if test_filter: command.extend(["-k", test_filter]) - command.extend(["--memgraph-port", memgraph_port]) command.extend(["--neo4j-port", neo4j_port]) + + subprocess.run(command) @@ -59,7 +70,9 @@ def main(test_filter: str = None, memgraph_port = str(memgraph_port) if neo4j_port: neo4j_port = str(neo4j_port) - - main(test_filter=test_filter, - memgraph_port=memgraph_port, - neo4j_port=neo4j_port) + + main( + test_filter=test_filter, + memgraph_port=memgraph_port, + neo4j_port=neo4j_port, + )