From 4d8f8be1ab900cd0d955ecefc3b2b85dd27c1aba Mon Sep 17 00:00:00 2001 From: Tonko Sabolcec <tonko.sabolcec@memgraph.io> Date: Tue, 7 May 2019 15:10:28 +0200 Subject: [PATCH] Add function that dumps vertices and edges of the graph. Summary: Fix lint errors Reviewers: teon.banek Reviewed By: teon.banek Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D1996 --- src/CMakeLists.txt | 1 + src/database/single_node/dump.cpp | 127 +++++++++ src/database/single_node/dump.hpp | 18 ++ .../interpret/awesome_memgraph_functions.cpp | 16 ++ tests/unit/CMakeLists.txt | 3 + tests/unit/database_dump.cpp | 260 ++++++++++++++++++ 6 files changed, 425 insertions(+) create mode 100644 src/database/single_node/dump.cpp create mode 100644 src/database/single_node/dump.hpp create mode 100644 tests/unit/database_dump.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 172a2a6fd..6561c64b0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,6 +19,7 @@ set(mg_single_node_sources audit/log.cpp data_structures/concurrent/skiplist_gc.cpp database/single_node/config.cpp + database/single_node/dump.cpp database/single_node/graph_db.cpp database/single_node/graph_db_accessor.cpp durability/single_node/state_delta.cpp diff --git a/src/database/single_node/dump.cpp b/src/database/single_node/dump.cpp new file mode 100644 index 000000000..ce3d13bb0 --- /dev/null +++ b/src/database/single_node/dump.cpp @@ -0,0 +1,127 @@ +#include "database/single_node/dump.hpp" + +#include <map> +#include <ostream> +#include <utility> +#include <vector> + +#include <glog/logging.h> + +#include "database/graph_db_accessor.hpp" +#include "utils/algorithm.hpp" +#include "utils/string.hpp" + +namespace database { + +namespace { + +void DumpPropertyValue(std::ostream *os, const PropertyValue &value) { + switch (value.type()) { + case PropertyValue::Type::Null: + *os << "Null"; + return; + case PropertyValue::Type::Bool: + *os << (value.Value<bool>() ? "true" : "false"); + return; + case PropertyValue::Type::String: + *os << ::utils::Escape(value.Value<std::string>()); + return; + case PropertyValue::Type::Int: + *os << value.Value<int64_t>(); + return; + case PropertyValue::Type::Double: + // TODO(tsabolcec): By default, this will output only 6 significant digits + // of the number. We should increase that number to avoid precision loss. + *os << value.Value<double>(); + return; + case PropertyValue::Type::List: { + *os << "["; + const auto &list = value.Value<std::vector<PropertyValue>>(); + utils::PrintIterable(*os, list, ", ", [](auto &os, const auto &item) { + DumpPropertyValue(&os, item); + }); + *os << "]"; + return; + } + case PropertyValue::Type::Map: { + *os << "{"; + const auto &map = value.Value<std::map<std::string, PropertyValue>>(); + utils::PrintIterable(*os, map, ", ", [](auto &os, const auto &kv) { + os << kv.first << ": "; + DumpPropertyValue(&os, kv.second); + }); + *os << "}"; + return; + } + } +} + +void DumpProperties(std::ostream *os, GraphDbAccessor *dba, + const PropertyValueStore &store) { + *os << "{"; + utils::PrintIterable(*os, store, ", ", [&dba](auto &os, const auto &kv) { + os << dba->PropertyName(kv.first) << ": "; + DumpPropertyValue(&os, kv.second); + }); + *os << "}"; +} + +void DumpVertex(std::ostream *os, GraphDbAccessor *dba, + const VertexAccessor &vertex) { + *os << "(n" << vertex.gid(); + for (const auto &label : vertex.labels()) { + *os << ":" << dba->LabelName(label); + } + const auto &props = vertex.Properties(); + if (props.size() > 0) { + *os << " "; + DumpProperties(os, dba, props); + } + *os << ")"; +} + +void DumpVertices(std::ostream *os, GraphDbAccessor *dba) { + auto vertices = dba->Vertices(false); + utils::PrintIterable( + os, vertices.begin(), vertices.end(), ", ", + [&dba](auto &os, const auto &vertex) { DumpVertex(&os, dba, vertex); }); +} + +void DumpEdge(std::ostream *os, GraphDbAccessor *dba, + const EdgeAccessor &edge) { + *os << "(n" << edge.from().gid() << ")-["; + *os << ":" << dba->EdgeTypeName(edge.EdgeType()); + const auto &props = edge.Properties(); + if (props.size() > 0) { + *os << " "; + DumpProperties(os, dba, props); + } + *os << "]->(n" << edge.to().gid() << ")"; +} + +void DumpEdges(std::ostream *os, GraphDbAccessor *dba) { + auto edges = dba->Edges(false); + utils::PrintIterable( + os, edges.begin(), edges.end(), ", ", + [&dba](auto &os, const auto &edge) { DumpEdge(&os, dba, edge); }); +} + +} // namespace + +void DumpToCypher(std::ostream *os, GraphDbAccessor *dba) { + CHECK(os); + CHECK(dba); + + if (dba->VerticesCount() > 0) { + *os << "CREATE "; + DumpVertices(os, dba); + if (dba->EdgesCount() > 0) { + *os << ", "; + DumpEdges(os, dba); + } + *os << ";"; + } + // TODO(tsabolcec): Dump other data as well. +} + +} // namespace database diff --git a/src/database/single_node/dump.hpp b/src/database/single_node/dump.hpp new file mode 100644 index 000000000..e2480204c --- /dev/null +++ b/src/database/single_node/dump.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include <ostream> + +#include "database/graph_db_accessor.hpp" + +namespace database { + +/// Dumps database state to output stream as openCypher queries. +/// +/// Currently, it only dumps vertices and edges of the graph. In the future, +/// it should also dump indexes, constraints, roles, etc. +/// +/// @param os Output stream +/// @param dba Database accessor +void DumpToCypher(std::ostream *os, GraphDbAccessor *dba); + +} // namespace database diff --git a/src/query/interpret/awesome_memgraph_functions.cpp b/src/query/interpret/awesome_memgraph_functions.cpp index a14205201..dd14cc3af 100644 --- a/src/query/interpret/awesome_memgraph_functions.cpp +++ b/src/query/interpret/awesome_memgraph_functions.cpp @@ -6,6 +6,7 @@ #include <functional> #include <random> +#include "database/single_node/dump.hpp" #include "query/context.hpp" #include "query/exceptions.hpp" #include "utils/string.hpp" @@ -895,6 +896,18 @@ TypedValue Substring(TypedValue *args, int64_t nargs, const EvaluationContext &, return start < str.size() ? str.substr(start, len) : ""; } +#if MG_SINGLE_NODE +TypedValue Dump(TypedValue *args, int64_t nargs, const EvaluationContext &, + database::GraphDbAccessor *dba) { + if (nargs != 0) { + throw QueryRuntimeException("'dump' does not expect any arguments."); + } + std::ostringstream oss; + database::DumpToCypher(&oss, dba); + return oss.str(); +} +#endif // MG_SINGLE_NODE + } // namespace std::function<TypedValue(TypedValue *, int64_t, const EvaluationContext &, @@ -972,6 +985,9 @@ NameToFunction(const std::string &function_name) { if (function_name == "ASSERT") return Assert; if (function_name == "COUNTER") return Counter; if (function_name == "COUNTERSET") return CounterSet; +#ifdef MG_SINGLE_NODE + if (function_name == "DUMP") return Dump; +#endif #ifdef MG_DISTRIBUTED if (function_name == "WORKERID") return WorkerId; #endif diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 27ad4f069..5bed9fe15 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -45,6 +45,9 @@ target_link_libraries(${test_prefix}counters mg-distributed kvstore_dummy_lib) add_unit_test(cypher_main_visitor.cpp) target_link_libraries(${test_prefix}cypher_main_visitor mg-single-node kvstore_dummy_lib) +add_unit_test(database_dump.cpp) +target_link_libraries(${test_prefix}database_dump mg-single-node kvstore_dummy_lib) + add_unit_test(database_key_index.cpp) target_link_libraries(${test_prefix}database_key_index mg-single-node kvstore_dummy_lib) diff --git a/tests/unit/database_dump.cpp b/tests/unit/database_dump.cpp new file mode 100644 index 000000000..c7824f832 --- /dev/null +++ b/tests/unit/database_dump.cpp @@ -0,0 +1,260 @@ +#include <gtest/gtest.h> + +#include <map> +#include <set> +#include <vector> + +#include <glog/logging.h> + +#include "communication/result_stream_faker.hpp" +#include "database/graph_db.hpp" +#include "database/graph_db_accessor.hpp" +#include "database/single_node/dump.hpp" +#include "query/interpreter.hpp" +#include "query/typed_value.hpp" +#include "storage/common/types/property_value.hpp" + +const char *kPropertyId = "property_id"; + +// A helper struct that contains info about database that is used to compare +// two databases (to check if their states are the same). It is assumed that +// each vertex and each edge have unique integer property under key +// `kPropertyId`. +struct DatabaseState { + struct Vertex { + int64_t id; + std::set<std::string> labels; + std::map<std::string, PropertyValue> props; + }; + + struct Edge { + int64_t from, to; + std::string edge_type; + std::map<std::string, PropertyValue> props; + }; + + std::set<Vertex> vertices; + std::set<Edge> edges; +}; + +bool operator<(const DatabaseState::Vertex &first, + const DatabaseState::Vertex &second) { + if (first.id != second.id) return first.id < second.id; + if (first.labels != second.labels) return first.labels < second.labels; + return first.props < second.props; +} + +bool operator<(const DatabaseState::Edge &first, + const DatabaseState::Edge &second) { + if (first.from != second.from) return first.from < second.from; + if (first.to != second.to) return first.to < second.to; + if (first.edge_type != second.edge_type) + return first.edge_type < second.edge_type; + return first.props < second.props; +} + +bool operator==(const DatabaseState::Vertex &first, + const DatabaseState::Vertex &second) { + return first.id == second.id && first.labels == second.labels && + first.props == second.props; +} + +bool operator==(const DatabaseState::Edge &first, + const DatabaseState::Edge &second) { + return first.from == second.from && first.to == second.to && + first.edge_type == second.edge_type && first.props == second.props; +} + +bool operator==(const DatabaseState &first, const DatabaseState &second) { + return first.vertices == second.vertices && first.edges == second.edges; +} + +class DatabaseEnvironment { + public: + std::string DumpStr() { + auto dba = db_.Access(); + std::ostringstream oss; + database::DumpToCypher(&oss, &dba); + return oss.str(); + } + + void Execute(const std::string &query) { + auto dba = db_.Access(); + ResultStreamFaker<query::TypedValue> results; + query::Interpreter()(query, dba, {}, false).PullAll(results); + dba.Commit(); + } + + VertexAccessor CreateVertex(const std::vector<std::string> &labels, + const std::map<std::string, PropertyValue> &props, + bool add_property_id = true) { + auto dba = db_.Access(); + auto vertex = dba.InsertVertex(); + for (const auto &label_name : labels) { + vertex.add_label(dba.Label(label_name)); + } + for (const auto &kv : props) { + vertex.PropsSet(dba.Property(kv.first), kv.second); + } + if (add_property_id) { + vertex.PropsSet(dba.Property(kPropertyId), + PropertyValue(static_cast<int64_t>(vertex.gid()))); + } + dba.Commit(); + return vertex; + } + + EdgeAccessor CreateEdge(VertexAccessor from, VertexAccessor to, + const std::string &edge_type_name, + const std::map<std::string, PropertyValue> &props, + bool add_property_id = true) { + auto dba = db_.Access(); + auto edge = dba.InsertEdge(from, to, dba.EdgeType(edge_type_name)); + for (const auto &kv : props) { + edge.PropsSet(dba.Property(kv.first), kv.second); + } + if (add_property_id) { + edge.PropsSet(dba.Property(kPropertyId), + PropertyValue(static_cast<int64_t>(edge.gid()))); + } + dba.Commit(); + return edge; + } + + DatabaseState GetState() { + // Capture all vertices + std::map<gid::Gid, int64_t> gid_mapping; + std::set<DatabaseState::Vertex> vertices; + auto dba = db_.Access(); + for (const auto &vertex : dba.Vertices(false)) { + std::set<std::string> labels; + for (const auto &label : vertex.labels()) { + labels.insert(dba.LabelName(label)); + } + std::map<std::string, PropertyValue> props; + for (const auto &kv : vertex.Properties()) { + props.emplace(dba.PropertyName(kv.first), kv.second); + } + CHECK(props.count(kPropertyId) == 1); + const auto id = props[kPropertyId].Value<int64_t>(); + gid_mapping[vertex.gid()] = id; + vertices.insert({id, labels, props}); + } + + // Capture all edges + std::set<DatabaseState::Edge> edges; + for (const auto &edge : dba.Edges(false)) { + const auto &edge_type_name = dba.EdgeTypeName(edge.EdgeType()); + std::map<std::string, PropertyValue> props; + for (const auto &kv : edge.Properties()) { + props.emplace(dba.PropertyName(kv.first), kv.second); + } + const auto from = gid_mapping[edge.from().gid()]; + const auto to = gid_mapping[edge.to().gid()]; + edges.insert({from, to, edge_type_name, props}); + } + + return {vertices, edges}; + } + + private: + database::GraphDb db_; +}; + +TEST(DumpTest, EmptyGraph) { + DatabaseEnvironment db; + EXPECT_EQ("", db.DumpStr()); +} + +TEST(DumpTest, SingleVertex) { + DatabaseEnvironment db; + db.CreateVertex({}, {}, false); + EXPECT_EQ(db.DumpStr(), "CREATE (n0);"); +} + +TEST(DumpTest, VertexWithSingleLabel) { + DatabaseEnvironment db; + db.CreateVertex({"Label1"}, {}, false); + EXPECT_EQ(db.DumpStr(), "CREATE (n0:Label1);"); +} + +TEST(DumpTest, VertexWithMultipleLabels) { + DatabaseEnvironment db; + db.CreateVertex({"Label1", "Label2"}, {}, false); + EXPECT_EQ(db.DumpStr(), "CREATE (n0:Label1:Label2);"); +} + +TEST(DumpTest, VertexWithSingleProperty) { + DatabaseEnvironment db; + db.CreateVertex({}, {{"prop", PropertyValue(42)}}, false); + EXPECT_EQ(db.DumpStr(), "CREATE (n0 {prop: 42});"); +} + +TEST(DumpTest, MultipleVertices) { + DatabaseEnvironment db; + db.CreateVertex({}, {}, false); + db.CreateVertex({}, {}, false); + db.CreateVertex({}, {}, false); + EXPECT_EQ(db.DumpStr(), "CREATE (n0), (n1), (n2);"); +} + +TEST(DumpTest, SingleEdge) { + DatabaseEnvironment db; + auto u = db.CreateVertex({}, {}, false); + auto v = db.CreateVertex({}, {}, false); + db.CreateEdge(u, v, "EdgeType", {}, false); + EXPECT_EQ(db.DumpStr(), "CREATE (n0), (n1), (n0)-[:EdgeType]->(n1);"); +} + +TEST(DumpTest, MultipleEdges) { + DatabaseEnvironment db; + auto u = db.CreateVertex({}, {}, false); + auto v = db.CreateVertex({}, {}, false); + auto w = db.CreateVertex({}, {}, false); + db.CreateEdge(u, v, "EdgeType", {}, false); + db.CreateEdge(v, u, "EdgeType", {}, false); + db.CreateEdge(v, w, "EdgeType", {}, false); + const char *expected = + "CREATE (n0), (n1), (n2), (n0)-[:EdgeType]->(n1), " + "(n1)-[:EdgeType]->(n0), (n1)-[:EdgeType]->(n2);"; + EXPECT_EQ(db.DumpStr(), expected); +} + +TEST(DumpTest, EdgeWithProperties) { + DatabaseEnvironment db; + auto u = db.CreateVertex({}, {}, false); + auto v = db.CreateVertex({}, {}, false); + db.CreateEdge(u, v, "EdgeType", {{"prop", PropertyValue(13)}}, false); + EXPECT_EQ(db.DumpStr(), + "CREATE (n0), (n1), (n0)-[:EdgeType {prop: 13}]->(n1);"); +} + +TEST(DumpTest, CheckStateVertexWithMultipleProperties) { + DatabaseEnvironment db; + std::map<std::string, PropertyValue> prop1 = { + {"nested1", PropertyValue(1337)}, {"nested2", PropertyValue(3.14)}}; + db.CreateVertex({"Label1", "Label2"}, + {{"prop1", prop1}, {"prop2", PropertyValue("$'\t'")}}); + DatabaseEnvironment db_dump; + db_dump.Execute(db.DumpStr()); + EXPECT_EQ(db.GetState(), db_dump.GetState()); +} + +TEST(DumpTest, CheckStateSimpleGraph) { + DatabaseEnvironment db; + auto u = db.CreateVertex({"Person"}, {{"name", "Ivan"}}); + auto v = db.CreateVertex({"Person"}, {{"name", "Josko"}}); + auto w = db.CreateVertex({"Person"}, {{"name", "Bosko"}}); + auto z = db.CreateVertex({"Person"}, {{"name", "Buha"}}); + db.CreateEdge(u, v, "Knows", {}); + db.CreateEdge(v, w, "Knows", {{"how_long", 5}}); + db.CreateEdge(w, u, "Knows", {{"how", "distant past"}}); + db.CreateEdge(v, u, "Knows", {}); + db.CreateEdge(v, u, "Likes", {}); + db.CreateEdge(z, u, "Knows", {}); + db.CreateEdge(w, z, "Knows", {{"how", "school"}}); + db.CreateEdge(w, z, "Likes", {{"how", "very much"}}); + DatabaseEnvironment db_dump; + db_dump.Execute(db.DumpStr()); + EXPECT_EQ(db.GetState(), db_dump.GetState()); +}