#include #include #include #include #include #include #include "database/single_node/graph_db.hpp" #include "database/single_node/graph_db_accessor.hpp" #include "utils/bound.hpp" using testing::UnorderedElementsAreArray; template auto Count(TIterable iterable) { return std::distance(iterable.begin(), iterable.end()); } /** * A test fixture that contains a database, accessor, * label, property and an edge_type. */ class GraphDbAccessorIndex : public testing::Test { protected: database::GraphDb db; database::GraphDbAccessor dba{db.Access()}; storage::Property property = dba.Property("property"); storage::Label label = dba.Label("label"); storage::EdgeType edge_type = dba.EdgeType("edge_type"); auto AddVertex() { auto vertex = dba.InsertVertex(); vertex.add_label(label); return vertex; } auto AddVertex(int property_value) { auto vertex = dba.InsertVertex(); vertex.add_label(label); vertex.PropsSet(property, property_value); return vertex; } // commits the current dba, and replaces it with a new one void Commit() { dba.Commit(); dba = db.Access(); } }; TEST_F(GraphDbAccessorIndex, LabelIndexCount) { auto label2 = dba.Label("label2"); EXPECT_EQ(dba.VerticesCount(label), 0); EXPECT_EQ(dba.VerticesCount(label2), 0); EXPECT_EQ(dba.VerticesCount(), 0); for (int i = 0; i < 11; ++i) dba.InsertVertex().add_label(label); for (int i = 0; i < 17; ++i) dba.InsertVertex().add_label(label2); // even though xxx_count functions in database::GraphDbAccessor can // over-estaimate in this situation they should be exact (nothing was ever // deleted) EXPECT_EQ(dba.VerticesCount(label), 11); EXPECT_EQ(dba.VerticesCount(label2), 17); EXPECT_EQ(dba.VerticesCount(), 28); } TEST_F(GraphDbAccessorIndex, LabelIndexIteration) { // add 10 vertices, check visibility for (int i = 0; i < 10; i++) AddVertex(); EXPECT_EQ(Count(dba.Vertices(label, false)), 0); EXPECT_EQ(Count(dba.Vertices(label, true)), 10); Commit(); EXPECT_EQ(Count(dba.Vertices(label, false)), 10); EXPECT_EQ(Count(dba.Vertices(label, true)), 10); // remove 3 vertices, check visibility int deleted = 0; for (auto vertex : dba.Vertices(false)) { dba.RemoveVertex(vertex); if (++deleted >= 3) break; } EXPECT_EQ(Count(dba.Vertices(label, false)), 10); EXPECT_EQ(Count(dba.Vertices(label, true)), 7); Commit(); EXPECT_EQ(Count(dba.Vertices(label, false)), 7); EXPECT_EQ(Count(dba.Vertices(label, true)), 7); } TEST_F(GraphDbAccessorIndex, EdgesCount) { auto edge_type2 = dba.EdgeType("edge_type2"); EXPECT_EQ(dba.EdgesCount(), 0); auto v1 = AddVertex(); auto v2 = AddVertex(); for (int i = 0; i < 11; ++i) dba.InsertEdge(v1, v2, edge_type); for (int i = 0; i < 17; ++i) dba.InsertEdge(v1, v2, edge_type2); // even though xxx_count functions in database::GraphDbAccessor can // over-estaimate in this situation they should be exact (nothing was ever // deleted) EXPECT_EQ(dba.EdgesCount(), 28); } TEST_F(GraphDbAccessorIndex, LabelPropertyIndexBuild) { AddVertex(0); Commit(); dba.BuildIndex(label, property); Commit(); EXPECT_EQ(dba.VerticesCount(label, property), 1); // confirm there is a differentiation of indexes based on (label, property) auto label2 = dba.Label("label2"); auto property2 = dba.Property("property2"); dba.BuildIndex(label2, property); dba.BuildIndex(label, property2); Commit(); EXPECT_EQ(dba.VerticesCount(label, property), 1); EXPECT_EQ(dba.VerticesCount(label2, property), 0); EXPECT_EQ(dba.VerticesCount(label, property2), 0); } TEST_F(GraphDbAccessorIndex, LabelPropertyIndexDelete) { dba.BuildIndex(label, property); Commit(); EXPECT_TRUE(dba.LabelPropertyIndexExists(label, property)); dba.DeleteIndex(label, property); Commit(); EXPECT_FALSE(dba.LabelPropertyIndexExists(label, property)); } TEST_F(GraphDbAccessorIndex, LabelPropertyIndexBuildTwice) { dba.BuildIndex(label, property); EXPECT_THROW(dba.BuildIndex(label, property), utils::BasicException); } TEST_F(GraphDbAccessorIndex, LabelPropertyIndexCount) { dba.BuildIndex(label, property); EXPECT_EQ(dba.VerticesCount(label, property), 0); EXPECT_EQ(Count(dba.Vertices(label, property, true)), 0); for (int i = 0; i < 14; ++i) AddVertex(0); EXPECT_EQ(dba.VerticesCount(label, property), 14); EXPECT_EQ(Count(dba.Vertices(label, property, true)), 14); } TEST(GraphDbAccessorIndexApi, LabelPropertyBuildIndexConcurrent) { const int THREAD_COUNT = 10; std::vector threads; database::GraphDb db; std::atomic failed{false}; for (int index = 0; index < THREAD_COUNT; ++index) { threads.emplace_back([&db, &failed, index]() { // If we fail to create a new transaction, don't bother. try { auto dba = db.Access(); try { // This could either pass or throw. dba.BuildIndex(dba.Label("l" + std::to_string(index)), dba.Property("p" + std::to_string(index))); // If it throws, make sure the exception is right. } catch (const database::TransactionException &e) { // Nothing to see here, move along. } catch (...) { failed.store(true); } } catch (...) { // Ignore this one also. } }); } for (auto &thread : threads) { if (thread.joinable()) { thread.join(); } } EXPECT_FALSE(failed.load()); } #define EXPECT_WITH_MARGIN(x, center) \ EXPECT_THAT( \ x, testing::AllOf(testing::Ge(center - 2), testing::Le(center + 2))); TEST_F(GraphDbAccessorIndex, LabelPropertyValueCount) { dba.BuildIndex(label, property); // add some vertices without the property for (int i = 0; i < 20; i++) AddVertex(); // add vertices with prop values [0, 29), ten vertices for each value for (int i = 0; i < 300; i++) AddVertex(i / 10); // add verties in t he [30, 40) range, 100 vertices for each value for (int i = 0; i < 1000; i++) AddVertex(30 + i / 100); // test estimates for exact value count EXPECT_WITH_MARGIN(dba.VerticesCount(label, property, 10), 10); EXPECT_WITH_MARGIN(dba.VerticesCount(label, property, 14), 10); EXPECT_WITH_MARGIN(dba.VerticesCount(label, property, 30), 100); EXPECT_WITH_MARGIN(dba.VerticesCount(label, property, 39), 100); EXPECT_EQ(dba.VerticesCount(label, property, 40), 0); // helper functions auto Inclusive = [](int64_t value) { return std::make_optional(utils::MakeBoundInclusive(PropertyValue(value))); }; auto Exclusive = [](int64_t value) { return std::make_optional(utils::MakeBoundExclusive(PropertyValue(value))); }; auto VerticesCount = [this](auto lower, auto upper) { return dba.VerticesCount(label, property, lower, upper); }; using std::nullopt; ::testing::FLAGS_gtest_death_test_style = "threadsafe"; EXPECT_DEATH(VerticesCount(nullopt, nullopt), "bound must be provided"); EXPECT_WITH_MARGIN(VerticesCount(nullopt, Exclusive(4)), 40); EXPECT_WITH_MARGIN(VerticesCount(nullopt, Inclusive(4)), 50); EXPECT_WITH_MARGIN(VerticesCount(Exclusive(13), nullopt), 160 + 1000); EXPECT_WITH_MARGIN(VerticesCount(Inclusive(13), nullopt), 170 + 1000); EXPECT_WITH_MARGIN(VerticesCount(Inclusive(13), Exclusive(14)), 10); EXPECT_WITH_MARGIN(VerticesCount(Exclusive(13), Inclusive(14)), 10); EXPECT_WITH_MARGIN(VerticesCount(Exclusive(13), Exclusive(13)), 0); EXPECT_WITH_MARGIN(VerticesCount(Inclusive(20), Exclusive(13)), 0); } #undef EXPECT_WITH_MARGIN TEST_F(GraphDbAccessorIndex, LabelPropertyValueIteration) { dba.BuildIndex(label, property); Commit(); // insert 10 verties and and check visibility for (int i = 0; i < 10; i++) AddVertex(12); EXPECT_EQ(Count(dba.Vertices(label, property, 12, false)), 0); EXPECT_EQ(Count(dba.Vertices(label, property, 12, true)), 10); Commit(); EXPECT_EQ(Count(dba.Vertices(label, property, 12, false)), 10); EXPECT_EQ(Count(dba.Vertices(label, property, 12, true)), 10); } TEST_F(GraphDbAccessorIndex, LabelPropertyValueSorting) { dba.BuildIndex(label, property); Commit(); std::vector expected_property_value(50, 0); // strings for (int i = 0; i < 10; ++i) { auto vertex_accessor = dba.InsertVertex(); vertex_accessor.add_label(label); vertex_accessor.PropsSet(property, static_cast(std::to_string(i))); expected_property_value[i] = vertex_accessor.PropsAt(property); } // bools - insert in reverse to check for comparison between values. for (int i = 9; i >= 0; --i) { auto vertex_accessor = dba.InsertVertex(); vertex_accessor.add_label(label); vertex_accessor.PropsSet(property, static_cast(i / 5)); expected_property_value[10 + i] = vertex_accessor.PropsAt(property); } // integers for (int i = 0; i < 10; ++i) { auto vertex_accessor = dba.InsertVertex(); vertex_accessor.add_label(label); vertex_accessor.PropsSet(property, i); expected_property_value[20 + 2 * i] = vertex_accessor.PropsAt(property); } // doubles for (int i = 0; i < 10; ++i) { auto vertex_accessor = dba.InsertVertex(); vertex_accessor.add_label(label); vertex_accessor.PropsSet(property, static_cast(i + 0.5)); expected_property_value[20 + 2 * i + 1] = vertex_accessor.PropsAt(property); } // lists of ints - insert in reverse to check for comparision between // lists. for (int i = 9; i >= 0; --i) { auto vertex_accessor = dba.InsertVertex(); vertex_accessor.add_label(label); std::vector value; value.push_back(PropertyValue(i)); vertex_accessor.PropsSet(property, value); expected_property_value[40 + i] = vertex_accessor.PropsAt(property); } // Maps. Declare a vector in the expected order, then shuffle when setting on // vertices. std::vector> maps{ {{"b", 12}}, {{"b", 12}, {"a", 77}}, {{"a", 77}, {"c", 0}}, {{"a", 78}, {"b", 12}}}; expected_property_value.insert(expected_property_value.end(), maps.begin(), maps.end()); auto shuffled = maps; std::random_shuffle(shuffled.begin(), shuffled.end()); for (const auto &map : shuffled) { auto vertex_accessor = dba.InsertVertex(); vertex_accessor.add_label(label); vertex_accessor.PropsSet(property, map); } EXPECT_EQ(Count(dba.Vertices(label, property, false)), 0); EXPECT_EQ(Count(dba.Vertices(label, property, true)), 54); int cnt = 0; for (auto vertex : dba.Vertices(label, property, true)) { const PropertyValue &property_value = vertex.PropsAt(property); EXPECT_EQ(property_value.type(), expected_property_value[cnt].type()); switch (property_value.type()) { case PropertyValue::Type::Bool: EXPECT_EQ(property_value.Value(), expected_property_value[cnt].Value()); break; case PropertyValue::Type::Double: EXPECT_EQ(property_value.Value(), expected_property_value[cnt].Value()); break; case PropertyValue::Type::Int: EXPECT_EQ(property_value.Value(), expected_property_value[cnt].Value()); break; case PropertyValue::Type::String: EXPECT_EQ(property_value.Value(), expected_property_value[cnt].Value()); break; case PropertyValue::Type::List: { auto received_value = property_value.Value>(); auto expected_value = expected_property_value[cnt].Value>(); EXPECT_EQ(received_value.size(), expected_value.size()); EXPECT_EQ(received_value.size(), 1); EXPECT_EQ(received_value[0].Value(), expected_value[0].Value()); break; } case PropertyValue::Type::Map: { auto received_value = property_value.Value>(); auto expected_value = expected_property_value[cnt] .Value>(); EXPECT_EQ(received_value.size(), expected_value.size()); for (const auto &kv : expected_value) { auto found = expected_value.find(kv.first); EXPECT_NE(found, expected_value.end()); EXPECT_EQ(kv.second.Value(), found->second.Value()); } break; } case PropertyValue::Type::Null: ASSERT_FALSE("Invalid value type."); } ++cnt; } } /** * A test fixture that contains a database, accessor, * (label, property) index and 100 vertices, 10 for * each of [0, 10) property values. */ class GraphDbAccessorIndexRange : public GraphDbAccessorIndex { protected: void SetUp() override { dba.BuildIndex(label, property); for (int i = 0; i < 100; i++) AddVertex(i / 10); ASSERT_EQ(Count(dba.Vertices(false)), 0); ASSERT_EQ(Count(dba.Vertices(true)), 100); Commit(); ASSERT_EQ(Count(dba.Vertices(false)), 100); } auto Vertices(std::optional> lower, std::optional> upper, bool current_state = false) { return dba.Vertices(label, property, lower, upper, current_state); } auto Inclusive(PropertyValue value) { return std::make_optional(utils::MakeBoundInclusive(PropertyValue(value))); } auto Exclusive(int value) { return std::make_optional(utils::MakeBoundExclusive(PropertyValue(value))); } }; TEST_F(GraphDbAccessorIndexRange, RangeIteration) { using std::nullopt; EXPECT_EQ(Count(Vertices(nullopt, Inclusive(7))), 80); EXPECT_EQ(Count(Vertices(nullopt, Exclusive(7))), 70); EXPECT_EQ(Count(Vertices(Inclusive(7), nullopt)), 30); EXPECT_EQ(Count(Vertices(Exclusive(7), nullopt)), 20); EXPECT_EQ(Count(Vertices(Exclusive(3), Exclusive(6))), 20); EXPECT_EQ(Count(Vertices(Inclusive(3), Inclusive(6))), 40); EXPECT_EQ(Count(Vertices(Inclusive(6), Inclusive(3))), 0); ::testing::FLAGS_gtest_death_test_style = "threadsafe"; EXPECT_DEATH(Vertices(nullopt, nullopt), "bound must be provided"); } TEST_F(GraphDbAccessorIndexRange, RangeIterationCurrentState) { using std::nullopt; EXPECT_EQ(Count(Vertices(nullopt, Inclusive(7))), 80); for (int i = 0; i < 20; i++) AddVertex(2); EXPECT_EQ(Count(Vertices(nullopt, Inclusive(7))), 80); EXPECT_EQ(Count(Vertices(nullopt, Inclusive(7), true)), 100); Commit(); EXPECT_EQ(Count(Vertices(nullopt, Inclusive(7))), 100); } TEST_F(GraphDbAccessorIndexRange, RangeInterationIncompatibleTypes) { using std::nullopt; // using PropertyValue::Null as a bound fails with an assertion ::testing::FLAGS_gtest_death_test_style = "threadsafe"; EXPECT_DEATH(Vertices(nullopt, Inclusive(PropertyValue::Null)), "not a valid index bound"); EXPECT_DEATH(Vertices(Inclusive(PropertyValue::Null), nullopt), "not a valid index bound"); std::vector incompatible_with_int{ "string", true, std::vector{1}}; // using incompatible upper and lower bounds yields no results EXPECT_EQ(Count(Vertices(Inclusive(2), Inclusive("string"))), 0); // for incomparable bound and stored data, // expect that no results are returned ASSERT_EQ(Count(Vertices(Inclusive(0), nullopt)), 100); for (PropertyValue value : incompatible_with_int) { ::testing::FLAGS_gtest_death_test_style = "threadsafe"; EXPECT_EQ(Count(Vertices(nullopt, Inclusive(value))), 0) << "Found vertices of type int for predicate value type: " << value.type(); EXPECT_EQ(Count(Vertices(Inclusive(value), nullopt)), 0) << "Found vertices of type int for predicate value type: " << value.type(); } // we can compare int to double EXPECT_EQ(Count(Vertices(nullopt, Inclusive(1000.0))), 100); EXPECT_EQ(Count(Vertices(Inclusive(0.0), nullopt)), 100); }