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
This commit is contained in:
florijan 2017-08-24 00:13:26 +02:00
parent 4ccffbfd9b
commit 2954276ca8
10 changed files with 159 additions and 28 deletions

View File

@ -4,8 +4,9 @@
### Major Features and Improvements ### Major Features and Improvements
* CASE construct (without aggregations) * CASE construct (without aggregations).
* rand() function added * `rand` function added.
* Maps can now be stored as vertex/edge properties.
### Bug Fixes and Other Changes ### Bug Fixes and Other Changes

View File

@ -34,11 +34,14 @@ types. Following is a table of supported data types.
`Integer` | An integer number. `Integer` | An integer number.
`Float` | A floating-point number, i.e. a real 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. `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"

View File

@ -446,6 +446,19 @@ class LabelPropertyIndex {
return lexicographical_compare(va.begin(), va.end(), vb.begin(), return lexicographical_compare(va.begin(), va.end(), vb.begin(),
vb.end(), Less); 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: default:
permanent_fail("Unimplemented type operator."); permanent_fail("Unimplemented type operator.");
} }
@ -482,12 +495,13 @@ class LabelPropertyIndex {
}; };
/** /**
* @brief - Insert value, vlist, vertex into corresponding index (key) if the * @brief - Insert value, vlist, vertex into corresponding index (key) if
* index exists. * the index exists.
* @param index - into which index to add * @param index - into which index to add
* @param value - value which to add * @param value - value which to add
* @param vlist - pointer to vlist entry 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, void Insert(SkipList<IndexEntry> &index, const PropertyValue &value,
mvcc::VersionList<Vertex> *const vlist, mvcc::VersionList<Vertex> *const vlist,

View File

@ -1202,6 +1202,12 @@ bool SetProperty::SetPropertyCursor::Pull(Frame &frame,
case TypedValue::Type::Null: case TypedValue::Type::Null:
// Skip setting properties on Null (can occur in optional match). // Skip setting properties on Null (can occur in optional match).
break; 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: default:
throw QueryRuntimeException( throw QueryRuntimeException(
"Properties can only be set on Vertices and Edges"); "Properties can only be set on Vertices and Edges");

View File

@ -33,11 +33,18 @@ TypedValue::TypedValue(const PropertyValue &value) {
type_ = Type::String; type_ = Type::String;
new (&string_v) std::string(value.Value<std::string>()); new (&string_v) std::string(value.Value<std::string>());
return; return;
case PropertyValue::Type::List: case PropertyValue::Type::List: {
type_ = Type::List; type_ = Type::List;
auto vec = value.Value<std::vector<PropertyValue>>(); auto vec = value.Value<std::vector<PropertyValue>>();
new (&list_v) std::vector<TypedValue>(vec.begin(), vec.end()); new (&list_v) std::vector<TypedValue>(vec.begin(), vec.end());
return; 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"); permanent_fail("Unsupported type");
} }
@ -57,10 +64,14 @@ TypedValue::operator PropertyValue() const {
case TypedValue::Type::List: case TypedValue::Type::List:
return PropertyValue( return PropertyValue(
std::vector<PropertyValue>(list_v.begin(), list_v.end())); 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: default:
throw TypedValueException( break;
"Unsupported conversion from TypedValue to PropertyValue");
} }
throw TypedValueException(
"Unsupported conversion from TypedValue to PropertyValue");
} }
// TODO: Refactor this. Value<bool> should be ValueBool. If we do it in that way // 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>>(); return Value<std::vector<TypedValue>>();
} }
template <> template <>
std::map<std::string, TypedValue> std::map<std::string, TypedValue>
&TypedValue::Value<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::Double:
case Type::String: case Type::String:
case Type::List: case Type::List:
case Type::Map:
return true; return true;
default: default:
return false; return false;
} }
} }

View File

@ -49,7 +49,16 @@ std::vector<PropertyValue> PropertyValue::Value<std::vector<PropertyValue>>()
return *list_v; 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_) { switch (other.type_) {
case PropertyValue::Type::Null: case PropertyValue::Type::Null:
return; return;
@ -70,15 +79,20 @@ PropertyValue::PropertyValue(const PropertyValue& other) : type_(other.type_) {
this->double_v = other.double_v; this->double_v = other.double_v;
return; return;
case PropertyValue::Type::List: case Type::List:
new (&list_v) std::shared_ptr<std::vector<PropertyValue>>(other.list_v); new (&list_v) std::shared_ptr<std::vector<PropertyValue>>(other.list_v);
return; return;
case Type::Map:
new (&map_v)
std::shared_ptr<std::map<std::string, PropertyValue>>(other.map_v);
return;
} }
permanent_fail("Unsupported PropertyValue::Type"); 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) { switch (type) {
case PropertyValue::Type::Null: case PropertyValue::Type::Null:
return os << "null"; return os << "null";
@ -92,11 +106,13 @@ std::ostream& operator<<(std::ostream& os, const PropertyValue::Type type) {
return os << "double"; return os << "double";
case PropertyValue::Type::List: case PropertyValue::Type::List:
return os << "list"; return os << "list";
case PropertyValue::Type::Map:
return os << "map";
} }
permanent_fail("Unsupported PropertyValue::Type"); 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_) { switch (value.type_) {
case PropertyValue::Type::Null: case PropertyValue::Type::Null:
return os << "Null"; return os << "Null";
@ -110,15 +126,22 @@ std::ostream& operator<<(std::ostream& os, const PropertyValue& value) {
return os << value.Value<double>(); return os << value.Value<double>();
case PropertyValue::Type::List: case PropertyValue::Type::List:
os << "["; os << "[";
for (const auto& x : value.Value<std::vector<PropertyValue>>()) { for (const auto &x : value.Value<std::vector<PropertyValue>>()) {
os << x << ","; os << x << ",";
} }
return os << "]"; 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"); permanent_fail("Unsupported PropertyValue::Type");
} }
PropertyValue& PropertyValue::operator=(const PropertyValue& other) { PropertyValue &PropertyValue::operator=(const PropertyValue &other) {
this->~PropertyValue(); this->~PropertyValue();
type_ = other.type_; type_ = other.type_;
@ -140,6 +163,10 @@ PropertyValue& PropertyValue::operator=(const PropertyValue& other) {
case PropertyValue::Type::List: case PropertyValue::Type::List:
new (&list_v) std::shared_ptr<std::vector<PropertyValue>>(other.list_v); new (&list_v) std::shared_ptr<std::vector<PropertyValue>>(other.list_v);
return *this; 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"); permanent_fail("Unsupported PropertyValue::Type");
@ -163,6 +190,9 @@ PropertyValue::~PropertyValue() {
case Type::List: case Type::List:
list_v.~shared_ptr<std::vector<PropertyValue>>(); list_v.~shared_ptr<std::vector<PropertyValue>>();
return; return;
case Type::Map:
map_v.~shared_ptr<std::map<std::string, PropertyValue>>();
return;
} }
permanent_fail("Unsupported PropertyValue::Type"); permanent_fail("Unsupported PropertyValue::Type");
} }

View File

@ -2,6 +2,7 @@
#include <cassert> #include <cassert>
#include <iostream> #include <iostream>
#include <map>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@ -23,7 +24,7 @@ class PropertyValue {
public: public:
/** A value type. Each type corresponds to exactly one C++ type */ /** 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 // single static reference to Null, used whenever Null should be returned
static const PropertyValue Null; static const PropertyValue Null;
@ -54,6 +55,11 @@ class PropertyValue {
new (&list_v) std::shared_ptr<std::vector<PropertyValue>>( new (&list_v) std::shared_ptr<std::vector<PropertyValue>>(
new std::vector<PropertyValue>(value)); 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 // assignment op
PropertyValue &operator=(const PropertyValue &other); PropertyValue &operator=(const PropertyValue &other);
@ -86,6 +92,7 @@ class PropertyValue {
// We support lists of values of different types, neo4j supports lists of // We support lists of values of different types, neo4j supports lists of
// values of the same type. // values of the same type.
std::shared_ptr<std::vector<PropertyValue>> list_v; std::shared_ptr<std::vector<PropertyValue>> list_v;
std::shared_ptr<std::map<std::string, PropertyValue>> map_v;
}; };
/** /**

View File

@ -10,12 +10,19 @@ Feature: Map operators
| {a: 1, b: 'bla', c: [1, 2], d: {a: 42}} | | {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: 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 Scenario: A property lookup on a map literal

View File

@ -286,8 +286,25 @@ TEST_F(GraphDbAccessorIndex, LabelPropertyValueSorting) {
expected_property_value[40 + i] = vertex_accessor.PropsAt(property); 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, false)), 0);
EXPECT_EQ(Count(dba->Vertices(label, property, true)), 50); EXPECT_EQ(Count(dba->Vertices(label, property, true)), 54);
int cnt = 0; int cnt = 0;
for (auto vertex : dba->Vertices(label, property, true)) { for (auto vertex : dba->Vertices(label, property, true)) {
@ -321,6 +338,20 @@ TEST_F(GraphDbAccessorIndex, LabelPropertyValueSorting) {
expected_value[0].Value<int64_t>()); expected_value[0].Value<int64_t>());
break; 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: case PropertyValue::Type::Null:
ASSERT_FALSE("Invalid value type."); ASSERT_FALSE("Invalid value type.");
} }

View File

@ -6,8 +6,8 @@
#include "gtest/gtest.h" #include "gtest/gtest.h"
#include "storage/property_value_store.hpp"
#include "storage/property_value.hpp" #include "storage/property_value.hpp"
#include "storage/property_value_store.hpp"
using std::string; using std::string;
@ -108,8 +108,9 @@ TEST(PropertyValueStore, Accept) {
int count_props = 0; int count_props = 0;
int count_finish = 0; int count_finish = 0;
auto handler = auto handler = [&](const uint32_t, const PropertyValue &) {
[&](const uint32_t, const PropertyValue&) { count_props += 1; }; count_props += 1;
};
auto finish = [&]() { count_finish += 1; }; auto finish = [&]() { count_finish += 1; };
PropertyValueStore<uint32_t> props; PropertyValueStore<uint32_t> props;
@ -148,3 +149,23 @@ TEST(PropertyValueStore, InsertRetrieveList) {
EXPECT_EQ(l[3].Value<std::string>(), "something"); EXPECT_EQ(l[3].Value<std::string>(), "something");
EXPECT_EQ(l[4].type(), PropertyValue::Type::Null); 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");
}