From 2954276ca827753b668cad33a0959618f323ad9b Mon Sep 17 00:00:00 2001 From: florijan <florijan@memgraph.io> Date: Thu, 24 Aug 2017 00:13:26 +0200 Subject: [PATCH] Property storage now supports Map Summary: Added: - map support in PropertyValue - conversion of map TypedValue to PropertyValue if appropriate flag is set (undocumented because it's private) - ordering of map PropertyValue in LabelPropertyIndex - issue raised regarding list and value property modifications in storage (currently unsupported) Maybe I missed some feature or whatever? Reviewers: mislav.bradac, buda, teon.banek Reviewed By: mislav.bradac, buda Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D692 --- CHANGELOG.md | 5 ++- docs/user_technical/data-types.md | 11 +++-- src/database/indexes/label_property_index.hpp | 20 +++++++-- src/query/plan/operator.cpp | 6 +++ src/query/typed_value.cpp | 21 +++++++--- src/storage/property_value.cpp | 42 ++++++++++++++++--- src/storage/property_value.hpp | 9 +++- .../features/map_operations.feature | 13 ++++-- tests/unit/graph_db_accessor_index_api.cpp | 33 ++++++++++++++- tests/unit/property_value_store.cpp | 27 ++++++++++-- 10 files changed, 159 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac6cf1c84..952fe0aba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ ### Major Features and Improvements -* CASE construct (without aggregations) -* rand() function added +* CASE construct (without aggregations). +* `rand` function added. +* Maps can now be stored as vertex/edge properties. ### Bug Fixes and Other Changes diff --git a/docs/user_technical/data-types.md b/docs/user_technical/data-types.md index 5311a24fc..ca13405ee 100644 --- a/docs/user_technical/data-types.md +++ b/docs/user_technical/data-types.md @@ -34,11 +34,14 @@ types. Following is a table of supported data types. `Integer` | An integer number. `Float` | A floating-point number, i.e. a real number. `List` | A list containing any number of property values of any supported type. It can be used to store multiple values under a single property name. + `Map` | A mapping of string keys to values of any supported type. -Note that even though map literals are supported in openCypher queries, they can't be stored in the graph. For example, the following query is legal: + Note that even though it's possible to store `List` and `Map` property values, it is not possible to modify them. It is however possible to replace them completely. So, the following queries are legal: - MATCH (n:Person) RETURN {name: n.name, age: n.age} + CREATE (:Node {property: [1, 2, 3]}) + CREATE (:Node {property: {key: "value"}}) -However, the next query is not: +However, these queries are not: - MATCH (n:Person {name: "John"}) SET n.address = {city: "London", street: "Fleet St."} + MATCH (n:Node) SET n.property[0] = 0 + MATCH (n:Node) SET n.property.key = "other value" diff --git a/src/database/indexes/label_property_index.hpp b/src/database/indexes/label_property_index.hpp index a27724b4b..d75b8dda3 100644 --- a/src/database/indexes/label_property_index.hpp +++ b/src/database/indexes/label_property_index.hpp @@ -446,6 +446,19 @@ class LabelPropertyIndex { return lexicographical_compare(va.begin(), va.end(), vb.begin(), vb.end(), Less); } + case PropertyValue::Type::Map: { + auto ma = a.Value<std::map<std::string, PropertyValue>>(); + auto mb = b.Value<std::map<std::string, PropertyValue>>(); + if (ma.size() != mb.size()) return ma.size() < mb.size(); + const auto cmp = [](const auto &a, const auto &b) { + if (a.first != b.first) + return a.first < b.first; + else + return Less(a.second, b.second); + }; + return lexicographical_compare(ma.begin(), ma.end(), mb.begin(), + mb.end(), cmp); + } default: permanent_fail("Unimplemented type operator."); } @@ -482,12 +495,13 @@ class LabelPropertyIndex { }; /** - * @brief - Insert value, vlist, vertex into corresponding index (key) if the - * index exists. + * @brief - Insert value, vlist, vertex into corresponding index (key) if + * the index exists. * @param index - into which index to add * @param value - value which to add * @param vlist - pointer to vlist entry to add - * @param vertex - pointer to vertex record entry to add (contained in vlist) + * @param vertex - pointer to vertex record entry to add (contained in + * vlist) */ void Insert(SkipList<IndexEntry> &index, const PropertyValue &value, mvcc::VersionList<Vertex> *const vlist, diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 27d5839ba..dfe56c52d 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -1202,6 +1202,12 @@ bool SetProperty::SetPropertyCursor::Pull(Frame &frame, case TypedValue::Type::Null: // Skip setting properties on Null (can occur in optional match). break; + case TypedValue::Type::Map: + // Semantically modifying a map makes sense, but it's not supported due to + // all the copying we do (when PropertyValue -> TypedValue and in + // ExpressionEvaluator). So even though we set a map property here, that + // is never visible to the user and it's not stored. + // TODO: fix above described bug default: throw QueryRuntimeException( "Properties can only be set on Vertices and Edges"); diff --git a/src/query/typed_value.cpp b/src/query/typed_value.cpp index de8c7512b..5eee0c57b 100644 --- a/src/query/typed_value.cpp +++ b/src/query/typed_value.cpp @@ -33,11 +33,18 @@ TypedValue::TypedValue(const PropertyValue &value) { type_ = Type::String; new (&string_v) std::string(value.Value<std::string>()); return; - case PropertyValue::Type::List: + case PropertyValue::Type::List: { type_ = Type::List; auto vec = value.Value<std::vector<PropertyValue>>(); new (&list_v) std::vector<TypedValue>(vec.begin(), vec.end()); return; + } + case PropertyValue::Type::Map: { + type_ = Type::Map; + auto map = value.Value<std::map<std::string, PropertyValue>>(); + new (&map_v) std::map<std::string, TypedValue>(map.begin(), map.end()); + return; + } } permanent_fail("Unsupported type"); } @@ -57,10 +64,14 @@ TypedValue::operator PropertyValue() const { case TypedValue::Type::List: return PropertyValue( std::vector<PropertyValue>(list_v.begin(), list_v.end())); + case TypedValue::Type::Map: + return PropertyValue( + std::map<std::string, PropertyValue>(map_v.begin(), map_v.end())); default: - throw TypedValueException( - "Unsupported conversion from TypedValue to PropertyValue"); + break; } + throw TypedValueException( + "Unsupported conversion from TypedValue to PropertyValue"); } // TODO: Refactor this. Value<bool> should be ValueBool. If we do it in that way @@ -190,7 +201,6 @@ const std::vector<TypedValue> &TypedValue::ValueList() const { return Value<std::vector<TypedValue>>(); } - template <> std::map<std::string, TypedValue> &TypedValue::Value<std::map<std::string, TypedValue>>() { @@ -273,9 +283,10 @@ bool TypedValue::IsPropertyValue() const { case Type::Double: case Type::String: case Type::List: + case Type::Map: return true; default: - return false; + return false; } } diff --git a/src/storage/property_value.cpp b/src/storage/property_value.cpp index c27247a15..ff7d5f877 100644 --- a/src/storage/property_value.cpp +++ b/src/storage/property_value.cpp @@ -49,7 +49,16 @@ std::vector<PropertyValue> PropertyValue::Value<std::vector<PropertyValue>>() return *list_v; } -PropertyValue::PropertyValue(const PropertyValue& other) : type_(other.type_) { +template <> +std::map<std::string, PropertyValue> +PropertyValue::Value<std::map<std::string, PropertyValue>>() const { + if (type_ != PropertyValue::Type::Map) { + throw PropertyValueException("Incompatible template param and type"); + } + return *map_v; +} + +PropertyValue::PropertyValue(const PropertyValue &other) : type_(other.type_) { switch (other.type_) { case PropertyValue::Type::Null: return; @@ -70,15 +79,20 @@ PropertyValue::PropertyValue(const PropertyValue& other) : type_(other.type_) { this->double_v = other.double_v; return; - case PropertyValue::Type::List: + case Type::List: new (&list_v) std::shared_ptr<std::vector<PropertyValue>>(other.list_v); return; + + case Type::Map: + new (&map_v) + std::shared_ptr<std::map<std::string, PropertyValue>>(other.map_v); + return; } permanent_fail("Unsupported PropertyValue::Type"); } -std::ostream& operator<<(std::ostream& os, const PropertyValue::Type type) { +std::ostream &operator<<(std::ostream &os, const PropertyValue::Type type) { switch (type) { case PropertyValue::Type::Null: return os << "null"; @@ -92,11 +106,13 @@ std::ostream& operator<<(std::ostream& os, const PropertyValue::Type type) { return os << "double"; case PropertyValue::Type::List: return os << "list"; + case PropertyValue::Type::Map: + return os << "map"; } permanent_fail("Unsupported PropertyValue::Type"); } -std::ostream& operator<<(std::ostream& os, const PropertyValue& value) { +std::ostream &operator<<(std::ostream &os, const PropertyValue &value) { switch (value.type_) { case PropertyValue::Type::Null: return os << "Null"; @@ -110,15 +126,22 @@ std::ostream& operator<<(std::ostream& os, const PropertyValue& value) { return os << value.Value<double>(); case PropertyValue::Type::List: os << "["; - for (const auto& x : value.Value<std::vector<PropertyValue>>()) { + for (const auto &x : value.Value<std::vector<PropertyValue>>()) { os << x << ","; } return os << "]"; + case PropertyValue::Type::Map: + os << "{"; + for (const auto &kv : + value.Value<std::map<std::string, PropertyValue>>()) { + os << kv.first << ": " << kv.second << ","; + } + return os << "}"; } permanent_fail("Unsupported PropertyValue::Type"); } -PropertyValue& PropertyValue::operator=(const PropertyValue& other) { +PropertyValue &PropertyValue::operator=(const PropertyValue &other) { this->~PropertyValue(); type_ = other.type_; @@ -140,6 +163,10 @@ PropertyValue& PropertyValue::operator=(const PropertyValue& other) { case PropertyValue::Type::List: new (&list_v) std::shared_ptr<std::vector<PropertyValue>>(other.list_v); return *this; + case PropertyValue::Type::Map: + new (&map_v) + std::shared_ptr<std::map<std::string, PropertyValue>>(other.map_v); + return *this; } } permanent_fail("Unsupported PropertyValue::Type"); @@ -163,6 +190,9 @@ PropertyValue::~PropertyValue() { case Type::List: list_v.~shared_ptr<std::vector<PropertyValue>>(); return; + case Type::Map: + map_v.~shared_ptr<std::map<std::string, PropertyValue>>(); + return; } permanent_fail("Unsupported PropertyValue::Type"); } diff --git a/src/storage/property_value.hpp b/src/storage/property_value.hpp index 7258c6ed8..555c07e4d 100644 --- a/src/storage/property_value.hpp +++ b/src/storage/property_value.hpp @@ -2,6 +2,7 @@ #include <cassert> #include <iostream> +#include <map> #include <memory> #include <string> #include <vector> @@ -23,7 +24,7 @@ class PropertyValue { public: /** A value type. Each type corresponds to exactly one C++ type */ - enum class Type : unsigned { Null, String, Bool, Int, Double, List }; + enum class Type : unsigned { Null, String, Bool, Int, Double, List, Map }; // single static reference to Null, used whenever Null should be returned static const PropertyValue Null; @@ -54,6 +55,11 @@ class PropertyValue { new (&list_v) std::shared_ptr<std::vector<PropertyValue>>( new std::vector<PropertyValue>(value)); } + PropertyValue(const std::map<std::string, PropertyValue> &value) + : type_(Type::Map) { + new (&map_v) std::shared_ptr<std::map<std::string, PropertyValue>>( + new std::map<std::string, PropertyValue>(value)); + } // assignment op PropertyValue &operator=(const PropertyValue &other); @@ -86,6 +92,7 @@ class PropertyValue { // We support lists of values of different types, neo4j supports lists of // values of the same type. std::shared_ptr<std::vector<PropertyValue>> list_v; + std::shared_ptr<std::map<std::string, PropertyValue>> map_v; }; /** diff --git a/tests/qa/tck_engine/tests/memgraph_V1/features/map_operations.feature b/tests/qa/tck_engine/tests/memgraph_V1/features/map_operations.feature index c6569154f..9b460e8dc 100644 --- a/tests/qa/tck_engine/tests/memgraph_V1/features/map_operations.feature +++ b/tests/qa/tck_engine/tests/memgraph_V1/features/map_operations.feature @@ -10,12 +10,19 @@ Feature: Map operators | {a: 1, b: 'bla', c: [1, 2], d: {a: 42}} | - Scenario: Storing a map property fails + Scenario: Storing a map property + Given an empty graph + And having executed + """ + CREATE ({prop: {x: 1, y: true, z: "bla"}}) + """ When executing query: """ - CREATE (n {property: {a: 1, b: 2}}) + MATCH (n) RETURN n.prop """ - Then an error should be raised + Then the result should be: + | n.prop | + | {x: 1, y: true, z: 'bla'} | Scenario: A property lookup on a map literal diff --git a/tests/unit/graph_db_accessor_index_api.cpp b/tests/unit/graph_db_accessor_index_api.cpp index 514669a5c..920cb0867 100644 --- a/tests/unit/graph_db_accessor_index_api.cpp +++ b/tests/unit/graph_db_accessor_index_api.cpp @@ -286,8 +286,25 @@ TEST_F(GraphDbAccessorIndex, LabelPropertyValueSorting) { 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<std::map<std::string, PropertyValue>> 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)), 50); + EXPECT_EQ(Count(dba->Vertices(label, property, true)), 54); int cnt = 0; for (auto vertex : dba->Vertices(label, property, true)) { @@ -321,6 +338,20 @@ TEST_F(GraphDbAccessorIndex, LabelPropertyValueSorting) { expected_value[0].Value<int64_t>()); break; } + case PropertyValue::Type::Map: { + auto received_value = + property_value.Value<std::map<std::string, PropertyValue>>(); + auto expected_value = + expected_property_value[cnt] + .Value<std::map<std::string, PropertyValue>>(); + 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<int64_t>(), found->second.Value<int64_t>()); + } + break; + } case PropertyValue::Type::Null: ASSERT_FALSE("Invalid value type."); } diff --git a/tests/unit/property_value_store.cpp b/tests/unit/property_value_store.cpp index 2984ecbda..dc461e7ae 100644 --- a/tests/unit/property_value_store.cpp +++ b/tests/unit/property_value_store.cpp @@ -6,8 +6,8 @@ #include "gtest/gtest.h" -#include "storage/property_value_store.hpp" #include "storage/property_value.hpp" +#include "storage/property_value_store.hpp" using std::string; @@ -108,8 +108,9 @@ TEST(PropertyValueStore, Accept) { int count_props = 0; int count_finish = 0; - auto handler = - [&](const uint32_t, const PropertyValue&) { count_props += 1; }; + auto handler = [&](const uint32_t, const PropertyValue &) { + count_props += 1; + }; auto finish = [&]() { count_finish += 1; }; PropertyValueStore<uint32_t> props; @@ -148,3 +149,23 @@ TEST(PropertyValueStore, InsertRetrieveList) { EXPECT_EQ(l[3].Value<std::string>(), "something"); EXPECT_EQ(l[4].type(), PropertyValue::Type::Null); } + +TEST(PropertyValueStore, InsertRetrieveMap) { + PropertyValueStore<> props; + props.set(0, std::map<std::string, PropertyValue>{ + {"a", 1}, {"b", true}, {"c", "something"}}); + + auto p = props.at(0); + EXPECT_EQ(p.type(), PropertyValue::Type::Map); + auto m = p.Value<std::map<std::string, PropertyValue>>(); + EXPECT_EQ(m.size(), 3); + auto get = [&m](const std::string &prop_name) { + return m.find(prop_name)->second; + }; + EXPECT_EQ(get("a").type(), PropertyValue::Type::Int); + EXPECT_EQ(get("a").Value<int64_t>(), 1); + EXPECT_EQ(get("b").type(), PropertyValue::Type::Bool); + EXPECT_EQ(get("b").Value<bool>(), true); + EXPECT_EQ(get("c").type(), PropertyValue::Type::String); + EXPECT_EQ(get("c").Value<std::string>(), "something"); +}