diff --git a/src/storage/v2/property_value.hpp b/src/storage/v2/property_value.hpp
new file mode 100644
index 000000000..e8655d6d1
--- /dev/null
+++ b/src/storage/v2/property_value.hpp
@@ -0,0 +1,398 @@
+#pragma once
+
+#include <iostream>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "utils/algorithm.hpp"
+#include "utils/exceptions.hpp"
+
+namespace storage {
+
+/// An exception raised by the PropertyValue. Typically when trying to perform
+/// operations (such as addition) on PropertyValues of incompatible Types.
+class PropertyValueException : public utils::BasicException {
+ public:
+  using utils::BasicException::BasicException;
+};
+
+/// Encapsulation of a value and its type in a class that has no compile-time
+/// info about the type.
+///
+/// Values can be of a number of predefined types that are enumerated in
+/// PropertyValue::Type. Each such type corresponds to exactly one C++ type.
+class PropertyValue {
+ public:
+  enum class Type : uint8_t { Null, Bool, Int, Double, String, List, Map };
+
+  // default constructor, makes Null
+  PropertyValue() : type_(Type::Null) {}
+
+  // constructors for primitive types
+  explicit PropertyValue(bool value) : type_(Type::Bool) { bool_v = value; }
+  explicit PropertyValue(int value) : type_(Type::Int) { int_v = value; }
+  explicit PropertyValue(int64_t value) : type_(Type::Int) { int_v = value; }
+  explicit PropertyValue(double value) : type_(Type::Double) {
+    double_v = value;
+  }
+
+  // copy constructors for non-primitive types
+  explicit PropertyValue(const std::string &value) : type_(Type::String) {
+    new (&string_v) std::string(value);
+  }
+  explicit PropertyValue(const char *value) : type_(Type::String) {
+    new (&string_v) std::string(value);
+  }
+  explicit PropertyValue(const std::vector<PropertyValue> &value)
+      : type_(Type::List) {
+    new (&list_v) std::vector<PropertyValue>(value);
+  }
+  explicit PropertyValue(const std::map<std::string, PropertyValue> &value)
+      : type_(Type::Map) {
+    new (&map_v) std::map<std::string, PropertyValue>(value);
+  }
+
+  // move constructors for non-primitive types
+  explicit PropertyValue(std::string &&value) noexcept : type_(Type::String) {
+    new (&string_v) std::string(std::move(value));
+  }
+  explicit PropertyValue(std::vector<PropertyValue> &&value) noexcept
+      : type_(Type::List) {
+    new (&list_v) std::vector<PropertyValue>(std::move(value));
+  }
+  explicit PropertyValue(std::map<std::string, PropertyValue> &&value) noexcept
+      : type_(Type::Map) {
+    new (&map_v) std::map<std::string, PropertyValue>(std::move(value));
+  }
+
+  // copy constructor
+  PropertyValue(const PropertyValue &other) : type_(other.type_) {
+    switch (other.type_) {
+      case Type::Null:
+        return;
+      case Type::Bool:
+        this->bool_v = other.bool_v;
+        return;
+      case Type::Int:
+        this->int_v = other.int_v;
+        return;
+      case Type::Double:
+        this->double_v = other.double_v;
+        return;
+      case Type::String:
+        new (&string_v) std::string(other.string_v);
+        return;
+      case Type::List:
+        new (&list_v) std::vector<PropertyValue>(other.list_v);
+        return;
+      case Type::Map:
+        new (&map_v) std::map<std::string, PropertyValue>(other.map_v);
+        return;
+    }
+  }
+
+  // move constructor
+  PropertyValue(PropertyValue &&other) noexcept : type_(other.type_) {
+    switch (other.type_) {
+      case Type::Null:
+        break;
+      case Type::Bool:
+        this->bool_v = other.bool_v;
+        break;
+      case Type::Int:
+        this->int_v = other.int_v;
+        break;
+      case Type::Double:
+        this->double_v = other.double_v;
+        break;
+      case Type::String:
+        new (&string_v) std::string(std::move(other.string_v));
+        break;
+      case Type::List:
+        new (&list_v) std::vector<PropertyValue>(std::move(other.list_v));
+        break;
+      case Type::Map:
+        new (&map_v)
+            std::map<std::string, PropertyValue>(std::move(other.map_v));
+        break;
+    }
+
+    // reset the type of other
+    other.DestroyValue();
+    other.type_ = Type::Null;
+  }
+
+  // copy assignment
+  PropertyValue &operator=(const PropertyValue &other) {
+    if (this == &other) return *this;
+
+    DestroyValue();
+    type_ = other.type_;
+
+    switch (other.type_) {
+      case Type::Null:
+        break;
+      case Type::Bool:
+        this->bool_v = other.bool_v;
+        break;
+      case Type::Int:
+        this->int_v = other.int_v;
+        break;
+      case Type::Double:
+        this->double_v = other.double_v;
+        break;
+      case Type::String:
+        new (&string_v) std::string(other.string_v);
+        break;
+      case Type::List:
+        new (&list_v) std::vector<PropertyValue>(other.list_v);
+        break;
+      case Type::Map:
+        new (&map_v) std::map<std::string, PropertyValue>(other.map_v);
+        break;
+    }
+
+    return *this;
+  }
+
+  // move assignment
+  PropertyValue &operator=(PropertyValue &&other) noexcept {
+    if (this == &other) return *this;
+
+    DestroyValue();
+    type_ = other.type_;
+
+    switch (other.type_) {
+      case Type::Null:
+        break;
+      case Type::Bool:
+        this->bool_v = other.bool_v;
+        break;
+      case Type::Int:
+        this->int_v = other.int_v;
+        break;
+      case Type::Double:
+        this->double_v = other.double_v;
+        break;
+      case Type::String:
+        new (&string_v) std::string(std::move(other.string_v));
+        break;
+      case Type::List:
+        new (&list_v) std::vector<PropertyValue>(std::move(other.list_v));
+        break;
+      case Type::Map:
+        new (&map_v)
+            std::map<std::string, PropertyValue>(std::move(other.map_v));
+        break;
+    }
+
+    // reset the type of other
+    other.DestroyValue();
+    other.type_ = Type::Null;
+
+    return *this;
+  }
+
+  // TODO: Implement copy assignment operators for primitive types.
+  // TODO: Implement copy and move assignment operators for non-primitive types.
+
+  ~PropertyValue() { DestroyValue(); }
+
+  Type type() const { return type_; }
+
+  // type checkers
+  bool IsNull() const { return type_ == Type::Null; }
+  bool IsBool() const { return type_ == Type::Bool; }
+  bool IsInt() const { return type_ == Type::Int; }
+  bool IsDouble() const { return type_ == Type::Double; }
+  bool IsString() const { return type_ == Type::String; }
+  bool IsList() const { return type_ == Type::List; }
+  bool IsMap() const { return type_ == Type::Map; }
+
+  // value getters for primitive types
+  bool ValueBool() const {
+    if (type_ != Type::Bool) {
+      throw PropertyValueException("The value isn't a bool!");
+    }
+    return bool_v;
+  }
+  int64_t ValueInt() const {
+    if (type_ != Type::Int) {
+      throw PropertyValueException("The value isn't an int!");
+    }
+    return int_v;
+  }
+  double ValueDouble() const {
+    if (type_ != Type::Double) {
+      throw PropertyValueException("The value isn't a double!");
+    }
+    return double_v;
+  }
+
+  // const value getters for non-primitive types
+  const std::string &ValueString() const {
+    if (type_ != Type::String) {
+      throw PropertyValueException("The value isn't a string!");
+    }
+    return string_v;
+  }
+  const std::vector<PropertyValue> &ValueList() const {
+    if (type_ != Type::List) {
+      throw PropertyValueException("The value isn't a list!");
+    }
+    return list_v;
+  }
+  const std::map<std::string, PropertyValue> &ValueMap() const {
+    if (type_ != Type::Map) {
+      throw PropertyValueException("The value isn't a map!");
+    }
+    return map_v;
+  }
+
+  // reference value getters for non-primitive types
+  std::string &ValueString() {
+    if (type_ != Type::String) {
+      throw PropertyValueException("The value isn't a string!");
+    }
+    return string_v;
+  }
+  std::vector<PropertyValue> &ValueList() {
+    if (type_ != Type::List) {
+      throw PropertyValueException("The value isn't a list!");
+    }
+    return list_v;
+  }
+  std::map<std::string, PropertyValue> &ValueMap() {
+    if (type_ != Type::Map) {
+      throw PropertyValueException("The value isn't a map!");
+    }
+    return map_v;
+  }
+
+ private:
+  void DestroyValue() {
+    switch (type_) {
+      // destructor for primitive types does nothing
+      case Type::Null:
+      case Type::Bool:
+      case Type::Int:
+      case Type::Double:
+        return;
+
+      // destructor for non primitive types since we used placement new
+      case Type::String:
+        // Clang fails to compile ~std::string. It seems it is a bug in some
+        // versions of clang. Using namespace std statement solves the issue.
+        using namespace std;
+        string_v.~string();
+        return;
+      case Type::List:
+        list_v.~vector();
+        return;
+      case Type::Map:
+        map_v.~map();
+        return;
+    }
+  }
+
+  union {
+    bool bool_v;
+    int64_t int_v;
+    double double_v;
+    std::string string_v;
+    std::vector<PropertyValue> list_v;
+    std::map<std::string, PropertyValue> map_v;
+  };
+
+  Type type_;
+};
+
+// stream output
+inline std::ostream &operator<<(std::ostream &os,
+                                const PropertyValue::Type type) {
+  switch (type) {
+    case PropertyValue::Type::Null:
+      return os << "null";
+    case PropertyValue::Type::Bool:
+      return os << "bool";
+    case PropertyValue::Type::Int:
+      return os << "int";
+    case PropertyValue::Type::Double:
+      return os << "double";
+    case PropertyValue::Type::String:
+      return os << "string";
+    case PropertyValue::Type::List:
+      return os << "list";
+    case PropertyValue::Type::Map:
+      return os << "map";
+  }
+}
+inline std::ostream &operator<<(std::ostream &os, const PropertyValue &value) {
+  switch (value.type()) {
+    case PropertyValue::Type::Null:
+      return os << "null";
+    case PropertyValue::Type::Bool:
+      return os << (value.ValueBool() ? "true" : "false");
+    case PropertyValue::Type::Int:
+      return os << value.ValueInt();
+    case PropertyValue::Type::Double:
+      return os << value.ValueDouble();
+    case PropertyValue::Type::String:
+      return os << value.ValueString();
+    case PropertyValue::Type::List:
+      os << "[";
+      utils::PrintIterable(os, value.ValueList());
+      return os << "]";
+    case PropertyValue::Type::Map:
+      os << "{";
+      utils::PrintIterable(os, value.ValueMap(), ", ",
+                           [](auto &stream, const auto &pair) {
+                             stream << pair.first << ": " << pair.second;
+                           });
+      return os << "}";
+  }
+}
+
+// comparison
+inline bool operator==(const PropertyValue &first,
+                       const PropertyValue &second) {
+  if (first.type() != second.type()) return false;
+  switch (first.type()) {
+    case PropertyValue::Type::Null:
+      return true;
+    case PropertyValue::Type::Bool:
+      return first.ValueBool() == second.ValueBool();
+    case PropertyValue::Type::Int:
+      return first.ValueInt() == second.ValueInt();
+    case PropertyValue::Type::Double:
+      return first.ValueDouble() == second.ValueDouble();
+    case PropertyValue::Type::String:
+      return first.ValueString() == second.ValueString();
+    case PropertyValue::Type::List:
+      return first.ValueList() == second.ValueList();
+    case PropertyValue::Type::Map:
+      return first.ValueMap() == second.ValueMap();
+  }
+}
+inline bool operator<(const PropertyValue &first, const PropertyValue &second) {
+  if (first.type() != second.type()) return first.type() < second.type();
+  switch (first.type()) {
+    case PropertyValue::Type::Null:
+      return false;
+    case PropertyValue::Type::Bool:
+      return first.ValueBool() < second.ValueBool();
+    case PropertyValue::Type::Int:
+      return first.ValueInt() < second.ValueInt();
+    case PropertyValue::Type::Double:
+      return first.ValueDouble() < second.ValueDouble();
+    case PropertyValue::Type::String:
+      return first.ValueString() < second.ValueString();
+    case PropertyValue::Type::List:
+      return first.ValueList() < second.ValueList();
+    case PropertyValue::Type::Map:
+      return first.ValueMap() < second.ValueMap();
+  }
+}
+
+}  // namespace storage
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 59adc6db6..6f5551ee1 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -379,6 +379,9 @@ target_link_libraries(${test_prefix}auth mg-auth kvstore_lib)
 
 # Test storage v2
 
+add_unit_test(property_value_v2.cpp)
+target_link_libraries(${test_prefix}property_value_v2 mg-utils)
+
 add_unit_test(storage_v2.cpp)
 target_link_libraries(${test_prefix}storage_v2 mg-utils)
 
diff --git a/tests/unit/property_value_v2.cpp b/tests/unit/property_value_v2.cpp
new file mode 100644
index 000000000..6aa93fbdf
--- /dev/null
+++ b/tests/unit/property_value_v2.cpp
@@ -0,0 +1,731 @@
+#include <gtest/gtest.h>
+
+#include <sstream>
+
+#include "storage/v2/property_value.hpp"
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, Null) {
+  storage::PropertyValue pv;
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::Null);
+
+  ASSERT_TRUE(pv.IsNull());
+  ASSERT_FALSE(pv.IsBool());
+  ASSERT_FALSE(pv.IsInt());
+  ASSERT_FALSE(pv.IsDouble());
+  ASSERT_FALSE(pv.IsString());
+  ASSERT_FALSE(pv.IsList());
+  ASSERT_FALSE(pv.IsMap());
+
+  ASSERT_THROW(pv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueMap(), storage::PropertyValueException);
+
+  const auto &cpv = pv;
+
+  ASSERT_THROW(cpv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueMap(), storage::PropertyValueException);
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "null");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "null");
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, Bool) {
+  storage::PropertyValue pv(false);
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::Bool);
+
+  ASSERT_FALSE(pv.IsNull());
+  ASSERT_TRUE(pv.IsBool());
+  ASSERT_FALSE(pv.IsInt());
+  ASSERT_FALSE(pv.IsDouble());
+  ASSERT_FALSE(pv.IsString());
+  ASSERT_FALSE(pv.IsList());
+  ASSERT_FALSE(pv.IsMap());
+
+  ASSERT_EQ(pv.ValueBool(), false);
+  ASSERT_THROW(pv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueMap(), storage::PropertyValueException);
+
+  const auto &cpv = pv;
+
+  ASSERT_EQ(cpv.ValueBool(), false);
+  ASSERT_THROW(cpv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueMap(), storage::PropertyValueException);
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "bool");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "false");
+  }
+  {
+    storage::PropertyValue pvtrue(true);
+    std::stringstream ss;
+    ss << pvtrue;
+    ASSERT_EQ(ss.str(), "true");
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, Int) {
+  storage::PropertyValue pv(123L);
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::Int);
+
+  ASSERT_FALSE(pv.IsNull());
+  ASSERT_FALSE(pv.IsBool());
+  ASSERT_TRUE(pv.IsInt());
+  ASSERT_FALSE(pv.IsDouble());
+  ASSERT_FALSE(pv.IsString());
+  ASSERT_FALSE(pv.IsList());
+  ASSERT_FALSE(pv.IsMap());
+
+  ASSERT_THROW(pv.ValueBool(), storage::PropertyValueException);
+  ASSERT_EQ(pv.ValueInt(), 123L);
+  ASSERT_THROW(pv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueMap(), storage::PropertyValueException);
+
+  const auto &cpv = pv;
+
+  ASSERT_THROW(cpv.ValueBool(), storage::PropertyValueException);
+  ASSERT_EQ(cpv.ValueInt(), 123L);
+  ASSERT_THROW(cpv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueMap(), storage::PropertyValueException);
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "int");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "123");
+  }
+
+  {
+    storage::PropertyValue pvint(123);
+    ASSERT_EQ(pvint.type(), storage::PropertyValue::Type::Int);
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, Double) {
+  storage::PropertyValue pv(123.5);
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::Double);
+
+  ASSERT_FALSE(pv.IsNull());
+  ASSERT_FALSE(pv.IsBool());
+  ASSERT_FALSE(pv.IsInt());
+  ASSERT_TRUE(pv.IsDouble());
+  ASSERT_FALSE(pv.IsString());
+  ASSERT_FALSE(pv.IsList());
+  ASSERT_FALSE(pv.IsMap());
+
+  ASSERT_THROW(pv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueInt(), storage::PropertyValueException);
+  ASSERT_EQ(pv.ValueDouble(), 123.5);
+  ASSERT_THROW(pv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueMap(), storage::PropertyValueException);
+
+  const auto &cpv = pv;
+
+  ASSERT_THROW(cpv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueInt(), storage::PropertyValueException);
+  ASSERT_EQ(cpv.ValueDouble(), 123.5);
+  ASSERT_THROW(cpv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueMap(), storage::PropertyValueException);
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "double");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "123.5");
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, StringCopy) {
+  std::string str("nandare");
+  storage::PropertyValue pv(str);
+
+  ASSERT_EQ(str, "nandare");
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::String);
+
+  ASSERT_FALSE(pv.IsNull());
+  ASSERT_FALSE(pv.IsBool());
+  ASSERT_FALSE(pv.IsInt());
+  ASSERT_FALSE(pv.IsDouble());
+  ASSERT_TRUE(pv.IsString());
+  ASSERT_FALSE(pv.IsList());
+  ASSERT_FALSE(pv.IsMap());
+
+  ASSERT_THROW(pv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_EQ(pv.ValueString(), "nandare");
+  ASSERT_THROW(pv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueMap(), storage::PropertyValueException);
+
+  const auto &cpv = pv;
+
+  ASSERT_THROW(cpv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_EQ(cpv.ValueString(), "nandare");
+  ASSERT_THROW(cpv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueMap(), storage::PropertyValueException);
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "string");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "nandare");
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, StringMove) {
+  std::string str("nandare");
+  storage::PropertyValue pv(std::move(str));
+
+  ASSERT_EQ(str, "");
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::String);
+
+  ASSERT_FALSE(pv.IsNull());
+  ASSERT_FALSE(pv.IsBool());
+  ASSERT_FALSE(pv.IsInt());
+  ASSERT_FALSE(pv.IsDouble());
+  ASSERT_TRUE(pv.IsString());
+  ASSERT_FALSE(pv.IsList());
+  ASSERT_FALSE(pv.IsMap());
+
+  ASSERT_THROW(pv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_EQ(pv.ValueString(), "nandare");
+  ASSERT_THROW(pv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueMap(), storage::PropertyValueException);
+
+  const auto &cpv = pv;
+
+  ASSERT_THROW(cpv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_EQ(cpv.ValueString(), "nandare");
+  ASSERT_THROW(cpv.ValueList(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueMap(), storage::PropertyValueException);
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "string");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "nandare");
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, ListCopy) {
+  std::vector<storage::PropertyValue> vec{storage::PropertyValue("nandare"),
+                                          storage::PropertyValue(123)};
+  storage::PropertyValue pv(vec);
+
+  ASSERT_EQ(vec.size(), 2);
+  ASSERT_EQ(vec[0].ValueString(), "nandare");
+  ASSERT_EQ(vec[1].ValueInt(), 123);
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::List);
+
+  ASSERT_FALSE(pv.IsNull());
+  ASSERT_FALSE(pv.IsBool());
+  ASSERT_FALSE(pv.IsInt());
+  ASSERT_FALSE(pv.IsDouble());
+  ASSERT_FALSE(pv.IsString());
+  ASSERT_TRUE(pv.IsList());
+  ASSERT_FALSE(pv.IsMap());
+
+  ASSERT_THROW(pv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueString(), storage::PropertyValueException);
+  {
+    const auto &ret = pv.ValueList();
+    ASSERT_EQ(ret.size(), 2);
+    ASSERT_EQ(ret[0].ValueString(), "nandare");
+    ASSERT_EQ(ret[1].ValueInt(), 123);
+  }
+  ASSERT_THROW(pv.ValueMap(), storage::PropertyValueException);
+
+  const auto &cpv = pv;
+
+  ASSERT_THROW(cpv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueString(), storage::PropertyValueException);
+  {
+    const auto &ret = cpv.ValueList();
+    ASSERT_EQ(ret.size(), 2);
+    ASSERT_EQ(ret[0].ValueString(), "nandare");
+    ASSERT_EQ(ret[1].ValueInt(), 123);
+  }
+  ASSERT_THROW(cpv.ValueMap(), storage::PropertyValueException);
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "list");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "[nandare, 123]");
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, ListMove) {
+  std::vector<storage::PropertyValue> vec{storage::PropertyValue("nandare"),
+                                          storage::PropertyValue(123)};
+  storage::PropertyValue pv(std::move(vec));
+
+  ASSERT_EQ(vec.size(), 0);
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::List);
+
+  ASSERT_FALSE(pv.IsNull());
+  ASSERT_FALSE(pv.IsBool());
+  ASSERT_FALSE(pv.IsInt());
+  ASSERT_FALSE(pv.IsDouble());
+  ASSERT_FALSE(pv.IsString());
+  ASSERT_TRUE(pv.IsList());
+  ASSERT_FALSE(pv.IsMap());
+
+  ASSERT_THROW(pv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueString(), storage::PropertyValueException);
+  {
+    const auto &ret = pv.ValueList();
+    ASSERT_EQ(ret.size(), 2);
+    ASSERT_EQ(ret[0].ValueString(), "nandare");
+    ASSERT_EQ(ret[1].ValueInt(), 123);
+  }
+  ASSERT_THROW(pv.ValueMap(), storage::PropertyValueException);
+
+  const auto &cpv = pv;
+
+  ASSERT_THROW(cpv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueString(), storage::PropertyValueException);
+  {
+    const auto &ret = cpv.ValueList();
+    ASSERT_EQ(ret.size(), 2);
+    ASSERT_EQ(ret[0].ValueString(), "nandare");
+    ASSERT_EQ(ret[1].ValueInt(), 123);
+  }
+  ASSERT_THROW(cpv.ValueMap(), storage::PropertyValueException);
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "list");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "[nandare, 123]");
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, MapCopy) {
+  std::map<std::string, storage::PropertyValue> map{
+      {"nandare", storage::PropertyValue(123)}};
+  storage::PropertyValue pv(map);
+
+  ASSERT_EQ(map.size(), 1);
+  ASSERT_EQ(map.at("nandare").ValueInt(), 123);
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::Map);
+
+  ASSERT_FALSE(pv.IsNull());
+  ASSERT_FALSE(pv.IsBool());
+  ASSERT_FALSE(pv.IsInt());
+  ASSERT_FALSE(pv.IsDouble());
+  ASSERT_FALSE(pv.IsString());
+  ASSERT_FALSE(pv.IsList());
+  ASSERT_TRUE(pv.IsMap());
+
+  ASSERT_THROW(pv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueList(), storage::PropertyValueException);
+  {
+    const auto &ret = pv.ValueMap();
+    ASSERT_EQ(ret.size(), 1);
+    ASSERT_EQ(ret.at("nandare").ValueInt(), 123);
+  }
+
+  const auto &cpv = pv;
+
+  ASSERT_THROW(cpv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueList(), storage::PropertyValueException);
+  {
+    const auto &ret = cpv.ValueMap();
+    ASSERT_EQ(ret.size(), 1);
+    ASSERT_EQ(ret.at("nandare").ValueInt(), 123);
+  }
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "map");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "{nandare: 123}");
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, MapMove) {
+  std::map<std::string, storage::PropertyValue> map{
+      {"nandare", storage::PropertyValue(123)}};
+  storage::PropertyValue pv(std::move(map));
+
+  ASSERT_EQ(map.size(), 0);
+
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::Map);
+
+  ASSERT_FALSE(pv.IsNull());
+  ASSERT_FALSE(pv.IsBool());
+  ASSERT_FALSE(pv.IsInt());
+  ASSERT_FALSE(pv.IsDouble());
+  ASSERT_FALSE(pv.IsString());
+  ASSERT_FALSE(pv.IsList());
+  ASSERT_TRUE(pv.IsMap());
+
+  ASSERT_THROW(pv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(pv.ValueList(), storage::PropertyValueException);
+  {
+    const auto &ret = pv.ValueMap();
+    ASSERT_EQ(ret.size(), 1);
+    ASSERT_EQ(ret.at("nandare").ValueInt(), 123);
+  }
+
+  const auto &cpv = pv;
+
+  ASSERT_THROW(cpv.ValueBool(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueInt(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueDouble(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueString(), storage::PropertyValueException);
+  ASSERT_THROW(cpv.ValueList(), storage::PropertyValueException);
+  {
+    const auto &ret = cpv.ValueMap();
+    ASSERT_EQ(ret.size(), 1);
+    ASSERT_EQ(ret.at("nandare").ValueInt(), 123);
+  }
+
+  {
+    std::stringstream ss;
+    ss << pv.type();
+    ASSERT_EQ(ss.str(), "map");
+  }
+  {
+    std::stringstream ss;
+    ss << pv;
+    ASSERT_EQ(ss.str(), "{nandare: 123}");
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, CopyConstructor) {
+  std::vector<storage::PropertyValue> vec{storage::PropertyValue(true),
+                                          storage::PropertyValue(123)};
+  std::map<std::string, storage::PropertyValue> map{
+      {"nandare", storage::PropertyValue(false)}};
+  std::vector<storage::PropertyValue> data{
+      storage::PropertyValue(),          storage::PropertyValue(true),
+      storage::PropertyValue(123),       storage::PropertyValue(123.5),
+      storage::PropertyValue("nandare"), storage::PropertyValue(vec),
+      storage::PropertyValue(map)};
+  for (const auto &item : data) {
+    storage::PropertyValue pv(item);
+    ASSERT_EQ(pv.type(), item.type());
+    switch (item.type()) {
+      case storage::PropertyValue::Type::Null:
+        ASSERT_TRUE(pv.IsNull());
+        break;
+      case storage::PropertyValue::Type::Bool:
+        ASSERT_EQ(pv.ValueBool(), item.ValueBool());
+        break;
+      case storage::PropertyValue::Type::Int:
+        ASSERT_EQ(pv.ValueInt(), item.ValueInt());
+        break;
+      case storage::PropertyValue::Type::Double:
+        ASSERT_EQ(pv.ValueDouble(), item.ValueDouble());
+        break;
+      case storage::PropertyValue::Type::String:
+        ASSERT_EQ(pv.ValueString(), item.ValueString());
+        break;
+      case storage::PropertyValue::Type::List:
+        ASSERT_EQ(pv.ValueList(), item.ValueList());
+        break;
+      case storage::PropertyValue::Type::Map:
+        ASSERT_EQ(pv.ValueMap(), item.ValueMap());
+        break;
+    }
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, MoveConstructor) {
+  std::vector<storage::PropertyValue> vec{storage::PropertyValue(true),
+                                          storage::PropertyValue(123)};
+  std::map<std::string, storage::PropertyValue> map{
+      {"nandare", storage::PropertyValue(false)}};
+  std::vector<storage::PropertyValue> data{
+      storage::PropertyValue(),          storage::PropertyValue(true),
+      storage::PropertyValue(123),       storage::PropertyValue(123.5),
+      storage::PropertyValue("nandare"), storage::PropertyValue(vec),
+      storage::PropertyValue(map)};
+  for (auto &item : data) {
+    storage::PropertyValue copy(item);
+    storage::PropertyValue pv(std::move(item));
+    ASSERT_EQ(item.type(), storage::PropertyValue::Type::Null);
+    ASSERT_EQ(pv.type(), copy.type());
+    switch (copy.type()) {
+      case storage::PropertyValue::Type::Null:
+        ASSERT_TRUE(pv.IsNull());
+        break;
+      case storage::PropertyValue::Type::Bool:
+        ASSERT_EQ(pv.ValueBool(), copy.ValueBool());
+        break;
+      case storage::PropertyValue::Type::Int:
+        ASSERT_EQ(pv.ValueInt(), copy.ValueInt());
+        break;
+      case storage::PropertyValue::Type::Double:
+        ASSERT_EQ(pv.ValueDouble(), copy.ValueDouble());
+        break;
+      case storage::PropertyValue::Type::String:
+        ASSERT_EQ(pv.ValueString(), copy.ValueString());
+        break;
+      case storage::PropertyValue::Type::List:
+        ASSERT_EQ(pv.ValueList(), copy.ValueList());
+        break;
+      case storage::PropertyValue::Type::Map:
+        ASSERT_EQ(pv.ValueMap(), copy.ValueMap());
+        break;
+    }
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, CopyAssignment) {
+  std::vector<storage::PropertyValue> vec{storage::PropertyValue(true),
+                                          storage::PropertyValue(123)};
+  std::map<std::string, storage::PropertyValue> map{
+      {"nandare", storage::PropertyValue(false)}};
+  std::vector<storage::PropertyValue> data{
+      storage::PropertyValue(),          storage::PropertyValue(true),
+      storage::PropertyValue(123),       storage::PropertyValue(123.5),
+      storage::PropertyValue("nandare"), storage::PropertyValue(vec),
+      storage::PropertyValue(map)};
+  for (const auto &item : data) {
+    storage::PropertyValue pv(123);
+    pv = item;
+    ASSERT_EQ(pv.type(), item.type());
+    switch (item.type()) {
+      case storage::PropertyValue::Type::Null:
+        ASSERT_TRUE(pv.IsNull());
+        break;
+      case storage::PropertyValue::Type::Bool:
+        ASSERT_EQ(pv.ValueBool(), item.ValueBool());
+        break;
+      case storage::PropertyValue::Type::Int:
+        ASSERT_EQ(pv.ValueInt(), item.ValueInt());
+        break;
+      case storage::PropertyValue::Type::Double:
+        ASSERT_EQ(pv.ValueDouble(), item.ValueDouble());
+        break;
+      case storage::PropertyValue::Type::String:
+        ASSERT_EQ(pv.ValueString(), item.ValueString());
+        break;
+      case storage::PropertyValue::Type::List:
+        ASSERT_EQ(pv.ValueList(), item.ValueList());
+        break;
+      case storage::PropertyValue::Type::Map:
+        ASSERT_EQ(pv.ValueMap(), item.ValueMap());
+        break;
+    }
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, MoveAssignment) {
+  std::vector<storage::PropertyValue> vec{storage::PropertyValue(true),
+                                          storage::PropertyValue(123)};
+  std::map<std::string, storage::PropertyValue> map{
+      {"nandare", storage::PropertyValue(false)}};
+  std::vector<storage::PropertyValue> data{
+      storage::PropertyValue(),          storage::PropertyValue(true),
+      storage::PropertyValue(123),       storage::PropertyValue(123.5),
+      storage::PropertyValue("nandare"), storage::PropertyValue(vec),
+      storage::PropertyValue(map)};
+  for (auto &item : data) {
+    storage::PropertyValue copy(item);
+    storage::PropertyValue pv(123);
+    pv = std::move(item);
+    ASSERT_EQ(item.type(), storage::PropertyValue::Type::Null);
+    ASSERT_EQ(pv.type(), copy.type());
+    switch (copy.type()) {
+      case storage::PropertyValue::Type::Null:
+        ASSERT_TRUE(pv.IsNull());
+        break;
+      case storage::PropertyValue::Type::Bool:
+        ASSERT_EQ(pv.ValueBool(), copy.ValueBool());
+        break;
+      case storage::PropertyValue::Type::Int:
+        ASSERT_EQ(pv.ValueInt(), copy.ValueInt());
+        break;
+      case storage::PropertyValue::Type::Double:
+        ASSERT_EQ(pv.ValueDouble(), copy.ValueDouble());
+        break;
+      case storage::PropertyValue::Type::String:
+        ASSERT_EQ(pv.ValueString(), copy.ValueString());
+        break;
+      case storage::PropertyValue::Type::List:
+        ASSERT_EQ(pv.ValueList(), copy.ValueList());
+        break;
+      case storage::PropertyValue::Type::Map:
+        ASSERT_EQ(pv.ValueMap(), copy.ValueMap());
+        break;
+    }
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, CopyAssignmentSelf) {
+  storage::PropertyValue pv("nandare");
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wself-assign-overloaded"
+  pv = pv;
+#pragma clang diagnostic pop
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::String);
+  ASSERT_EQ(pv.ValueString(), "nandare");
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, MoveAssignmentSelf) {
+  storage::PropertyValue pv("nandare");
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wself-move"
+  pv = std::move(pv);
+#pragma clang diagnostic pop
+  ASSERT_EQ(pv.type(), storage::PropertyValue::Type::String);
+  ASSERT_EQ(pv.ValueString(), "nandare");
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, Equal) {
+  std::vector<storage::PropertyValue> vec{storage::PropertyValue(true),
+                                          storage::PropertyValue(123)};
+  std::map<std::string, storage::PropertyValue> map{
+      {"nandare", storage::PropertyValue(false)}};
+  std::vector<storage::PropertyValue> data{
+      storage::PropertyValue(),          storage::PropertyValue(true),
+      storage::PropertyValue(123),       storage::PropertyValue(123.5),
+      storage::PropertyValue("nandare"), storage::PropertyValue(vec),
+      storage::PropertyValue(map)};
+  for (const auto item1 : data) {
+    for (const auto item2 : data) {
+      if (item1.type() == item2.type()) {
+        ASSERT_TRUE(item1 == item2);
+      } else {
+        ASSERT_FALSE(item1 == item2);
+      }
+    }
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST(PropertyValue, Less) {
+  std::vector<storage::PropertyValue> vec{storage::PropertyValue(true),
+                                          storage::PropertyValue(123)};
+  std::map<std::string, storage::PropertyValue> map{
+      {"nandare", storage::PropertyValue(false)}};
+  std::vector<storage::PropertyValue> data{
+      storage::PropertyValue(),          storage::PropertyValue(true),
+      storage::PropertyValue(123),       storage::PropertyValue(123.5),
+      storage::PropertyValue("nandare"), storage::PropertyValue(vec),
+      storage::PropertyValue(map)};
+  for (size_t i = 0; i < data.size(); ++i) {
+    for (size_t j = 0; j < data.size(); ++j) {
+      auto item1 = data[i];
+      auto item2 = data[j];
+      if (i < j) {
+        ASSERT_TRUE(item1 < item2);
+      } else {
+        ASSERT_FALSE(item1 < item2);
+      }
+    }
+  }
+}