diff --git a/query_modules/schema.cpp b/query_modules/schema.cpp index 1b3035bab..848ccedc4 100644 --- a/query_modules/schema.cpp +++ b/query_modules/schema.cpp @@ -108,31 +108,83 @@ void Schema::ProcessPropertiesRel(mgp::Record &record, const std::string_view &t record.Insert(std::string(kReturnMandatory).c_str(), mandatory); } +struct Property { + std::string name; + mgp::Value value; + + Property(const std::string &name, mgp::Value &&value) : name(name), value(std::move(value)) {} +}; + +struct LabelsHash { + std::size_t operator()(const std::set &set) const { + std::size_t seed = set.size(); + for (const auto &i : set) { + seed ^= std::hash{}(i) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } + return seed; + } +}; + +struct LabelsComparator { + bool operator()(const std::set &lhs, const std::set &rhs) const { return lhs == rhs; } +}; + +struct PropertyComparator { + bool operator()(const Property &lhs, const Property &rhs) const { return lhs.name < rhs.name; } +}; + +struct PropertyInfo { + std::set properties; + bool 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(); + std::unordered_map, PropertyInfo, LabelsHash, LabelsComparator> node_types_properties; + + for (auto node : mgp::Graph(memgraph_graph).Nodes()) { + std::set labels_set = {}; for (auto label : node.Labels()) { - labels.AppendExtend(mgp::Value(label)); - type += ":`" + std::string(label) + "`"; + labels_set.emplace(label); + } + + if (node_types_properties.find(labels_set) == node_types_properties.end()) { + node_types_properties[labels_set] = PropertyInfo{std::set(), true}; } if (node.Properties().empty()) { - auto record = record_factory.NewRecord(); - ProcessPropertiesNode(record, type, labels, "", "", false); + node_types_properties[labels_set].mandatory = false; // if there is node with no property, it is not mandatory continue; } + auto &property_info = node_types_properties.at(labels_set); for (auto &[key, prop] : node.Properties()) { - auto property_type = mgp::List(); + property_info.properties.emplace(key, std::move(prop)); + if (property_info.mandatory) { + property_info.mandatory = + property_info.properties.size() == 1; // if there is only one property, it is mandatory + } + } + } + + for (auto &[labels, property_info] : node_types_properties) { + std::string label_type; + mgp::List labels_list = mgp::List(); + for (auto const &label : labels) { + label_type += ":`" + std::string(label) + "`"; + labels_list.AppendExtend(mgp::Value(label)); + } + for (auto const &prop : property_info.properties) { auto record = record_factory.NewRecord(); - property_type.AppendExtend(mgp::Value(TypeOf(prop.Type()))); - ProcessPropertiesNode(record, type, labels, key, property_type, true); + ProcessPropertiesNode(record, label_type, labels_list, prop.name, TypeOf(prop.value.Type()), + property_info.mandatory); + } + if (property_info.properties.empty()) { + auto record = record_factory.NewRecord(); + ProcessPropertiesNode(record, label_type, labels_list, "", "", false); } } @@ -144,23 +196,41 @@ void Schema::NodeTypeProperties(mgp_list * /*args*/, mgp_graph *memgraph_graph, void Schema::RelTypeProperties(mgp_list * /*args*/, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { mgp::MemoryDispatcherGuard guard{memory}; + + std::unordered_map rel_types_properties; 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()) + "`"; + std::string rel_type = std::string(rel.Type()); + if (rel_types_properties.find(rel_type) == rel_types_properties.end()) { + rel_types_properties[rel_type] = PropertyInfo{std::set(), true}; + } + if (rel.Properties().empty()) { - auto record = record_factory.NewRecord(); - ProcessPropertiesRel(record, type, "", "", false); + rel_types_properties[rel_type].mandatory = false; // if there is rel with no property, it is not mandatory continue; } + auto &property_info = rel_types_properties.at(rel_type); for (auto &[key, prop] : rel.Properties()) { - auto property_type = mgp::List(); + property_info.properties.emplace(key, std::move(prop)); + if (property_info.mandatory) { + property_info.mandatory = + property_info.properties.size() == 1; // if there is only one property, it is mandatory + } + } + } + + for (auto &[type, property_info] : rel_types_properties) { + std::string type_str = ":`" + std::string(type) + "`"; + for (auto const &prop : property_info.properties) { auto record = record_factory.NewRecord(); - property_type.AppendExtend(mgp::Value(TypeOf(prop.Type()))); - ProcessPropertiesRel(record, type, key, property_type, true); + ProcessPropertiesRel(record, type_str, prop.name, TypeOf(prop.value.Type()), property_info.mandatory); + } + if (property_info.properties.empty()) { + auto record = record_factory.NewRecord(); + ProcessPropertiesRel(record, type_str, "", "", false); } } diff --git a/tests/e2e/query_modules/schema_test.py b/tests/e2e/query_modules/schema_test.py index 515514a74..fbb376a22 100644 --- a/tests/e2e/query_modules/schema_test.py +++ b/tests/e2e/query_modules/schema_test.py @@ -431,7 +431,7 @@ def test_node_type_properties1(): f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];", )[0] ) - assert (result) == [":`Activity`", ["Activity"], "location", ["String"], True] + assert (result) == [":`Activity`", ["Activity"], "location", "String", False] result = list( execute_and_fetch_all( @@ -439,7 +439,7 @@ def test_node_type_properties1(): f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];", )[1] ) - assert (result) == [":`Activity`", ["Activity"], "name", ["String"], True] + assert (result) == [":`Activity`", ["Activity"], "name", "String", False] result = list( execute_and_fetch_all( @@ -447,7 +447,7 @@ def test_node_type_properties1(): f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];", )[2] ) - assert (result) == [":`Dog`", ["Dog"], "name", ["String"], True] + assert (result) == [":`Dog`", ["Dog"], "name", "String", False] result = list( execute_and_fetch_all( @@ -455,7 +455,81 @@ def test_node_type_properties1(): f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];", )[3] ) - assert (result) == [":`Dog`", ["Dog"], "owner", ["String"], True] + assert (result) == [":`Dog`", ["Dog"], "owner", "String", False] + + +def test_node_type_properties2(): + cursor = connect().cursor() + execute_and_fetch_all( + cursor, + """ + CREATE (d:MyNode) + CREATE (n:MyNode) + """, + ) + result = execute_and_fetch_all( + cursor, + f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];", + ) + assert (list(result[0])) == [":`MyNode`", ["MyNode"], "", "", False] + assert (result.__len__()) == 1 + + +def test_node_type_properties3(): + cursor = connect().cursor() + execute_and_fetch_all( + cursor, + """ + CREATE (d:Dog {name: 'Rex', owner: 'Carl'}) + CREATE (n:Dog) + """, + ) + result = execute_and_fetch_all( + cursor, + f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];", + ) + + assert (list(result[0])) == [":`Dog`", ["Dog"], "name", "String", False] + assert (list(result[1])) == [":`Dog`", ["Dog"], "owner", "String", False] + assert (result.__len__()) == 2 + + +def test_node_type_properties4(): + cursor = connect().cursor() + execute_and_fetch_all( + cursor, + """ + CREATE (n:Label1:Label2 {property1: 'value1', property2: 'value2'}) + CREATE (m:Label2:Label1 {property3: 'value3'}) + """, + ) + result = list( + execute_and_fetch_all( + cursor, + f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];", + ) + ) + assert (list(result[0])) == [":`Label1`:`Label2`", ["Label1", "Label2"], "property1", "String", False] + assert (list(result[1])) == [":`Label1`:`Label2`", ["Label1", "Label2"], "property2", "String", False] + assert (list(result[2])) == [":`Label1`:`Label2`", ["Label1", "Label2"], "property3", "String", False] + assert (result.__len__()) == 3 + + +def test_node_type_properties5(): + cursor = connect().cursor() + execute_and_fetch_all( + cursor, + """ + CREATE (d:Dog {name: 'Rex'}) + """, + ) + result = execute_and_fetch_all( + cursor, + f"CALL libschema.node_type_properties() YIELD nodeType, nodeLabels, propertyName, propertyTypes , mandatory RETURN nodeType, nodeLabels, propertyName, propertyTypes , mandatory ORDER BY propertyName, nodeLabels[0];", + ) + + assert (list(result[0])) == [":`Dog`", ["Dog"], "name", "String", True] + assert (result.__len__()) == 1 def test_rel_type_properties1(): @@ -473,5 +547,38 @@ def test_rel_type_properties1(): assert (result) == [":`LOVES`", "", "", False] +def test_rel_type_properties2(): + cursor = connect().cursor() + execute_and_fetch_all( + cursor, + """ + CREATE (d:Dog {name: 'Rex', owner: 'Carl'})-[l:LOVES]->(a:Activity {name: 'Running', location: 'Zadar'}) + CREATE (n:Dog {name: 'Simba', owner: 'Lucy'})-[j:LOVES {duration: 30}]->(b:Activity {name: 'Running', location: 'Zadar'}) + """, + ) + result = execute_and_fetch_all( + cursor, + f"CALL libschema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory;", + ) + assert (list(result[0])) == [":`LOVES`", "duration", "Int", False] + assert (result.__len__()) == 1 + + +def test_rel_type_properties3(): + cursor = connect().cursor() + execute_and_fetch_all( + cursor, + """ + CREATE (n:Dog {name: 'Simba', owner: 'Lucy'})-[j:LOVES {duration: 30}]->(b:Activity {name: 'Running', location: 'Zadar'}) + """, + ) + result = execute_and_fetch_all( + cursor, + f"CALL libschema.rel_type_properties() YIELD relType,propertyName, propertyTypes , mandatory RETURN relType, propertyName, propertyTypes , mandatory;", + ) + assert (list(result[0])) == [":`LOVES`", "duration", "Int", True] + assert (result.__len__()) == 1 + + if __name__ == "__main__": sys.exit(pytest.main([__file__, "-rA"]))