diff --git a/src/storage/v3/CMakeLists.txt b/src/storage/v3/CMakeLists.txt
index e9322b45a..723fd406f 100644
--- a/src/storage/v3/CMakeLists.txt
+++ b/src/storage/v3/CMakeLists.txt
@@ -8,6 +8,7 @@ set(storage_v3_src_files
     durability/wal.cpp
     edge_accessor.cpp
     indices.cpp
+    key_store.cpp
     property_store.cpp
     vertex_accessor.cpp
     storage.cpp)
diff --git a/src/storage/v3/constraints.cpp b/src/storage/v3/constraints.cpp
index e04fa4070..05bf7f0a6 100644
--- a/src/storage/v3/constraints.cpp
+++ b/src/storage/v3/constraints.cpp
@@ -279,7 +279,7 @@ void UniqueConstraints::UpdateBeforeCommit(const Vertex *vertex, const Transacti
 }
 
 utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus> UniqueConstraints::CreateConstraint(
-    LabelId label, const std::set<PropertyId> &properties, utils::SkipList<Vertex>::Accessor vertices) {
+    LabelId label, const std::set<PropertyId> &properties, VerticesSkipList::Accessor vertices) {
   if (properties.empty()) {
     return CreationStatus::EMPTY_PROPERTIES;
   }
@@ -300,7 +300,8 @@ utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus> Uniqu
   {
     auto acc = constraint->second.access();
 
-    for (const Vertex &vertex : vertices) {
+    for (const auto &lo_vertex : vertices) {
+      const auto &vertex = lo_vertex.vertex;
       if (vertex.deleted || !utils::Contains(vertex.labels, label)) {
         continue;
       }
diff --git a/src/storage/v3/constraints.hpp b/src/storage/v3/constraints.hpp
index 3c0715c4d..d127df4fe 100644
--- a/src/storage/v3/constraints.hpp
+++ b/src/storage/v3/constraints.hpp
@@ -18,6 +18,7 @@
 #include "storage/v3/id_types.hpp"
 #include "storage/v3/transaction.hpp"
 #include "storage/v3/vertex.hpp"
+#include "storage/v3/vertices_skip_list.hpp"
 #include "utils/logging.hpp"
 #include "utils/result.hpp"
 #include "utils/skip_list.hpp"
@@ -108,7 +109,7 @@ class UniqueConstraints {
   /// @throw std::bad_alloc
   utils::BasicResult<ConstraintViolation, CreationStatus> CreateConstraint(LabelId label,
                                                                            const std::set<PropertyId> &properties,
-                                                                           utils::SkipList<Vertex>::Accessor vertices);
+                                                                           VerticesSkipList::Accessor vertices);
 
   /// Deletes the specified constraint. Returns `DeletionStatus::NOT_FOUND` if
   /// there is not such constraint in the storage,
@@ -152,12 +153,15 @@ struct Constraints {
 ///
 /// @throw std::bad_alloc
 /// @throw std::length_error
-inline utils::BasicResult<ConstraintViolation, bool> CreateExistenceConstraint(
-    Constraints *constraints, LabelId label, PropertyId property, utils::SkipList<Vertex>::Accessor vertices) {
+inline utils::BasicResult<ConstraintViolation, bool> CreateExistenceConstraint(Constraints *constraints, LabelId label,
+                                                                               PropertyId property,
+                                                                               VerticesSkipList::Accessor vertices) {
   if (utils::Contains(constraints->existence_constraints, std::make_pair(label, property))) {
     return false;
   }
-  for (const auto &vertex : vertices) {
+  for (const auto &lgo_vertex : vertices) {
+    const auto &vertex = lgo_vertex.vertex;
+
     if (!vertex.deleted && utils::Contains(vertex.labels, label) && !vertex.properties.HasProperty(property)) {
       return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label, std::set<PropertyId>{property}};
     }
diff --git a/src/storage/v3/durability/durability.cpp b/src/storage/v3/durability/durability.cpp
index 8be9a8b17..a96594258 100644
--- a/src/storage/v3/durability/durability.cpp
+++ b/src/storage/v3/durability/durability.cpp
@@ -113,7 +113,7 @@ std::optional<std::vector<WalDurabilityInfo>> GetWalFiles(const std::filesystem:
 // to ensure that the indices and constraints are consistent at the end of the
 // recovery process.
 void RecoverIndicesAndConstraints(const RecoveredIndicesAndConstraints &indices_constraints, Indices *indices,
-                                  Constraints *constraints, utils::SkipList<Vertex> *vertices) {
+                                  Constraints *constraints, VerticesSkipList *vertices) {
   spdlog::info("Recreating indices from metadata.");
   // Recover label indices.
   spdlog::info("Recreating {} label indices from metadata.", indices_constraints.indices.label.size());
@@ -157,14 +157,11 @@ void RecoverIndicesAndConstraints(const RecoveredIndicesAndConstraints &indices_
   spdlog::info("Constraints are recreated from metadata.");
 }
 
-std::optional<RecoveryInfo> RecoverData(const std::filesystem::path &snapshot_directory,
-                                        const std::filesystem::path &wal_directory, std::string *uuid,
-                                        std::string *epoch_id,
-                                        std::deque<std::pair<std::string, uint64_t>> *epoch_history,
-                                        utils::SkipList<Vertex> *vertices, utils::SkipList<Edge> *edges,
-                                        std::atomic<uint64_t> *edge_count, NameIdMapper *name_id_mapper,
-                                        Indices *indices, Constraints *constraints, Config::Items items,
-                                        uint64_t *wal_seq_num) {
+std::optional<RecoveryInfo> RecoverData(
+    const std::filesystem::path &snapshot_directory, const std::filesystem::path &wal_directory, std::string *uuid,
+    std::string *epoch_id, std::deque<std::pair<std::string, uint64_t>> *epoch_history, VerticesSkipList *vertices,
+    utils::SkipList<Edge> *edges, std::atomic<uint64_t> *edge_count, NameIdMapper *name_id_mapper, Indices *indices,
+    Constraints *constraints, Config::Items items, uint64_t *wal_seq_num) {
   utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
   spdlog::info("Recovering persisted data using snapshot ({}) and WAL directory ({}).", snapshot_directory,
                wal_directory);
diff --git a/src/storage/v3/durability/durability.hpp b/src/storage/v3/durability/durability.hpp
index 3b0266062..b7fc39005 100644
--- a/src/storage/v3/durability/durability.hpp
+++ b/src/storage/v3/durability/durability.hpp
@@ -97,18 +97,15 @@ std::optional<std::vector<WalDurabilityInfo>> GetWalFiles(const std::filesystem:
 // recovery process.
 /// @throw RecoveryFailure
 void RecoverIndicesAndConstraints(const RecoveredIndicesAndConstraints &indices_constraints, Indices *indices,
-                                  Constraints *constraints, utils::SkipList<Vertex> *vertices);
+                                  Constraints *constraints, VerticesSkipList *vertices);
 
 /// Recovers data either from a snapshot and/or WAL files.
 /// @throw RecoveryFailure
 /// @throw std::bad_alloc
-std::optional<RecoveryInfo> RecoverData(const std::filesystem::path &snapshot_directory,
-                                        const std::filesystem::path &wal_directory, std::string *uuid,
-                                        std::string *epoch_id,
-                                        std::deque<std::pair<std::string, uint64_t>> *epoch_history,
-                                        utils::SkipList<Vertex> *vertices, utils::SkipList<Edge> *edges,
-                                        std::atomic<uint64_t> *edge_count, NameIdMapper *name_id_mapper,
-                                        Indices *indices, Constraints *constraints, Config::Items items,
-                                        uint64_t *wal_seq_num);
+std::optional<RecoveryInfo> RecoverData(
+    const std::filesystem::path &snapshot_directory, const std::filesystem::path &wal_directory, std::string *uuid,
+    std::string *epoch_id, std::deque<std::pair<std::string, uint64_t>> *epoch_history, VerticesSkipList *vertices,
+    utils::SkipList<Edge> *edges, std::atomic<uint64_t> *edge_count, NameIdMapper *name_id_mapper, Indices *indices,
+    Constraints *constraints, Config::Items items, uint64_t *wal_seq_num);
 
 }  // namespace memgraph::storage::v3::durability
diff --git a/src/storage/v3/durability/snapshot.cpp b/src/storage/v3/durability/snapshot.cpp
index 48b4184ad..ee54f69d2 100644
--- a/src/storage/v3/durability/snapshot.cpp
+++ b/src/storage/v3/durability/snapshot.cpp
@@ -20,6 +20,7 @@
 #include "storage/v3/edge_ref.hpp"
 #include "storage/v3/mvcc.hpp"
 #include "storage/v3/vertex_accessor.hpp"
+#include "storage/v3/vertices_skip_list.hpp"
 #include "utils/file_locker.hpp"
 #include "utils/logging.hpp"
 #include "utils/message.hpp"
@@ -157,7 +158,7 @@ SnapshotInfo ReadSnapshotInfo(const std::filesystem::path &path) {
   return info;
 }
 
-RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipList<Vertex> *vertices,
+RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, VerticesSkipList *vertices,
                                utils::SkipList<Edge> *edges,
                                std::deque<std::pair<std::string, uint64_t>> *epoch_history,
                                NameIdMapper *name_id_mapper, std::atomic<uint64_t> *edge_count, Config::Items items) {
@@ -305,7 +306,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
       }
       last_vertex_gid = *gid;
       spdlog::debug("Recovering vertex {}.", *gid);
-      auto [it, inserted] = vertex_acc.insert(Vertex{Gid::FromUint(*gid), nullptr});
+      auto [it, inserted] = vertex_acc.insert({Vertex{Gid::FromUint(*gid), nullptr}});
       if (!inserted) throw RecoveryFailure("The vertex must be inserted here!");
 
       // Recover labels.
@@ -313,7 +314,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
       {
         auto labels_size = snapshot.ReadUint();
         if (!labels_size) throw RecoveryFailure("Invalid snapshot data!");
-        auto &labels = it->labels;
+        auto &labels = it->vertex.labels;
         labels.reserve(*labels_size);
         for (uint64_t j = 0; j < *labels_size; ++j) {
           auto label = snapshot.ReadUint();
@@ -329,7 +330,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
       {
         auto props_size = snapshot.ReadUint();
         if (!props_size) throw RecoveryFailure("Invalid snapshot data!");
-        auto &props = it->properties;
+        auto &props = it->vertex.properties;
         for (uint64_t j = 0; j < *props_size; ++j) {
           auto key = snapshot.ReadUint();
           if (!key) throw RecoveryFailure("Invalid snapshot data!");
@@ -372,17 +373,18 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
     // Recover vertices (in/out edges).
     spdlog::info("Recovering connectivity.");
     if (!snapshot.SetPosition(info.offset_vertices)) throw RecoveryFailure("Couldn't read data from snapshot!");
-    for (auto &vertex : vertex_acc) {
+    for (auto &lgo_vertex : vertex_acc) {
+      auto &vertex = lgo_vertex.vertex;
       {
         auto marker = snapshot.ReadMarker();
         if (!marker || *marker != Marker::SECTION_VERTEX) throw RecoveryFailure("Invalid snapshot data!");
       }
 
-      spdlog::trace("Recovering connectivity for vertex {}.", vertex.gid.AsUint());
+      spdlog::trace("Recovering connectivity for vertex {}.", vertex.Gid().AsUint());
       // Check vertex.
       auto gid = snapshot.ReadUint();
       if (!gid) throw RecoveryFailure("Invalid snapshot data!");
-      if (gid != vertex.gid.AsUint()) throw RecoveryFailure("Invalid snapshot data!");
+      if (gid != vertex.Gid().AsUint()) throw RecoveryFailure("Invalid snapshot data!");
 
       // Skip labels.
       {
@@ -408,7 +410,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
 
       // Recover in edges.
       {
-        spdlog::trace("Recovering inbound edges for vertex {}.", vertex.gid.AsUint());
+        spdlog::trace("Recovering inbound edges for vertex {}.", vertex.Gid().AsUint());
         auto in_size = snapshot.ReadUint();
         if (!in_size) throw RecoveryFailure("Invalid snapshot data!");
         vertex.in_edges.reserve(*in_size);
@@ -422,7 +424,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
           auto edge_type = snapshot.ReadUint();
           if (!edge_type) throw RecoveryFailure("Invalid snapshot data!");
 
-          auto from_vertex = vertex_acc.find(Gid::FromUint(*from_gid));
+          auto from_vertex = vertex_acc.find(std::vector{PropertyValue{Gid::FromUint(*from_gid).AsInt()}});
           if (from_vertex == vertex_acc.end()) throw RecoveryFailure("Invalid from vertex!");
 
           EdgeRef edge_ref(Gid::FromUint(*edge_gid));
@@ -437,14 +439,14 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
             }
           }
           SPDLOG_TRACE("Recovered inbound edge {} with label \"{}\" from vertex {}.", *edge_gid,
-                       name_id_mapper->IdToName(snapshot_id_map.at(*edge_type)), from_vertex->gid.AsUint());
-          vertex.in_edges.emplace_back(get_edge_type_from_id(*edge_type), &*from_vertex, edge_ref);
+                       name_id_mapper->IdToName(snapshot_id_map.at(*edge_type)), from_vertex->vertex.Gid().AsUint());
+          vertex.in_edges.emplace_back(get_edge_type_from_id(*edge_type), &from_vertex->vertex, edge_ref);
         }
       }
 
       // Recover out edges.
       {
-        spdlog::trace("Recovering outbound edges for vertex {}.", vertex.gid.AsUint());
+        spdlog::trace("Recovering outbound edges for vertex {}.", vertex.Gid().AsUint());
         auto out_size = snapshot.ReadUint();
         if (!out_size) throw RecoveryFailure("Invalid snapshot data!");
         vertex.out_edges.reserve(*out_size);
@@ -458,7 +460,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
           auto edge_type = snapshot.ReadUint();
           if (!edge_type) throw RecoveryFailure("Invalid snapshot data!");
 
-          auto to_vertex = vertex_acc.find(Gid::FromUint(*to_gid));
+          auto to_vertex = vertex_acc.find(std::vector{PropertyValue{Gid::FromUint(*to_gid).AsInt()}});
           if (to_vertex == vertex_acc.end()) throw RecoveryFailure("Invalid to vertex!");
 
           EdgeRef edge_ref(Gid::FromUint(*edge_gid));
@@ -473,8 +475,8 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
             }
           }
           SPDLOG_TRACE("Recovered outbound edge {} with label \"{}\" to vertex {}.", *edge_gid,
-                       name_id_mapper->IdToName(snapshot_id_map.at(*edge_type)), to_vertex->gid.AsUint());
-          vertex.out_edges.emplace_back(get_edge_type_from_id(*edge_type), &*to_vertex, edge_ref);
+                       name_id_mapper->IdToName(snapshot_id_map.at(*edge_type)), to_vertex->vertex.Gid().AsUint());
+          vertex.out_edges.emplace_back(get_edge_type_from_id(*edge_type), &to_vertex->vertex, edge_ref);
         }
         // Increment edge count. We only increment the count here because the
         // information is duplicated in in_edges.
@@ -627,7 +629,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
 
 void CreateSnapshot(Transaction *transaction, const std::filesystem::path &snapshot_directory,
                     const std::filesystem::path &wal_directory, uint64_t snapshot_retention_count,
-                    utils::SkipList<Vertex> *vertices, utils::SkipList<Edge> *edges, NameIdMapper *name_id_mapper,
+                    VerticesSkipList *vertices, utils::SkipList<Edge> *edges, NameIdMapper *name_id_mapper,
                     Indices *indices, Constraints *constraints, Config::Items items, const std::string &uuid,
                     const std::string_view epoch_id, const std::deque<std::pair<std::string, uint64_t>> &epoch_history,
                     utils::FileRetainer *file_retainer) {
@@ -740,9 +742,9 @@ void CreateSnapshot(Transaction *transaction, const std::filesystem::path &snaps
   {
     offset_vertices = snapshot.GetPosition();
     auto acc = vertices->access();
-    for (auto &vertex : acc) {
+    for (auto &lgo_vertex : acc) {
       // The visibility check is implemented for vertices so we use it here.
-      auto va = VertexAccessor::Create(&vertex, transaction, indices, constraints, items, View::OLD);
+      auto va = VertexAccessor::Create(&lgo_vertex.vertex, transaction, indices, constraints, items, View::OLD);
       if (!va) continue;
 
       // Get vertex data.
@@ -760,7 +762,7 @@ void CreateSnapshot(Transaction *transaction, const std::filesystem::path &snaps
       // Store the vertex.
       {
         snapshot.WriteMarker(Marker::SECTION_VERTEX);
-        snapshot.WriteUint(vertex.gid.AsUint());
+        snapshot.WriteUint(lgo_vertex.vertex.Gid().AsUint());
         const auto &labels = maybe_labels.GetValue();
         snapshot.WriteUint(labels.size());
         for (const auto &item : labels) {
diff --git a/src/storage/v3/durability/snapshot.hpp b/src/storage/v3/durability/snapshot.hpp
index 785ca7ed2..0ed81d1e2 100644
--- a/src/storage/v3/durability/snapshot.hpp
+++ b/src/storage/v3/durability/snapshot.hpp
@@ -59,7 +59,7 @@ SnapshotInfo ReadSnapshotInfo(const std::filesystem::path &path);
 
 /// Function used to load the snapshot data into the storage.
 /// @throw RecoveryFailure
-RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipList<Vertex> *vertices,
+RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, VerticesSkipList *vertices,
                                utils::SkipList<Edge> *edges,
                                std::deque<std::pair<std::string, uint64_t>> *epoch_history,
                                NameIdMapper *name_id_mapper, std::atomic<uint64_t> *edge_count, Config::Items items);
@@ -67,7 +67,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
 /// Function used to create a snapshot using the given transaction.
 void CreateSnapshot(Transaction *transaction, const std::filesystem::path &snapshot_directory,
                     const std::filesystem::path &wal_directory, uint64_t snapshot_retention_count,
-                    utils::SkipList<Vertex> *vertices, utils::SkipList<Edge> *edges, NameIdMapper *name_id_mapper,
+                    VerticesSkipList *vertices, utils::SkipList<Edge> *edges, NameIdMapper *name_id_mapper,
                     Indices *indices, Constraints *constraints, Config::Items items, const std::string &uuid,
                     std::string_view epoch_id, const std::deque<std::pair<std::string, uint64_t>> &epoch_history,
                     utils::FileRetainer *file_retainer);
diff --git a/src/storage/v3/durability/wal.cpp b/src/storage/v3/durability/wal.cpp
index 2cba7c07d..0699cca71 100644
--- a/src/storage/v3/durability/wal.cpp
+++ b/src/storage/v3/durability/wal.cpp
@@ -17,6 +17,7 @@
 #include "storage/v3/durability/version.hpp"
 #include "storage/v3/edge.hpp"
 #include "storage/v3/vertex.hpp"
+#include "storage/v3/vertices_skip_list.hpp"
 #include "utils/file_locker.hpp"
 #include "utils/logging.hpp"
 
@@ -492,12 +493,12 @@ void EncodeDelta(BaseEncoder *encoder, NameIdMapper *name_id_mapper, Config::Ite
     case Delta::Action::DELETE_OBJECT:
     case Delta::Action::RECREATE_OBJECT: {
       encoder->WriteMarker(VertexActionToMarker(delta.action));
-      encoder->WriteUint(vertex.gid.AsUint());
+      encoder->WriteUint(vertex.Gid().AsUint());
       break;
     }
     case Delta::Action::SET_PROPERTY: {
       encoder->WriteMarker(Marker::DELTA_VERTEX_SET_PROPERTY);
-      encoder->WriteUint(vertex.gid.AsUint());
+      encoder->WriteUint(vertex.Gid().AsUint());
       encoder->WriteString(name_id_mapper->IdToName(delta.property.key.AsUint()));
       // The property value is the value that is currently stored in the
       // vertex.
@@ -510,7 +511,7 @@ void EncodeDelta(BaseEncoder *encoder, NameIdMapper *name_id_mapper, Config::Ite
     case Delta::Action::ADD_LABEL:
     case Delta::Action::REMOVE_LABEL: {
       encoder->WriteMarker(VertexActionToMarker(delta.action));
-      encoder->WriteUint(vertex.gid.AsUint());
+      encoder->WriteUint(vertex.Gid().AsUint());
       encoder->WriteString(name_id_mapper->IdToName(delta.label.AsUint()));
       break;
     }
@@ -523,8 +524,8 @@ void EncodeDelta(BaseEncoder *encoder, NameIdMapper *name_id_mapper, Config::Ite
         encoder->WriteUint(delta.vertex_edge.edge.gid.AsUint());
       }
       encoder->WriteString(name_id_mapper->IdToName(delta.vertex_edge.edge_type.AsUint()));
-      encoder->WriteUint(vertex.gid.AsUint());
-      encoder->WriteUint(delta.vertex_edge.vertex->gid.AsUint());
+      encoder->WriteUint(vertex.Gid().AsUint());
+      encoder->WriteUint(delta.vertex_edge.vertex->Gid().AsUint());
       break;
     }
     case Delta::Action::ADD_IN_EDGE:
@@ -617,7 +618,7 @@ void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, Storage
 }
 
 RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConstraints *indices_constraints,
-                     const std::optional<uint64_t> last_loaded_timestamp, utils::SkipList<Vertex> *vertices,
+                     const std::optional<uint64_t> last_loaded_timestamp, VerticesSkipList *vertices,
                      utils::SkipList<Edge> *edges, NameIdMapper *name_id_mapper, std::atomic<uint64_t> *edge_count,
                      Config::Items items) {
   spdlog::info("Trying to load WAL file {}.", path);
@@ -653,7 +654,7 @@ RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConst
       auto delta = ReadWalDeltaData(&wal);
       switch (delta.type) {
         case WalDeltaData::Type::VERTEX_CREATE: {
-          auto [vertex, inserted] = vertex_acc.insert(Vertex{delta.vertex_create_delete.gid, nullptr});
+          auto [vertex, inserted] = vertex_acc.insert({Vertex{delta.vertex_create_delete.gid, nullptr}});
           if (!inserted) throw RecoveryFailure("The vertex must be inserted here!");
 
           ret.next_vertex_id = std::max(ret.next_vertex_id, delta.vertex_create_delete.gid.AsUint() + 1);
@@ -661,51 +662,66 @@ RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConst
           break;
         }
         case WalDeltaData::Type::VERTEX_DELETE: {
-          auto vertex = vertex_acc.find(delta.vertex_create_delete.gid);
-          if (vertex == vertex_acc.end()) throw RecoveryFailure("The vertex doesn't exist!");
-          if (!vertex->in_edges.empty() || !vertex->out_edges.empty())
+          auto lgo_vertex_it = vertex_acc.find(std::vector{PropertyValue{delta.vertex_create_delete.gid.AsInt()}});
+          if (lgo_vertex_it == vertex_acc.end()) {
+            throw RecoveryFailure("The vertex doesn't exist!");
+          }
+          auto &vertex = lgo_vertex_it->vertex;
+          if (!vertex.in_edges.empty() || !vertex.out_edges.empty())
             throw RecoveryFailure("The vertex can't be deleted because it still has edges!");
 
-          if (!vertex_acc.remove(delta.vertex_create_delete.gid))
+          if (!vertex_acc.remove(std::vector{PropertyValue{delta.vertex_create_delete.gid.AsInt()}}))
             throw RecoveryFailure("The vertex must be removed here!");
 
           break;
         }
         case WalDeltaData::Type::VERTEX_ADD_LABEL:
         case WalDeltaData::Type::VERTEX_REMOVE_LABEL: {
-          auto vertex = vertex_acc.find(delta.vertex_add_remove_label.gid);
-          if (vertex == vertex_acc.end()) throw RecoveryFailure("The vertex doesn't exist!");
+          auto lgo_vertex_it = vertex_acc.find(std::vector{PropertyValue{delta.vertex_add_remove_label.gid.AsInt()}});
+          if (lgo_vertex_it == vertex_acc.end()) {
+            throw RecoveryFailure("The vertex doesn't exist!");
+          }
+          auto &vertex = lgo_vertex_it->vertex;
 
           auto label_id = LabelId::FromUint(name_id_mapper->NameToId(delta.vertex_add_remove_label.label));
-          auto it = std::find(vertex->labels.begin(), vertex->labels.end(), label_id);
+          auto it = std::find(vertex.labels.begin(), vertex.labels.end(), label_id);
 
           if (delta.type == WalDeltaData::Type::VERTEX_ADD_LABEL) {
-            if (it != vertex->labels.end()) throw RecoveryFailure("The vertex already has the label!");
-            vertex->labels.push_back(label_id);
+            if (it != vertex.labels.end()) throw RecoveryFailure("The vertex already has the label!");
+            vertex.labels.push_back(label_id);
           } else {
-            if (it == vertex->labels.end()) throw RecoveryFailure("The vertex doesn't have the label!");
-            std::swap(*it, vertex->labels.back());
-            vertex->labels.pop_back();
+            if (it == vertex.labels.end()) throw RecoveryFailure("The vertex doesn't have the label!");
+            std::swap(*it, vertex.labels.back());
+            vertex.labels.pop_back();
           }
 
           break;
         }
         case WalDeltaData::Type::VERTEX_SET_PROPERTY: {
-          auto vertex = vertex_acc.find(delta.vertex_edge_set_property.gid);
-          if (vertex == vertex_acc.end()) throw RecoveryFailure("The vertex doesn't exist!");
+          auto lgo_vertex_it = vertex_acc.find(std::vector{PropertyValue{delta.vertex_edge_set_property.gid.AsInt()}});
+          if (lgo_vertex_it == vertex_acc.end()) {
+            throw RecoveryFailure("The vertex doesn't exist!");
+          }
 
           auto property_id = PropertyId::FromUint(name_id_mapper->NameToId(delta.vertex_edge_set_property.property));
           auto &property_value = delta.vertex_edge_set_property.value;
 
-          vertex->properties.SetProperty(property_id, property_value);
+          lgo_vertex_it->vertex.properties.SetProperty(property_id, property_value);
 
           break;
         }
         case WalDeltaData::Type::EDGE_CREATE: {
-          auto from_vertex = vertex_acc.find(delta.edge_create_delete.from_vertex);
-          if (from_vertex == vertex_acc.end()) throw RecoveryFailure("The from vertex doesn't exist!");
-          auto to_vertex = vertex_acc.find(delta.edge_create_delete.to_vertex);
-          if (to_vertex == vertex_acc.end()) throw RecoveryFailure("The to vertex doesn't exist!");
+          auto from_lgo_vertex =
+              vertex_acc.find(std::vector{PropertyValue{delta.edge_create_delete.from_vertex.AsInt()}});
+          if (from_lgo_vertex == vertex_acc.end()) {
+            throw RecoveryFailure("The from vertex doesn't exist!");
+          }
+          auto to_lgo_vertex = vertex_acc.find(std::vector{PropertyValue{delta.edge_create_delete.to_vertex.AsInt()}});
+          if (to_lgo_vertex == vertex_acc.end()) {
+            throw RecoveryFailure("The to vertex doesn't exist!");
+          }
+          auto &from_vertex = from_lgo_vertex->vertex;
+          auto &to_vertex = to_lgo_vertex->vertex;
 
           auto edge_gid = delta.edge_create_delete.gid;
           auto edge_type_id = EdgeTypeId::FromUint(name_id_mapper->NameToId(delta.edge_create_delete.edge_type));
@@ -716,16 +732,16 @@ RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConst
             edge_ref = EdgeRef(&*edge);
           }
           {
-            std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{edge_type_id, &*to_vertex, edge_ref};
-            auto it = std::find(from_vertex->out_edges.begin(), from_vertex->out_edges.end(), link);
-            if (it != from_vertex->out_edges.end()) throw RecoveryFailure("The from vertex already has this edge!");
-            from_vertex->out_edges.push_back(link);
+            std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{edge_type_id, &to_vertex, edge_ref};
+            auto it = std::find(from_vertex.out_edges.begin(), from_vertex.out_edges.end(), link);
+            if (it != from_vertex.out_edges.end()) throw RecoveryFailure("The from vertex already has this edge!");
+            from_vertex.out_edges.push_back(link);
           }
           {
-            std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{edge_type_id, &*from_vertex, edge_ref};
-            auto it = std::find(to_vertex->in_edges.begin(), to_vertex->in_edges.end(), link);
-            if (it != to_vertex->in_edges.end()) throw RecoveryFailure("The to vertex already has this edge!");
-            to_vertex->in_edges.push_back(link);
+            std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{edge_type_id, &from_vertex, edge_ref};
+            auto it = std::find(to_vertex.in_edges.begin(), to_vertex.in_edges.end(), link);
+            if (it != to_vertex.in_edges.end()) throw RecoveryFailure("The to vertex already has this edge!");
+            to_vertex.in_edges.push_back(link);
           }
 
           ret.next_edge_id = std::max(ret.next_edge_id, edge_gid.AsUint() + 1);
@@ -736,10 +752,17 @@ RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConst
           break;
         }
         case WalDeltaData::Type::EDGE_DELETE: {
-          auto from_vertex = vertex_acc.find(delta.edge_create_delete.from_vertex);
-          if (from_vertex == vertex_acc.end()) throw RecoveryFailure("The from vertex doesn't exist!");
-          auto to_vertex = vertex_acc.find(delta.edge_create_delete.to_vertex);
-          if (to_vertex == vertex_acc.end()) throw RecoveryFailure("The to vertex doesn't exist!");
+          auto from_lgo_vertex =
+              vertex_acc.find(std::vector{PropertyValue{delta.edge_create_delete.from_vertex.AsInt()}});
+          if (from_lgo_vertex == vertex_acc.end()) {
+            throw RecoveryFailure("The from vertex doesn't exist!");
+          }
+          auto to_lgo_vertex = vertex_acc.find(std::vector{PropertyValue{delta.edge_create_delete.to_vertex.AsInt()}});
+          if (to_lgo_vertex == vertex_acc.end()) {
+            throw RecoveryFailure("The to vertex doesn't exist!");
+          }
+          auto &from_vertex = from_lgo_vertex->vertex;
+          auto &to_vertex = to_lgo_vertex->vertex;
 
           auto edge_gid = delta.edge_create_delete.gid;
           auto edge_type_id = EdgeTypeId::FromUint(name_id_mapper->NameToId(delta.edge_create_delete.edge_type));
@@ -750,18 +773,18 @@ RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConst
             edge_ref = EdgeRef(&*edge);
           }
           {
-            std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{edge_type_id, &*to_vertex, edge_ref};
-            auto it = std::find(from_vertex->out_edges.begin(), from_vertex->out_edges.end(), link);
-            if (it == from_vertex->out_edges.end()) throw RecoveryFailure("The from vertex doesn't have this edge!");
-            std::swap(*it, from_vertex->out_edges.back());
-            from_vertex->out_edges.pop_back();
+            std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{edge_type_id, &to_vertex, edge_ref};
+            auto it = std::find(from_vertex.out_edges.begin(), from_vertex.out_edges.end(), link);
+            if (it == from_vertex.out_edges.end()) throw RecoveryFailure("The from vertex doesn't have this edge!");
+            std::swap(*it, from_vertex.out_edges.back());
+            from_vertex.out_edges.pop_back();
           }
           {
-            std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{edge_type_id, &*from_vertex, edge_ref};
-            auto it = std::find(to_vertex->in_edges.begin(), to_vertex->in_edges.end(), link);
-            if (it == to_vertex->in_edges.end()) throw RecoveryFailure("The to vertex doesn't have this edge!");
-            std::swap(*it, to_vertex->in_edges.back());
-            to_vertex->in_edges.pop_back();
+            std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{edge_type_id, &from_vertex, edge_ref};
+            auto it = std::find(to_vertex.in_edges.begin(), to_vertex.in_edges.end(), link);
+            if (it == to_vertex.in_edges.end()) throw RecoveryFailure("The to vertex doesn't have this edge!");
+            std::swap(*it, to_vertex.in_edges.back());
+            to_vertex.in_edges.pop_back();
           }
           if (items.properties_on_edges) {
             if (!edge_acc.remove(edge_gid)) throw RecoveryFailure("The edge must be removed here!");
diff --git a/src/storage/v3/durability/wal.hpp b/src/storage/v3/durability/wal.hpp
index 91b1f5b14..0e80134a5 100644
--- a/src/storage/v3/durability/wal.hpp
+++ b/src/storage/v3/durability/wal.hpp
@@ -25,6 +25,7 @@
 #include "storage/v3/name_id_mapper.hpp"
 #include "storage/v3/property_value.hpp"
 #include "storage/v3/vertex.hpp"
+#include "storage/v3/vertices_skip_list.hpp"
 #include "utils/file_locker.hpp"
 #include "utils/skip_list.hpp"
 
@@ -189,7 +190,7 @@ void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, Storage
 /// Function used to load the WAL data into the storage.
 /// @throw RecoveryFailure
 RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConstraints *indices_constraints,
-                     std::optional<uint64_t> last_loaded_timestamp, utils::SkipList<Vertex> *vertices,
+                     std::optional<uint64_t> last_loaded_timestamp, VerticesSkipList *vertices,
                      utils::SkipList<Edge> *edges, NameIdMapper *name_id_mapper, std::atomic<uint64_t> *edge_count,
                      Config::Items items);
 
diff --git a/src/storage/v3/indices.cpp b/src/storage/v3/indices.cpp
index 340484228..8a788bd31 100644
--- a/src/storage/v3/indices.cpp
+++ b/src/storage/v3/indices.cpp
@@ -270,7 +270,7 @@ void LabelIndex::UpdateOnAddLabel(LabelId label, Vertex *vertex, const Transacti
   acc.insert(Entry{vertex, tx.start_timestamp});
 }
 
-bool LabelIndex::CreateIndex(LabelId label, utils::SkipList<Vertex>::Accessor vertices) {
+bool LabelIndex::CreateIndex(LabelId label, VerticesSkipList::Accessor vertices) {
   utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
   auto [it, emplaced] = index_.emplace(std::piecewise_construct, std::forward_as_tuple(label), std::forward_as_tuple());
   if (!emplaced) {
@@ -279,7 +279,8 @@ bool LabelIndex::CreateIndex(LabelId label, utils::SkipList<Vertex>::Accessor ve
   }
   try {
     auto acc = it->second.access();
-    for (Vertex &vertex : vertices) {
+    for (auto &lgo_vertex : vertices) {
+      auto &vertex = lgo_vertex.vertex;
       if (vertex.deleted || !utils::Contains(vertex.labels, label)) {
         continue;
       }
@@ -416,7 +417,7 @@ void LabelPropertyIndex::UpdateOnSetProperty(PropertyId property, const Property
   }
 }
 
-bool LabelPropertyIndex::CreateIndex(LabelId label, PropertyId property, utils::SkipList<Vertex>::Accessor vertices) {
+bool LabelPropertyIndex::CreateIndex(LabelId label, PropertyId property, VerticesSkipList::Accessor vertices) {
   utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
   auto [it, emplaced] =
       index_.emplace(std::piecewise_construct, std::forward_as_tuple(label, property), std::forward_as_tuple());
@@ -426,7 +427,8 @@ bool LabelPropertyIndex::CreateIndex(LabelId label, PropertyId property, utils::
   }
   try {
     auto acc = it->second.access();
-    for (Vertex &vertex : vertices) {
+    for (auto &lgo_vertex : vertices) {
+      auto &vertex = lgo_vertex.vertex;
       if (vertex.deleted || !utils::Contains(vertex.labels, label)) {
         continue;
       }
diff --git a/src/storage/v3/indices.hpp b/src/storage/v3/indices.hpp
index fe9e83b88..d943bea90 100644
--- a/src/storage/v3/indices.hpp
+++ b/src/storage/v3/indices.hpp
@@ -19,6 +19,7 @@
 #include "storage/v3/property_value.hpp"
 #include "storage/v3/transaction.hpp"
 #include "storage/v3/vertex_accessor.hpp"
+#include "storage/v3/vertices_skip_list.hpp"
 #include "utils/bound.hpp"
 #include "utils/logging.hpp"
 #include "utils/skip_list.hpp"
@@ -58,7 +59,7 @@ class LabelIndex {
   void UpdateOnAddLabel(LabelId label, Vertex *vertex, const Transaction &tx);
 
   /// @throw std::bad_alloc
-  bool CreateIndex(LabelId label, utils::SkipList<Vertex>::Accessor vertices);
+  bool CreateIndex(LabelId label, VerticesSkipList::Accessor vertices);
 
   /// Returns false if there was no index to drop
   bool DropIndex(LabelId label) { return index_.erase(label) > 0; }
@@ -156,7 +157,7 @@ class LabelPropertyIndex {
   void UpdateOnSetProperty(PropertyId property, const PropertyValue &value, Vertex *vertex, const Transaction &tx);
 
   /// @throw std::bad_alloc
-  bool CreateIndex(LabelId label, PropertyId property, utils::SkipList<Vertex>::Accessor vertices);
+  bool CreateIndex(LabelId label, PropertyId property, VerticesSkipList::Accessor vertices);
 
   bool DropIndex(LabelId label, PropertyId property) { return index_.erase({label, property}) > 0; }
 
diff --git a/src/storage/v3/key_store.cpp b/src/storage/v3/key_store.cpp
new file mode 100644
index 000000000..c5e1f59a1
--- /dev/null
+++ b/src/storage/v3/key_store.cpp
@@ -0,0 +1,40 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include <algorithm>
+#include <iterator>
+#include <ranges>
+
+#include "storage/v3/key_store.hpp"
+#include "storage/v3/property_value.hpp"
+
+namespace memgraph::storage::v3 {
+
+KeyStore::KeyStore(const std::vector<PropertyValue> &key_values) {
+  for (auto i = 0; i < key_values.size(); ++i) {
+    MG_ASSERT(!key_values[i].IsNull());
+    store_.SetProperty(PropertyId::FromInt(i), key_values[i]);
+  }
+}
+
+PropertyValue KeyStore::GetKey(const size_t index) const { return store_.GetProperty(PropertyId::FromUint(index)); }
+
+std::vector<PropertyValue> KeyStore::Keys() const {
+  auto keys_map = store_.Properties();
+  std::vector<PropertyValue> keys;
+  keys.reserve(keys_map.size());
+  std::ranges::transform(
+      keys_map, std::back_inserter(keys),
+      [](std::pair<const PropertyId, PropertyValue> &id_and_value) { return std::move(id_and_value.second); });
+  return keys;
+}
+
+}  // namespace memgraph::storage::v3
diff --git a/src/storage/v3/key_store.hpp b/src/storage/v3/key_store.hpp
new file mode 100644
index 000000000..ec4905a4a
--- /dev/null
+++ b/src/storage/v3/key_store.hpp
@@ -0,0 +1,60 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include <algorithm>
+#include <compare>
+#include <functional>
+
+#include "storage/v3/property_store.hpp"
+#include "storage/v3/property_value.hpp"
+
+namespace memgraph::storage::v3 {
+
+class KeyStore {
+ public:
+  explicit KeyStore(const std::vector<PropertyValue> &key_values);
+
+  KeyStore(const KeyStore &) = delete;
+  KeyStore(KeyStore &&other) noexcept = default;
+  KeyStore &operator=(const KeyStore &) = delete;
+  KeyStore &operator=(KeyStore &&other) noexcept = default;
+
+  ~KeyStore() = default;
+
+  PropertyValue GetKey(size_t index) const;
+
+  std::vector<PropertyValue> Keys() const;
+
+  friend bool operator<(const KeyStore &lhs, const KeyStore &rhs) {
+    // TODO(antaljanosbenjamin): also compare the schema
+    return std::ranges::lexicographical_compare(lhs.Keys(), rhs.Keys(), std::less<PropertyValue>{});
+  }
+
+  friend bool operator==(const KeyStore &lhs, const KeyStore &rhs) {
+    return std::ranges::equal(lhs.Keys(), rhs.Keys());
+  }
+
+  friend bool operator<(const KeyStore &lhs, const std::vector<PropertyValue> &rhs) {
+    // TODO(antaljanosbenjamin): also compare the schema
+    return std::ranges::lexicographical_compare(lhs.Keys(), rhs, std::less<PropertyValue>{});
+  }
+
+  friend bool operator==(const KeyStore &lhs, const std::vector<PropertyValue> &rhs) {
+    return std::ranges::equal(lhs.Keys(), rhs);
+  }
+
+ private:
+  PropertyStore store_;
+};
+
+}  // namespace memgraph::storage::v3
diff --git a/src/storage/v3/lexicographically_ordered_vertex.hpp b/src/storage/v3/lexicographically_ordered_vertex.hpp
new file mode 100644
index 000000000..e039a2687
--- /dev/null
+++ b/src/storage/v3/lexicographically_ordered_vertex.hpp
@@ -0,0 +1,42 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include <concepts>
+#include <type_traits>
+
+#include "storage/v3/vertex.hpp"
+#include "utils/concepts.hpp"
+
+namespace memgraph::storage::v3 {
+
+struct LexicographicallyOrderedVertex {
+  Vertex vertex;
+
+  friend bool operator==(const LexicographicallyOrderedVertex &lhs, const LexicographicallyOrderedVertex &rhs) {
+    return lhs.vertex.keys == rhs.vertex.keys;
+  }
+
+  friend bool operator<(const LexicographicallyOrderedVertex &lhs, const LexicographicallyOrderedVertex &rhs) {
+    return lhs.vertex.keys < rhs.vertex.keys;
+  }
+
+  // TODO(antaljanosbenjamin): maybe it worth to overload this for std::array to avoid heap construction of the vector
+  friend bool operator==(const LexicographicallyOrderedVertex &lhs, const std::vector<PropertyValue> &rhs) {
+    return lhs.vertex.keys == rhs;
+  }
+
+  friend bool operator<(const LexicographicallyOrderedVertex &lhs, const std::vector<PropertyValue> &rhs) {
+    return lhs.vertex.keys < rhs;
+  }
+};
+}  // namespace memgraph::storage::v3
diff --git a/src/storage/v3/replication/replication_server.cpp b/src/storage/v3/replication/replication_server.cpp
index 0568598e1..55ef521fa 100644
--- a/src/storage/v3/replication/replication_server.cpp
+++ b/src/storage/v3/replication/replication_server.cpp
@@ -167,9 +167,10 @@ void Storage::ReplicationServer::SnapshotHandler(slk::Reader *req_reader, slk::B
       LabelPropertyIndex(&storage_->indices_, &storage_->constraints_, storage_->config_.items);
   try {
     spdlog::debug("Loading snapshot");
-    auto recovered_snapshot = durability::LoadSnapshot(*maybe_snapshot_path, &storage_->vertices_, &storage_->edges_,
-                                                       &storage_->epoch_history_, &storage_->name_id_mapper_,
-                                                       &storage_->edge_count_, storage_->config_.items);
+    auto recovered_snapshot = durability::RecoveredSnapshot{};
+
+    durability::LoadSnapshot(*maybe_snapshot_path, &storage_->vertices_, &storage_->edges_, &storage_->epoch_history_,
+                             &storage_->name_id_mapper_, &storage_->edge_count_, storage_->config_.items);
     spdlog::debug("Snapshot loaded successfully");
     // If this step is present it should always be the first step of
     // the recovery so we use the UUID we read from snasphost
diff --git a/src/storage/v3/storage.cpp b/src/storage/v3/storage.cpp
index 5ab334893..becad86a8 100644
--- a/src/storage/v3/storage.cpp
+++ b/src/storage/v3/storage.cpp
@@ -29,6 +29,7 @@
 #include "storage/v3/edge_accessor.hpp"
 #include "storage/v3/indices.hpp"
 #include "storage/v3/mvcc.hpp"
+#include "storage/v3/property_value.hpp"
 #include "storage/v3/replication/config.hpp"
 #include "storage/v3/replication/replication_client.hpp"
 #include "storage/v3/replication/replication_server.hpp"
@@ -52,11 +53,11 @@ namespace {
 inline constexpr uint16_t kEpochHistoryRetention = 1000;
 }  // namespace
 
-auto AdvanceToVisibleVertex(utils::SkipList<Vertex>::Iterator it, utils::SkipList<Vertex>::Iterator end,
+auto AdvanceToVisibleVertex(VerticesSkipList::Iterator it, VerticesSkipList::Iterator end,
                             std::optional<VertexAccessor> *vertex, Transaction *tx, View view, Indices *indices,
                             Constraints *constraints, Config::Items config) {
   while (it != end) {
-    *vertex = VertexAccessor::Create(&*it, tx, indices, constraints, config, view);
+    *vertex = VertexAccessor::Create(&it->vertex, tx, indices, constraints, config, view);
     if (!*vertex) {
       ++it;
       continue;
@@ -66,7 +67,7 @@ auto AdvanceToVisibleVertex(utils::SkipList<Vertex>::Iterator it, utils::SkipLis
   return it;
 }
 
-AllVerticesIterable::Iterator::Iterator(AllVerticesIterable *self, utils::SkipList<Vertex>::Iterator it)
+AllVerticesIterable::Iterator::Iterator(AllVerticesIterable *self, VerticesSkipList::Iterator it)
     : self_(self),
       it_(AdvanceToVisibleVertex(it, self->vertices_accessor_.end(), &self->vertex_, self->transaction_, self->view_,
                                  self->indices_, self_->constraints_, self->config_)) {}
@@ -342,9 +343,11 @@ Storage::Storage(Config config)
               config_.durability.storage_directory);
   }
   if (config_.durability.recover_on_startup) {
-    auto info = durability::RecoverData(snapshot_directory_, wal_directory_, &uuid_, &epoch_id_, &epoch_history_,
-                                        &vertices_, &edges_, &edge_count_, &name_id_mapper_, &indices_, &constraints_,
-                                        config_.items, &wal_seq_num_);
+    auto info = std::optional<durability::RecoveryInfo>{};
+
+    durability::RecoverData(snapshot_directory_, wal_directory_, &uuid_, &epoch_id_, &epoch_history_, &vertices_,
+                            &edges_, &edge_count_, &name_id_mapper_, &indices_, &constraints_, config_.items,
+                            &wal_seq_num_);
     if (info) {
       vertex_id_ = info->next_vertex_id;
       edge_id_ = info->next_edge_id;
@@ -467,11 +470,12 @@ VertexAccessor Storage::Accessor::CreateVertex() {
   auto gid = storage_->vertex_id_.fetch_add(1, std::memory_order_acq_rel);
   auto acc = storage_->vertices_.access();
   auto *delta = CreateDeleteObjectDelta(&transaction_);
-  auto [it, inserted] = acc.insert(Vertex{Gid::FromUint(gid), delta});
+  // TODO(antaljanosbenjamin): handle keys and schema
+  auto [it, inserted] = acc.insert({Vertex{Gid::FromUint(gid), delta}});
   MG_ASSERT(inserted, "The vertex must be inserted here!");
   MG_ASSERT(it != acc.end(), "Invalid Vertex accessor!");
-  delta->prev.Set(&*it);
-  return {&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_};
+  delta->prev.Set(&it->vertex);
+  return {&it->vertex, &transaction_, &storage_->indices_, &storage_->constraints_, config_};
 }
 
 VertexAccessor Storage::Accessor::CreateVertex(Gid gid) {
@@ -486,18 +490,20 @@ VertexAccessor Storage::Accessor::CreateVertex(Gid gid) {
                              std::memory_order_release);
   auto acc = storage_->vertices_.access();
   auto *delta = CreateDeleteObjectDelta(&transaction_);
-  auto [it, inserted] = acc.insert(Vertex{gid, delta});
+  auto [it, inserted] = acc.insert({Vertex{gid, delta}});
   MG_ASSERT(inserted, "The vertex must be inserted here!");
   MG_ASSERT(it != acc.end(), "Invalid Vertex accessor!");
-  delta->prev.Set(&*it);
-  return {&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_};
+  delta->prev.Set(&it->vertex);
+  return {&it->vertex, &transaction_, &storage_->indices_, &storage_->constraints_, config_};
 }
 
 std::optional<VertexAccessor> Storage::Accessor::FindVertex(Gid gid, View view) {
   auto acc = storage_->vertices_.access();
-  auto it = acc.find(gid);
+  auto it = acc.find(std::vector{PropertyValue{gid.AsInt()}});
   if (it == acc.end()) return std::nullopt;
-  return VertexAccessor::Create(&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_, view);
+  return VertexAccessor::Create(&it->vertex, &transaction_, &storage_->indices_, &storage_->constraints_, config_,
+                                view);
+  return {};
 }
 
 Result<std::optional<VertexAccessor>> Storage::Accessor::DeleteVertex(VertexAccessor *vertex) {
@@ -609,10 +615,10 @@ Result<EdgeAccessor> Storage::Accessor::CreateEdge(VertexAccessor *from, VertexA
   // Obtain the locks by `gid` order to avoid lock cycles.
   std::unique_lock<utils::SpinLock> guard_from(from_vertex->lock, std::defer_lock);
   std::unique_lock<utils::SpinLock> guard_to(to_vertex->lock, std::defer_lock);
-  if (from_vertex->gid < to_vertex->gid) {
+  if (from_vertex < to_vertex) {
     guard_from.lock();
     guard_to.lock();
-  } else if (from_vertex->gid > to_vertex->gid) {
+  } else if (from_vertex > to_vertex) {
     guard_to.lock();
     guard_from.lock();
   } else {
@@ -669,10 +675,10 @@ Result<EdgeAccessor> Storage::Accessor::CreateEdge(VertexAccessor *from, VertexA
   // Obtain the locks by `gid` order to avoid lock cycles.
   std::unique_lock<utils::SpinLock> guard_from(from_vertex->lock, std::defer_lock);
   std::unique_lock<utils::SpinLock> guard_to(to_vertex->lock, std::defer_lock);
-  if (from_vertex->gid < to_vertex->gid) {
+  if (&from_vertex < &to_vertex) {
     guard_from.lock();
     guard_to.lock();
-  } else if (from_vertex->gid > to_vertex->gid) {
+  } else if (&from_vertex > &to_vertex) {
     guard_to.lock();
     guard_from.lock();
   } else {
@@ -744,10 +750,10 @@ Result<std::optional<EdgeAccessor>> Storage::Accessor::DeleteEdge(EdgeAccessor *
   // Obtain the locks by `gid` order to avoid lock cycles.
   std::unique_lock<utils::SpinLock> guard_from(from_vertex->lock, std::defer_lock);
   std::unique_lock<utils::SpinLock> guard_to(to_vertex->lock, std::defer_lock);
-  if (from_vertex->gid < to_vertex->gid) {
+  if (&from_vertex < &to_vertex) {
     guard_from.lock();
     guard_to.lock();
-  } else if (from_vertex->gid > to_vertex->gid) {
+  } else if (&from_vertex > &to_vertex) {
     guard_to.lock();
     guard_from.lock();
   } else {
@@ -1021,7 +1027,7 @@ void Storage::Accessor::Abort() {
             }
             case Delta::Action::DELETE_OBJECT: {
               vertex->deleted = true;
-              my_deleted_vertices.push_back(vertex->gid);
+              my_deleted_vertices.push_back(vertex->Gid());
               break;
             }
             case Delta::Action::RECREATE_OBJECT: {
@@ -1412,7 +1418,7 @@ void Storage::CollectGarbage() {
             }
             vertex->delta = nullptr;
             if (vertex->deleted) {
-              current_deleted_vertices.push_back(vertex->gid);
+              current_deleted_vertices.push_back(vertex->Gid());
             }
             break;
           }
@@ -1530,13 +1536,17 @@ void Storage::CollectGarbage() {
     if constexpr (force) {
       // if force is set to true, then we have unique_lock and no transactions are active
       // so we can clean all of the deleted vertices
+      std::vector<PropertyValue> key(1);
       while (!garbage_vertices_.empty()) {
-        MG_ASSERT(vertex_acc.remove(garbage_vertices_.front().second), "Invalid database state!");
+        key.front() = PropertyValue{garbage_vertices_.front().second.AsInt()};
+        MG_ASSERT(vertex_acc.remove(key), "Invalid database state!");
         garbage_vertices_.pop_front();
       }
     } else {
+      std::vector<PropertyValue> key(1);
       while (!garbage_vertices_.empty() && garbage_vertices_.front().first < oldest_active_start_timestamp) {
-        MG_ASSERT(vertex_acc.remove(garbage_vertices_.front().second), "Invalid database state!");
+        key.front() = PropertyValue{garbage_vertices_.front().second.AsInt()};
+        MG_ASSERT(vertex_acc.remove(key), "Invalid database state!");
         garbage_vertices_.pop_front();
       }
     }
diff --git a/src/storage/v3/storage.hpp b/src/storage/v3/storage.hpp
index 40fe708b1..0c23667d2 100644
--- a/src/storage/v3/storage.hpp
+++ b/src/storage/v3/storage.hpp
@@ -27,12 +27,14 @@
 #include "storage/v3/edge_accessor.hpp"
 #include "storage/v3/indices.hpp"
 #include "storage/v3/isolation_level.hpp"
+#include "storage/v3/lexicographically_ordered_vertex.hpp"
 #include "storage/v3/mvcc.hpp"
 #include "storage/v3/name_id_mapper.hpp"
 #include "storage/v3/result.hpp"
 #include "storage/v3/transaction.hpp"
 #include "storage/v3/vertex.hpp"
 #include "storage/v3/vertex_accessor.hpp"
+#include "storage/v3/vertices_skip_list.hpp"
 #include "utils/file_locker.hpp"
 #include "utils/on_scope_exit.hpp"
 #include "utils/rw_lock.hpp"
@@ -60,7 +62,7 @@ namespace memgraph::storage::v3 {
 /// An instance of this will be usually be wrapped inside VerticesIterable for
 /// generic, public use.
 class AllVerticesIterable final {
-  utils::SkipList<Vertex>::Accessor vertices_accessor_;
+  VerticesSkipList::Accessor vertices_accessor_;
   Transaction *transaction_;
   View view_;
   Indices *indices_;
@@ -71,10 +73,10 @@ class AllVerticesIterable final {
  public:
   class Iterator final {
     AllVerticesIterable *self_;
-    utils::SkipList<Vertex>::Iterator it_;
+    VerticesSkipList::Iterator it_;
 
    public:
-    Iterator(AllVerticesIterable *self, utils::SkipList<Vertex>::Iterator it);
+    Iterator(AllVerticesIterable *self, VerticesSkipList::Iterator it);
 
     VertexAccessor operator*() const;
 
@@ -85,7 +87,7 @@ class AllVerticesIterable final {
     bool operator!=(const Iterator &other) const { return !(*this == other); }
   };
 
-  AllVerticesIterable(utils::SkipList<Vertex>::Accessor vertices_accessor, Transaction *transaction, View view,
+  AllVerticesIterable(VerticesSkipList::Accessor vertices_accessor, Transaction *transaction, View view,
                       Indices *indices, Constraints *constraints, Config::Items config)
       : vertices_accessor_(std::move(vertices_accessor)),
         transaction_(transaction),
@@ -482,7 +484,7 @@ class Storage final {
   mutable utils::RWLock main_lock_{utils::RWLock::Priority::WRITE};
 
   // Main object storage
-  utils::SkipList<Vertex> vertices_;
+  VerticesSkipList vertices_;
   utils::SkipList<Edge> edges_;
   std::atomic<uint64_t> vertex_id_{0};
   std::atomic<uint64_t> edge_id_{0};
diff --git a/src/storage/v3/vertex.hpp b/src/storage/v3/vertex.hpp
index 3bf1bbb49..92377096c 100644
--- a/src/storage/v3/vertex.hpp
+++ b/src/storage/v3/vertex.hpp
@@ -13,24 +13,28 @@
 
 #include <limits>
 #include <tuple>
+#include <type_traits>
 #include <vector>
 
 #include "storage/v3/delta.hpp"
 #include "storage/v3/edge_ref.hpp"
 #include "storage/v3/id_types.hpp"
+#include "storage/v3/key_store.hpp"
 #include "storage/v3/property_store.hpp"
+#include "storage/v3/property_value.hpp"
 #include "utils/spin_lock.hpp"
 
 namespace memgraph::storage::v3 {
 
 struct Vertex {
-  Vertex(Gid gid, Delta *delta) : gid(gid), deleted(false), delta(delta) {
+  Vertex(Gid gid, Delta *delta) : keys{{PropertyValue{gid.AsInt()}}}, deleted(false), delta(delta) {
     MG_ASSERT(delta == nullptr || delta->action == Delta::Action::DELETE_OBJECT,
               "Vertex must be created with an initial DELETE_OBJECT delta!");
   }
 
-  Gid gid;
+  Gid Gid() const { return Gid::FromInt(keys.GetKey(0).ValueInt()); }
 
+  KeyStore keys;
   std::vector<LabelId> labels;
   PropertyStore properties;
 
@@ -47,9 +51,4 @@ struct Vertex {
 
 static_assert(alignof(Vertex) >= 8, "The Vertex should be aligned to at least 8!");
 
-inline bool operator==(const Vertex &first, const Vertex &second) { return first.gid == second.gid; }
-inline bool operator<(const Vertex &first, const Vertex &second) { return first.gid < second.gid; }
-inline bool operator==(const Vertex &first, const Gid &second) { return first.gid == second; }
-inline bool operator<(const Vertex &first, const Gid &second) { return first.gid < second; }
-
 }  // namespace memgraph::storage::v3
diff --git a/src/storage/v3/vertex_accessor.hpp b/src/storage/v3/vertex_accessor.hpp
index 88a2828c8..3c8af3326 100644
--- a/src/storage/v3/vertex_accessor.hpp
+++ b/src/storage/v3/vertex_accessor.hpp
@@ -94,7 +94,10 @@ class VertexAccessor final {
 
   Result<size_t> OutDegree(View view) const;
 
-  Gid Gid() const noexcept { return vertex_->gid; }
+  Gid Gid() const noexcept {
+    // TODO(antaljanosbenjamin): remove this whole function.
+    return vertex_->Gid();
+  }
 
   bool operator==(const VertexAccessor &other) const noexcept {
     return vertex_ == other.vertex_ && transaction_ == other.transaction_;
diff --git a/src/storage/v3/vertices_skip_list.hpp b/src/storage/v3/vertices_skip_list.hpp
new file mode 100644
index 000000000..283dd5aeb
--- /dev/null
+++ b/src/storage/v3/vertices_skip_list.hpp
@@ -0,0 +1,19 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include "storage/v3/lexicographically_ordered_vertex.hpp"
+#include "utils/skip_list.hpp"
+
+namespace memgraph::storage::v3 {
+using VerticesSkipList = utils::SkipList<LexicographicallyOrderedVertex>;
+}  // namespace memgraph::storage::v3
diff --git a/src/utils/concepts.hpp b/src/utils/concepts.hpp
index 3a5442d4a..7c1f3a9c8 100644
--- a/src/utils/concepts.hpp
+++ b/src/utils/concepts.hpp
@@ -11,6 +11,7 @@
 
 #pragma once
 #include <concepts>
+#include <iterator>
 
 namespace memgraph::utils {
 template <typename T, typename... Args>
@@ -18,4 +19,19 @@ concept SameAsAnyOf = (std::same_as<T, Args> || ...);
 
 template <typename T>
 concept Enum = std::is_enum_v<T>;
+
+// WithRef, CanReference and Dereferenceable is based on the similarly named concepts in GCC 11.2.0
+// bits/iterator_concepts.h
+template <typename T>
+using WithRef = T &;
+
+template <typename T>
+concept CanReference = requires {
+  typename WithRef<T>;
+};
+
+template <typename T>
+concept Dereferenceable = requires(T t) {
+  { *t } -> CanReference;
+};
 }  // namespace memgraph::utils
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 319627715..2677f4ee6 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -363,5 +363,15 @@ add_unit_test(websocket.cpp)
 target_link_libraries(${test_prefix}websocket mg-communication Boost::headers)
 
 # Test storage-v3
+# Test utilities
+add_library(storage_v3_test_utils storage_v3_test_utils.cpp)
+target_link_libraries(storage_v3_test_utils mg-storage-v3)
+
 add_unit_test(storage_v3.cpp)
-target_link_libraries(${test_prefix}storage_v3 mg-storage-v3)
+target_link_libraries(${test_prefix}storage_v3 mg-storage-v3 storage_v3_test_utils)
+
+add_unit_test(storage_v3_property_store.cpp)
+target_link_libraries(${test_prefix}storage_v3_property_store mg-storage-v3 fmt)
+
+add_unit_test(storage_v3_key_store.cpp)
+target_link_libraries(${test_prefix}storage_v3_key_store mg-storage-v3 rapidcheck rapidcheck_gtest)
diff --git a/tests/unit/storage_v3.cpp b/tests/unit/storage_v3.cpp
index e30114faf..f3b3438e1 100644
--- a/tests/unit/storage_v3.cpp
+++ b/tests/unit/storage_v3.cpp
@@ -9,13 +9,2555 @@
 // by the Apache License, Version 2.0, included in the file
 // licenses/APL.txt.
 
+#include <limits>
+
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
+#include "storage/v3/property_value.hpp"
 #include "storage/v3/storage.hpp"
+#include "storage/v3/vertex_accessor.hpp"
+#include "storage_v3_test_utils.hpp"
+
+using testing::UnorderedElementsAre;
+
+namespace memgraph::storage::v3::tests {
+
+class StorageV3 : public ::testing::Test {
+ protected:
+  Storage store;
+};
 
 // NOLINTNEXTLINE(hicpp-special-member-functions)
-TEST(StorageV3, DummyTest) {
-  memgraph::storage::v3::Storage store;
-  EXPECT_EQ(store.GetInfo().vertex_count, 0);
+TEST_F(StorageV3, Commit) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    ASSERT_TRUE(acc.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 1U);
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    ASSERT_TRUE(acc.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 1U);
+    ASSERT_TRUE(acc.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 1U);
+    acc.Abort();
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::NEW);
+    ASSERT_TRUE(vertex);
+
+    auto res = acc.DeleteVertex(&*vertex);
+    ASSERT_FALSE(res.HasError());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+
+    acc.AdvanceCommand();
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    ASSERT_FALSE(acc.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    ASSERT_FALSE(acc.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+    acc.Abort();
+  }
 }
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, Abort) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    ASSERT_TRUE(acc.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 1U);
+    acc.Abort();
+  }
+  {
+    auto acc = store.Access();
+    ASSERT_FALSE(acc.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    ASSERT_FALSE(acc.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, AdvanceCommandCommit) {
+  Gid gid1 = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  Gid gid2 = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  {
+    auto acc = store.Access();
+
+    auto vertex1 = acc.CreateVertex();
+    gid1 = vertex1.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid1, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    ASSERT_TRUE(acc.FindVertex(gid1, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 1U);
+
+    acc.AdvanceCommand();
+
+    auto vertex2 = acc.CreateVertex();
+    gid2 = vertex2.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid2, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 1U);
+    ASSERT_TRUE(acc.FindVertex(gid2, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 2U);
+
+    ASSERT_TRUE(acc.FindVertex(gid1, View::OLD).has_value());
+    ASSERT_TRUE(acc.FindVertex(gid1, View::NEW).has_value());
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    ASSERT_TRUE(acc.FindVertex(gid1, View::OLD).has_value());
+    ASSERT_TRUE(acc.FindVertex(gid1, View::NEW).has_value());
+    ASSERT_TRUE(acc.FindVertex(gid2, View::OLD).has_value());
+    ASSERT_TRUE(acc.FindVertex(gid2, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 2U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 2U);
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, AdvanceCommandAbort) {
+  Gid gid1 = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  Gid gid2 = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  {
+    auto acc = store.Access();
+
+    auto vertex1 = acc.CreateVertex();
+    gid1 = vertex1.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid1, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    ASSERT_TRUE(acc.FindVertex(gid1, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 1U);
+
+    acc.AdvanceCommand();
+
+    auto vertex2 = acc.CreateVertex();
+    gid2 = vertex2.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid2, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 1U);
+    ASSERT_TRUE(acc.FindVertex(gid2, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 2U);
+
+    ASSERT_TRUE(acc.FindVertex(gid1, View::OLD).has_value());
+    ASSERT_TRUE(acc.FindVertex(gid1, View::NEW).has_value());
+
+    acc.Abort();
+  }
+  {
+    auto acc = store.Access();
+    ASSERT_FALSE(acc.FindVertex(gid1, View::OLD).has_value());
+    ASSERT_FALSE(acc.FindVertex(gid1, View::NEW).has_value());
+    ASSERT_FALSE(acc.FindVertex(gid2, View::OLD).has_value());
+    ASSERT_FALSE(acc.FindVertex(gid2, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, SnapshotIsolation) {
+  auto acc1 = store.Access();
+  auto acc2 = store.Access();
+
+  auto vertex = acc1.CreateVertex();
+  auto gid = vertex.Gid();
+
+  ASSERT_FALSE(acc2.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::OLD), 0U);
+  EXPECT_EQ(CountVertices(acc2, View::OLD), 0U);
+  ASSERT_FALSE(acc2.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::NEW), 1U);
+  EXPECT_EQ(CountVertices(acc2, View::NEW), 0U);
+
+  ASSERT_FALSE(acc1.Commit().HasError());
+
+  ASSERT_FALSE(acc2.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc2, View::OLD), 0U);
+  ASSERT_FALSE(acc2.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc2, View::NEW), 0U);
+
+  acc2.Abort();
+
+  auto acc3 = store.Access();
+  ASSERT_TRUE(acc3.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::OLD), 1U);
+  ASSERT_TRUE(acc3.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::NEW), 1U);
+  acc3.Abort();
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, AccessorMove) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+
+    ASSERT_FALSE(acc.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    ASSERT_TRUE(acc.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 1U);
+
+    Storage::Accessor moved(std::move(acc));
+
+    ASSERT_FALSE(moved.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(moved, View::OLD), 0U);
+    ASSERT_TRUE(moved.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(moved, View::NEW), 1U);
+
+    ASSERT_FALSE(moved.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    ASSERT_TRUE(acc.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 1U);
+    ASSERT_TRUE(acc.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 1U);
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexDeleteCommit) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+
+  auto acc1 = store.Access();  // read transaction
+  auto acc2 = store.Access();  // write transaction
+
+  // Create the vertex in transaction 2
+  {
+    auto vertex = acc2.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc2.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc2, View::OLD), 0U);
+    ASSERT_TRUE(acc2.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc2, View::NEW), 1U);
+    ASSERT_FALSE(acc2.Commit().HasError());
+  }
+
+  auto acc3 = store.Access();  // read transaction
+  auto acc4 = store.Access();  // write transaction
+
+  // Check whether the vertex exists in transaction 1
+  ASSERT_FALSE(acc1.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::OLD), 0U);
+  ASSERT_FALSE(acc1.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::NEW), 0U);
+
+  // Check whether the vertex exists in transaction 3
+  ASSERT_TRUE(acc3.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::OLD), 1U);
+  ASSERT_TRUE(acc3.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::NEW), 1U);
+
+  // Delete the vertex in transaction 4
+  {
+    auto vertex = acc4.FindVertex(gid, View::NEW);
+    ASSERT_TRUE(vertex);
+    EXPECT_EQ(CountVertices(acc4, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc4, View::NEW), 1U);
+
+    auto res = acc4.DeleteVertex(&*vertex);
+    ASSERT_TRUE(res.HasValue());
+    EXPECT_EQ(CountVertices(acc4, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc4, View::NEW), 0U);
+
+    acc4.AdvanceCommand();
+    EXPECT_EQ(CountVertices(acc4, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc4, View::NEW), 0U);
+
+    ASSERT_FALSE(acc4.Commit().HasError());
+  }
+
+  auto acc5 = store.Access();  // read transaction
+
+  // Check whether the vertex exists in transaction 1
+  ASSERT_FALSE(acc1.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::OLD), 0U);
+  ASSERT_FALSE(acc1.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::NEW), 0U);
+
+  // Check whether the vertex exists in transaction 3
+  ASSERT_TRUE(acc3.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::OLD), 1U);
+  ASSERT_TRUE(acc3.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::NEW), 1U);
+
+  // Check whether the vertex exists in transaction 5
+  ASSERT_FALSE(acc5.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc5, View::OLD), 0U);
+  ASSERT_FALSE(acc5.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc5, View::NEW), 0U);
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexDeleteAbort) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+
+  auto acc1 = store.Access();  // read transaction
+  auto acc2 = store.Access();  // write transaction
+
+  // Create the vertex in transaction 2
+  {
+    auto vertex = acc2.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc2.FindVertex(gid, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc2, View::OLD), 0U);
+    ASSERT_TRUE(acc2.FindVertex(gid, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc2, View::NEW), 1U);
+    ASSERT_FALSE(acc2.Commit().HasError());
+  }
+
+  auto acc3 = store.Access();  // read transaction
+  auto acc4 = store.Access();  // write transaction (aborted)
+
+  // Check whether the vertex exists in transaction 1
+  ASSERT_FALSE(acc1.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::OLD), 0U);
+  ASSERT_FALSE(acc1.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::NEW), 0U);
+
+  // Check whether the vertex exists in transaction 3
+  ASSERT_TRUE(acc3.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::OLD), 1U);
+  ASSERT_TRUE(acc3.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::NEW), 1U);
+
+  // Delete the vertex in transaction 4, but abort the transaction
+  {
+    auto vertex = acc4.FindVertex(gid, View::NEW);
+    ASSERT_TRUE(vertex);
+    EXPECT_EQ(CountVertices(acc4, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc4, View::NEW), 1U);
+
+    auto res = acc4.DeleteVertex(&*vertex);
+    ASSERT_TRUE(res.HasValue());
+    EXPECT_EQ(CountVertices(acc4, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc4, View::NEW), 0U);
+
+    acc4.AdvanceCommand();
+    EXPECT_EQ(CountVertices(acc4, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc4, View::NEW), 0U);
+
+    acc4.Abort();
+  }
+
+  auto acc5 = store.Access();  // read transaction
+  auto acc6 = store.Access();  // write transaction
+
+  // Check whether the vertex exists in transaction 1
+  ASSERT_FALSE(acc1.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::OLD), 0U);
+  ASSERT_FALSE(acc1.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::NEW), 0U);
+
+  // Check whether the vertex exists in transaction 3
+  ASSERT_TRUE(acc3.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::OLD), 1U);
+  ASSERT_TRUE(acc3.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::NEW), 1U);
+
+  // Check whether the vertex exists in transaction 5
+  ASSERT_TRUE(acc5.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc5, View::OLD), 1U);
+  ASSERT_TRUE(acc5.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc5, View::NEW), 1U);
+
+  // Delete the vertex in transaction 6
+  {
+    auto vertex = acc6.FindVertex(gid, View::NEW);
+    ASSERT_TRUE(vertex);
+    EXPECT_EQ(CountVertices(acc6, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc6, View::NEW), 1U);
+
+    auto res = acc6.DeleteVertex(&*vertex);
+    ASSERT_TRUE(res.HasValue());
+    EXPECT_EQ(CountVertices(acc6, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc6, View::NEW), 0U);
+
+    acc6.AdvanceCommand();
+    EXPECT_EQ(CountVertices(acc6, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc6, View::NEW), 0U);
+
+    ASSERT_FALSE(acc6.Commit().HasError());
+  }
+
+  auto acc7 = store.Access();  // read transaction
+
+  // Check whether the vertex exists in transaction 1
+  ASSERT_FALSE(acc1.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::OLD), 0U);
+  ASSERT_FALSE(acc1.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc1, View::NEW), 0U);
+
+  // Check whether the vertex exists in transaction 3
+  ASSERT_TRUE(acc3.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::OLD), 1U);
+  ASSERT_TRUE(acc3.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc3, View::NEW), 1U);
+
+  // Check whether the vertex exists in transaction 5
+  ASSERT_TRUE(acc5.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc5, View::OLD), 1U);
+  ASSERT_TRUE(acc5.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc5, View::NEW), 1U);
+
+  // Check whether the vertex exists in transaction 7
+  ASSERT_FALSE(acc7.FindVertex(gid, View::OLD).has_value());
+  EXPECT_EQ(CountVertices(acc7, View::OLD), 0U);
+  ASSERT_FALSE(acc7.FindVertex(gid, View::NEW).has_value());
+  EXPECT_EQ(CountVertices(acc7, View::NEW), 0U);
+
+  // Commit all accessors
+  ASSERT_FALSE(acc1.Commit().HasError());
+  ASSERT_FALSE(acc3.Commit().HasError());
+  ASSERT_FALSE(acc5.Commit().HasError());
+  ASSERT_FALSE(acc7.Commit().HasError());
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexDeleteSerializationError) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+
+  // Create vertex
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  auto acc1 = store.Access();
+  auto acc2 = store.Access();
+
+  // Delete vertex in accessor 1
+  {
+    auto vertex = acc1.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+    EXPECT_EQ(CountVertices(acc1, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc1, View::NEW), 1U);
+
+    {
+      auto res = acc1.DeleteVertex(&*vertex);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_TRUE(res.GetValue());
+      EXPECT_EQ(CountVertices(acc1, View::OLD), 1U);
+      EXPECT_EQ(CountVertices(acc1, View::NEW), 0U);
+    }
+
+    {
+      auto res = acc1.DeleteVertex(&*vertex);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_FALSE(res.GetValue());
+      EXPECT_EQ(CountVertices(acc1, View::OLD), 1U);
+      EXPECT_EQ(CountVertices(acc1, View::NEW), 0U);
+    }
+
+    acc1.AdvanceCommand();
+    EXPECT_EQ(CountVertices(acc1, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc1, View::NEW), 0U);
+  }
+
+  // Delete vertex in accessor 2
+  {
+    auto vertex = acc2.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+    EXPECT_EQ(CountVertices(acc2, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc2, View::NEW), 1U);
+    auto res = acc2.DeleteVertex(&*vertex);
+    ASSERT_TRUE(res.HasError());
+    ASSERT_EQ(res.GetError(), Error::SERIALIZATION_ERROR);
+    EXPECT_EQ(CountVertices(acc2, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc2, View::NEW), 1U);
+    acc2.AdvanceCommand();
+    EXPECT_EQ(CountVertices(acc2, View::OLD), 1U);
+    EXPECT_EQ(CountVertices(acc2, View::NEW), 1U);
+  }
+
+  // Finalize both accessors
+  ASSERT_FALSE(acc1.Commit().HasError());
+  acc2.Abort();
+
+  // Check whether the vertex exists
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_FALSE(vertex);
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexDeleteSpecialCases) {
+  Gid gid1 = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  Gid gid2 = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+
+  // Create vertex and delete it in the same transaction, but abort the
+  // transaction
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid1 = vertex.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid1, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    ASSERT_TRUE(acc.FindVertex(gid1, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 1U);
+    auto res = acc.DeleteVertex(&vertex);
+    ASSERT_TRUE(res.HasValue());
+    ASSERT_TRUE(res.GetValue());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+    acc.AdvanceCommand();
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+    acc.Abort();
+  }
+
+  // Create vertex and delete it in the same transaction
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid2 = vertex.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid2, View::OLD).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    ASSERT_TRUE(acc.FindVertex(gid2, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::NEW), 1U);
+    auto res = acc.DeleteVertex(&vertex);
+    ASSERT_TRUE(res.HasValue());
+    ASSERT_TRUE(res.GetValue());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+    acc.AdvanceCommand();
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Check whether the vertices exist
+  {
+    auto acc = store.Access();
+    ASSERT_FALSE(acc.FindVertex(gid1, View::OLD).has_value());
+    ASSERT_FALSE(acc.FindVertex(gid1, View::NEW).has_value());
+    ASSERT_FALSE(acc.FindVertex(gid2, View::OLD).has_value());
+    ASSERT_FALSE(acc.FindVertex(gid2, View::NEW).has_value());
+    EXPECT_EQ(CountVertices(acc, View::OLD), 0U);
+    EXPECT_EQ(CountVertices(acc, View::NEW), 0U);
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexDeleteLabel) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+
+  // Create the vertex
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid, View::OLD).has_value());
+    ASSERT_TRUE(acc.FindVertex(gid, View::NEW).has_value());
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Add label, delete the vertex and check the label API (same command)
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::NEW);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    // Check whether label 5 exists
+    ASSERT_FALSE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    // Add label 5
+    ASSERT_TRUE(vertex->AddLabel(label).GetValue());
+
+    // Check whether label 5 exists
+    ASSERT_FALSE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_TRUE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    // Delete the vertex
+    ASSERT_TRUE(acc.DeleteVertex(&*vertex).GetValue());
+
+    // Check whether label 5 exists
+    ASSERT_FALSE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_EQ(vertex->HasLabel(label, View::NEW).GetError(), Error::DELETED_OBJECT);
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Labels(View::NEW).GetError(), Error::DELETED_OBJECT);
+
+    // Try to add the label
+    {
+      auto ret = vertex->AddLabel(label);
+      ASSERT_TRUE(ret.HasError());
+      ASSERT_EQ(ret.GetError(), Error::DELETED_OBJECT);
+    }
+
+    // Try to remove the label
+    {
+      auto ret = vertex->RemoveLabel(label);
+      ASSERT_TRUE(ret.HasError());
+      ASSERT_EQ(ret.GetError(), Error::DELETED_OBJECT);
+    }
+
+    acc.Abort();
+  }
+
+  // Add label, delete the vertex and check the label API (different command)
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::NEW);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    // Check whether label 5 exists
+    ASSERT_FALSE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    // Add label 5
+    ASSERT_TRUE(vertex->AddLabel(label).GetValue());
+
+    // Check whether label 5 exists
+    ASSERT_FALSE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_TRUE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    // Advance command
+    acc.AdvanceCommand();
+
+    // Check whether label 5 exists
+    ASSERT_TRUE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_TRUE(vertex->HasLabel(label, View::NEW).GetValue());
+    {
+      auto labels = vertex->Labels(View::OLD).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    // Delete the vertex
+    ASSERT_TRUE(acc.DeleteVertex(&*vertex).GetValue());
+
+    // Check whether label 5 exists
+    ASSERT_TRUE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_EQ(vertex->HasLabel(label, View::NEW).GetError(), Error::DELETED_OBJECT);
+    {
+      auto labels = vertex->Labels(View::OLD).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+    ASSERT_EQ(vertex->Labels(View::NEW).GetError(), Error::DELETED_OBJECT);
+
+    // Advance command
+    acc.AdvanceCommand();
+
+    // Check whether label 5 exists
+    ASSERT_EQ(vertex->HasLabel(label, View::OLD).GetError(), Error::DELETED_OBJECT);
+    ASSERT_EQ(vertex->HasLabel(label, View::NEW).GetError(), Error::DELETED_OBJECT);
+    ASSERT_EQ(vertex->Labels(View::OLD).GetError(), Error::DELETED_OBJECT);
+    ASSERT_EQ(vertex->Labels(View::NEW).GetError(), Error::DELETED_OBJECT);
+
+    // Try to add the label
+    {
+      auto ret = vertex->AddLabel(label);
+      ASSERT_TRUE(ret.HasError());
+      ASSERT_EQ(ret.GetError(), Error::DELETED_OBJECT);
+    }
+
+    // Try to remove the label
+    {
+      auto ret = vertex->RemoveLabel(label);
+      ASSERT_TRUE(ret.HasError());
+      ASSERT_EQ(ret.GetError(), Error::DELETED_OBJECT);
+    }
+
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexDeleteProperty) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+
+  // Create the vertex
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc.FindVertex(gid, View::OLD).has_value());
+    ASSERT_TRUE(acc.FindVertex(gid, View::NEW).has_value());
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Set property, delete the vertex and check the property API (same command)
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::NEW);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    // Check whether property 5 exists
+    ASSERT_TRUE(vertex->GetProperty(property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    // Set property 5 to "nandare"
+    ASSERT_TRUE(vertex->SetProperty(property, PropertyValue("nandare"))->IsNull());
+
+    // Check whether property 5 exists
+    ASSERT_TRUE(vertex->GetProperty(property, View::OLD)->IsNull());
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    // Delete the vertex
+    ASSERT_TRUE(acc.DeleteVertex(&*vertex).GetValue());
+
+    // Check whether label 5 exists
+    ASSERT_TRUE(vertex->GetProperty(property, View::OLD)->IsNull());
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW).GetError(), Error::DELETED_OBJECT);
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Properties(View::NEW).GetError(), Error::DELETED_OBJECT);
+
+    // Try to set the property
+    {
+      auto ret = vertex->SetProperty(property, PropertyValue("haihai"));
+      ASSERT_TRUE(ret.HasError());
+      ASSERT_EQ(ret.GetError(), Error::DELETED_OBJECT);
+    }
+
+    acc.Abort();
+  }
+
+  // Set property, delete the vertex and check the property API (different
+  // command)
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::NEW);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    // Check whether property 5 exists
+    ASSERT_TRUE(vertex->GetProperty(property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    // Set property 5 to "nandare"
+    ASSERT_TRUE(vertex->SetProperty(property, PropertyValue("nandare"))->IsNull());
+
+    // Check whether property 5 exists
+    ASSERT_TRUE(vertex->GetProperty(property, View::OLD)->IsNull());
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    // Advance command
+    acc.AdvanceCommand();
+
+    // Check whether property 5 exists
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    // Delete the vertex
+    ASSERT_TRUE(acc.DeleteVertex(&*vertex).GetValue());
+
+    // Check whether property 5 exists
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW).GetError(), Error::DELETED_OBJECT);
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+    ASSERT_EQ(vertex->Properties(View::NEW).GetError(), Error::DELETED_OBJECT);
+
+    // Advance command
+    acc.AdvanceCommand();
+
+    // Check whether property 5 exists
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD).GetError(), Error::DELETED_OBJECT);
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW).GetError(), Error::DELETED_OBJECT);
+    ASSERT_EQ(vertex->Properties(View::OLD).GetError(), Error::DELETED_OBJECT);
+    ASSERT_EQ(vertex->Properties(View::NEW).GetError(), Error::DELETED_OBJECT);
+
+    // Try to set the property
+    {
+      auto ret = vertex->SetProperty(property, PropertyValue("haihai"));
+      ASSERT_TRUE(ret.HasError());
+      ASSERT_EQ(ret.GetError(), Error::DELETED_OBJECT);
+    }
+
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexLabelCommit) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+
+    auto label = acc.NameToLabel("label5");
+
+    ASSERT_FALSE(vertex.HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex.Labels(View::NEW)->size(), 0);
+
+    {
+      auto res = vertex.AddLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_TRUE(res.GetValue());
+    }
+
+    ASSERT_TRUE(vertex.HasLabel(label, View::NEW).GetValue());
+    {
+      auto labels = vertex.Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    {
+      auto res = vertex.AddLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_FALSE(res.GetValue());
+    }
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::OLD).GetValue());
+    {
+      auto labels = vertex->Labels(View::OLD).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::NEW).GetValue());
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    auto other_label = acc.NameToLabel("other");
+
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::NEW).GetValue());
+
+    acc.Abort();
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    {
+      auto res = vertex->RemoveLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_TRUE(res.GetValue());
+    }
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::OLD).GetValue());
+    {
+      auto labels = vertex->Labels(View::OLD).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    {
+      auto res = vertex->RemoveLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_FALSE(res.GetValue());
+    }
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    ASSERT_FALSE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    auto other_label = acc.NameToLabel("other");
+
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::NEW).GetValue());
+
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexLabelAbort) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+
+  // Create the vertex.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Add label 5, but abort the transaction.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    {
+      auto res = vertex->AddLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_TRUE(res.GetValue());
+    }
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::NEW).GetValue());
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    {
+      auto res = vertex->AddLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_FALSE(res.GetValue());
+    }
+
+    acc.Abort();
+  }
+
+  // Check that label 5 doesn't exist.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    ASSERT_FALSE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    auto other_label = acc.NameToLabel("other");
+
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::NEW).GetValue());
+
+    acc.Abort();
+  }
+
+  // Add label 5.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    {
+      auto res = vertex->AddLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_TRUE(res.GetValue());
+    }
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::NEW).GetValue());
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    {
+      auto res = vertex->AddLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_FALSE(res.GetValue());
+    }
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Check that label 5 exists.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::OLD).GetValue());
+    {
+      auto labels = vertex->Labels(View::OLD).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::NEW).GetValue());
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    auto other_label = acc.NameToLabel("other");
+
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::NEW).GetValue());
+
+    acc.Abort();
+  }
+
+  // Remove label 5, but abort the transaction.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    {
+      auto res = vertex->RemoveLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_TRUE(res.GetValue());
+    }
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::OLD).GetValue());
+    {
+      auto labels = vertex->Labels(View::OLD).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    {
+      auto res = vertex->RemoveLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_FALSE(res.GetValue());
+    }
+
+    acc.Abort();
+  }
+
+  // Check that label 5 exists.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::OLD).GetValue());
+    {
+      auto labels = vertex->Labels(View::OLD).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::NEW).GetValue());
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    auto other_label = acc.NameToLabel("other");
+
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::NEW).GetValue());
+
+    acc.Abort();
+  }
+
+  // Remove label 5.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    {
+      auto res = vertex->RemoveLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_TRUE(res.GetValue());
+    }
+
+    ASSERT_TRUE(vertex->HasLabel(label, View::OLD).GetValue());
+    {
+      auto labels = vertex->Labels(View::OLD).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label);
+    }
+
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    {
+      auto res = vertex->RemoveLabel(label);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_FALSE(res.GetValue());
+    }
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Check that label 5 doesn't exist.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label = acc.NameToLabel("label5");
+
+    ASSERT_FALSE(vertex->HasLabel(label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    auto other_label = acc.NameToLabel("other");
+
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(other_label, View::NEW).GetValue());
+
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexLabelSerializationError) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  auto acc1 = store.Access();
+  auto acc2 = store.Access();
+
+  // Add label 1 in accessor 1.
+  {
+    auto vertex = acc1.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label1 = acc1.NameToLabel("label1");
+    auto label2 = acc1.NameToLabel("label2");
+
+    ASSERT_FALSE(vertex->HasLabel(label1, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label1, View::NEW).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label2, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label2, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    {
+      auto res = vertex->AddLabel(label1);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_TRUE(res.GetValue());
+    }
+
+    ASSERT_FALSE(vertex->HasLabel(label1, View::OLD).GetValue());
+    ASSERT_TRUE(vertex->HasLabel(label1, View::NEW).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label2, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label2, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label1);
+    }
+
+    {
+      auto res = vertex->AddLabel(label1);
+      ASSERT_TRUE(res.HasValue());
+      ASSERT_FALSE(res.GetValue());
+    }
+  }
+
+  // Add label 2 in accessor 2.
+  {
+    auto vertex = acc2.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label1 = acc2.NameToLabel("label1");
+    auto label2 = acc2.NameToLabel("label2");
+
+    ASSERT_FALSE(vertex->HasLabel(label1, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label1, View::NEW).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label2, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label2, View::NEW).GetValue());
+    ASSERT_EQ(vertex->Labels(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Labels(View::NEW)->size(), 0);
+
+    {
+      auto res = vertex->AddLabel(label1);
+      ASSERT_TRUE(res.HasError());
+      ASSERT_EQ(res.GetError(), Error::SERIALIZATION_ERROR);
+    }
+  }
+
+  // Finalize both accessors.
+  ASSERT_FALSE(acc1.Commit().HasError());
+  acc2.Abort();
+
+  // Check which labels exist.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto label1 = acc.NameToLabel("label1");
+    auto label2 = acc.NameToLabel("label2");
+
+    ASSERT_TRUE(vertex->HasLabel(label1, View::OLD).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label2, View::OLD).GetValue());
+    {
+      auto labels = vertex->Labels(View::OLD).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label1);
+    }
+
+    ASSERT_TRUE(vertex->HasLabel(label1, View::NEW).GetValue());
+    ASSERT_FALSE(vertex->HasLabel(label2, View::NEW).GetValue());
+    {
+      auto labels = vertex->Labels(View::NEW).GetValue();
+      ASSERT_EQ(labels.size(), 1);
+      ASSERT_EQ(labels[0], label1);
+    }
+
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexPropertyCommit) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_TRUE(vertex.GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex.Properties(View::NEW)->size(), 0);
+
+    {
+      auto old_value = vertex.SetProperty(property, PropertyValue("temporary"));
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_TRUE(old_value->IsNull());
+    }
+
+    ASSERT_EQ(vertex.GetProperty(property, View::NEW)->ValueString(), "temporary");
+    {
+      auto properties = vertex.Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "temporary");
+    }
+
+    {
+      auto old_value = vertex.SetProperty(property, PropertyValue("nandare"));
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_FALSE(old_value->IsNull());
+    }
+
+    ASSERT_EQ(vertex.GetProperty(property, View::NEW)->ValueString(), "nandare");
+    {
+      auto properties = vertex.Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    auto other_property = acc.NameToProperty("other");
+
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::NEW)->IsNull());
+
+    acc.Abort();
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    {
+      auto old_value = vertex->SetProperty(property, PropertyValue());
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_FALSE(old_value->IsNull());
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    {
+      auto old_value = vertex->SetProperty(property, PropertyValue());
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_TRUE(old_value->IsNull());
+    }
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_TRUE(vertex->GetProperty(property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    auto other_property = acc.NameToProperty("other");
+
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::NEW)->IsNull());
+
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexPropertyAbort) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+
+  // Create the vertex.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Set property 5 to "nandare", but abort the transaction.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    {
+      auto old_value = vertex->SetProperty(property, PropertyValue("temporary"));
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_TRUE(old_value->IsNull());
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "temporary");
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "temporary");
+    }
+
+    {
+      auto old_value = vertex->SetProperty(property, PropertyValue("nandare"));
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_FALSE(old_value->IsNull());
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    acc.Abort();
+  }
+
+  // Check that property 5 is null.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_TRUE(vertex->GetProperty(property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    auto other_property = acc.NameToProperty("other");
+
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::NEW)->IsNull());
+
+    acc.Abort();
+  }
+
+  // Set property 5 to "nandare".
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    {
+      auto old_value = vertex->SetProperty(property, PropertyValue("temporary"));
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_TRUE(old_value->IsNull());
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "temporary");
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "temporary");
+    }
+
+    {
+      auto old_value = vertex->SetProperty(property, PropertyValue("nandare"));
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_FALSE(old_value->IsNull());
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Check that property 5 is "nandare".
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    auto other_property = acc.NameToProperty("other");
+
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::NEW)->IsNull());
+
+    acc.Abort();
+  }
+
+  // Set property 5 to null, but abort the transaction.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    {
+      auto old_value = vertex->SetProperty(property, PropertyValue());
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_FALSE(old_value->IsNull());
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    acc.Abort();
+  }
+
+  // Check that property 5 is "nandare".
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    auto other_property = acc.NameToProperty("other");
+
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::NEW)->IsNull());
+
+    acc.Abort();
+  }
+
+  // Set property 5 to null.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::NEW)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    {
+      auto old_value = vertex->SetProperty(property, PropertyValue());
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_FALSE(old_value->IsNull());
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property, View::OLD)->ValueString(), "nandare");
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property].ValueString(), "nandare");
+    }
+
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Check that property 5 is null.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property = acc.NameToProperty("property5");
+
+    ASSERT_TRUE(vertex->GetProperty(property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    auto other_property = acc.NameToProperty("other");
+
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(other_property, View::NEW)->IsNull());
+
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexPropertySerializationError) {
+  Gid gid = Gid::FromUint(std::numeric_limits<uint64_t>::max());
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  auto acc1 = store.Access();
+  auto acc2 = store.Access();
+
+  // Set property 1 to 123 in accessor 1.
+  {
+    auto vertex = acc1.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property1 = acc1.NameToProperty("property1");
+    auto property2 = acc1.NameToProperty("property2");
+
+    ASSERT_TRUE(vertex->GetProperty(property1, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property1, View::NEW)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    {
+      auto old_value = vertex->SetProperty(property1, PropertyValue(123));
+      ASSERT_TRUE(old_value.HasValue());
+      ASSERT_TRUE(old_value->IsNull());
+    }
+
+    ASSERT_TRUE(vertex->GetProperty(property1, View::OLD)->IsNull());
+    ASSERT_EQ(vertex->GetProperty(property1, View::NEW)->ValueInt(), 123);
+    ASSERT_TRUE(vertex->GetProperty(property2, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property1].ValueInt(), 123);
+    }
+  }
+
+  // Set property 2 to "nandare" in accessor 2.
+  {
+    auto vertex = acc2.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property1 = acc2.NameToProperty("property1");
+    auto property2 = acc2.NameToProperty("property2");
+
+    ASSERT_TRUE(vertex->GetProperty(property1, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property1, View::NEW)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::OLD)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::OLD)->size(), 0);
+    ASSERT_EQ(vertex->Properties(View::NEW)->size(), 0);
+
+    {
+      auto res = vertex->SetProperty(property2, PropertyValue("nandare"));
+      ASSERT_TRUE(res.HasError());
+      ASSERT_EQ(res.GetError(), Error::SERIALIZATION_ERROR);
+    }
+  }
+
+  // Finalize both accessors.
+  ASSERT_FALSE(acc1.Commit().HasError());
+  acc2.Abort();
+
+  // Check which properties exist.
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto property1 = acc.NameToProperty("property1");
+    auto property2 = acc.NameToProperty("property2");
+
+    ASSERT_EQ(vertex->GetProperty(property1, View::OLD)->ValueInt(), 123);
+    ASSERT_TRUE(vertex->GetProperty(property2, View::OLD)->IsNull());
+    {
+      auto properties = vertex->Properties(View::OLD).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property1].ValueInt(), 123);
+    }
+
+    ASSERT_EQ(vertex->GetProperty(property1, View::NEW)->ValueInt(), 123);
+    ASSERT_TRUE(vertex->GetProperty(property2, View::NEW)->IsNull());
+    {
+      auto properties = vertex->Properties(View::NEW).GetValue();
+      ASSERT_EQ(properties.size(), 1);
+      ASSERT_EQ(properties[property1].ValueInt(), 123);
+    }
+
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, VertexLabelPropertyMixed) {
+  auto acc = store.Access();
+  auto vertex = acc.CreateVertex();
+
+  auto label = acc.NameToLabel("label5");
+  auto property = acc.NameToProperty("property5");
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_FALSE(vertex.HasLabel(label, View::NEW).GetValue());
+  ASSERT_EQ(vertex.Labels(View::NEW)->size(), 0);
+  ASSERT_TRUE(vertex.GetProperty(property, View::NEW)->IsNull());
+  ASSERT_EQ(vertex.Properties(View::NEW)->size(), 0);
+
+  // Add label 5
+  ASSERT_TRUE(vertex.AddLabel(label).GetValue());
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_TRUE(vertex.HasLabel(label, View::NEW).GetValue());
+  {
+    auto labels = vertex.Labels(View::NEW).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  ASSERT_TRUE(vertex.GetProperty(property, View::NEW)->IsNull());
+  ASSERT_EQ(vertex.Properties(View::NEW)->size(), 0);
+
+  // Advance command
+  acc.AdvanceCommand();
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_TRUE(vertex.HasLabel(label, View::OLD).GetValue());
+  ASSERT_TRUE(vertex.HasLabel(label, View::NEW).GetValue());
+  {
+    auto labels = vertex.Labels(View::OLD).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  {
+    auto labels = vertex.Labels(View::NEW).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  ASSERT_TRUE(vertex.GetProperty(property, View::OLD)->IsNull());
+  ASSERT_TRUE(vertex.GetProperty(property, View::NEW)->IsNull());
+  ASSERT_EQ(vertex.Properties(View::OLD)->size(), 0);
+  ASSERT_EQ(vertex.Properties(View::NEW)->size(), 0);
+
+  // Set property 5 to "nandare"
+  ASSERT_TRUE(vertex.SetProperty(property, PropertyValue("nandare"))->IsNull());
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_TRUE(vertex.HasLabel(label, View::OLD).GetValue());
+  ASSERT_TRUE(vertex.HasLabel(label, View::NEW).GetValue());
+  {
+    auto labels = vertex.Labels(View::OLD).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  {
+    auto labels = vertex.Labels(View::NEW).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  ASSERT_TRUE(vertex.GetProperty(property, View::OLD)->IsNull());
+  ASSERT_EQ(vertex.GetProperty(property, View::NEW)->ValueString(), "nandare");
+  ASSERT_EQ(vertex.Properties(View::OLD)->size(), 0);
+  {
+    auto properties = vertex.Properties(View::NEW).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "nandare");
+  }
+
+  // Advance command
+  acc.AdvanceCommand();
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_TRUE(vertex.HasLabel(label, View::OLD).GetValue());
+  ASSERT_TRUE(vertex.HasLabel(label, View::NEW).GetValue());
+  {
+    auto labels = vertex.Labels(View::OLD).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  {
+    auto labels = vertex.Labels(View::NEW).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  ASSERT_EQ(vertex.GetProperty(property, View::OLD)->ValueString(), "nandare");
+  ASSERT_EQ(vertex.GetProperty(property, View::NEW)->ValueString(), "nandare");
+  {
+    auto properties = vertex.Properties(View::OLD).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "nandare");
+  }
+  {
+    auto properties = vertex.Properties(View::NEW).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "nandare");
+  }
+
+  // Set property 5 to "haihai"
+  ASSERT_FALSE(vertex.SetProperty(property, PropertyValue("haihai"))->IsNull());
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_TRUE(vertex.HasLabel(label, View::OLD).GetValue());
+  ASSERT_TRUE(vertex.HasLabel(label, View::NEW).GetValue());
+  {
+    auto labels = vertex.Labels(View::OLD).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  {
+    auto labels = vertex.Labels(View::NEW).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  ASSERT_EQ(vertex.GetProperty(property, View::OLD)->ValueString(), "nandare");
+  ASSERT_EQ(vertex.GetProperty(property, View::NEW)->ValueString(), "haihai");
+  {
+    auto properties = vertex.Properties(View::OLD).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "nandare");
+  }
+  {
+    auto properties = vertex.Properties(View::NEW).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "haihai");
+  }
+
+  // Advance command
+  acc.AdvanceCommand();
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_TRUE(vertex.HasLabel(label, View::OLD).GetValue());
+  ASSERT_TRUE(vertex.HasLabel(label, View::NEW).GetValue());
+  {
+    auto labels = vertex.Labels(View::OLD).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  {
+    auto labels = vertex.Labels(View::NEW).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  ASSERT_EQ(vertex.GetProperty(property, View::OLD)->ValueString(), "haihai");
+  ASSERT_EQ(vertex.GetProperty(property, View::NEW)->ValueString(), "haihai");
+  {
+    auto properties = vertex.Properties(View::OLD).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "haihai");
+  }
+  {
+    auto properties = vertex.Properties(View::NEW).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "haihai");
+  }
+
+  // Remove label 5
+  ASSERT_TRUE(vertex.RemoveLabel(label).GetValue());
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_TRUE(vertex.HasLabel(label, View::OLD).GetValue());
+  ASSERT_FALSE(vertex.HasLabel(label, View::NEW).GetValue());
+  {
+    auto labels = vertex.Labels(View::OLD).GetValue();
+    ASSERT_EQ(labels.size(), 1);
+    ASSERT_EQ(labels[0], label);
+  }
+  ASSERT_EQ(vertex.Labels(View::NEW)->size(), 0);
+  ASSERT_EQ(vertex.GetProperty(property, View::OLD)->ValueString(), "haihai");
+  ASSERT_EQ(vertex.GetProperty(property, View::NEW)->ValueString(), "haihai");
+  {
+    auto properties = vertex.Properties(View::OLD).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "haihai");
+  }
+  {
+    auto properties = vertex.Properties(View::NEW).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "haihai");
+  }
+
+  // Advance command
+  acc.AdvanceCommand();
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_FALSE(vertex.HasLabel(label, View::OLD).GetValue());
+  ASSERT_FALSE(vertex.HasLabel(label, View::NEW).GetValue());
+  ASSERT_EQ(vertex.Labels(View::OLD)->size(), 0);
+  ASSERT_EQ(vertex.Labels(View::NEW)->size(), 0);
+  ASSERT_EQ(vertex.GetProperty(property, View::OLD)->ValueString(), "haihai");
+  ASSERT_EQ(vertex.GetProperty(property, View::NEW)->ValueString(), "haihai");
+  {
+    auto properties = vertex.Properties(View::OLD).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "haihai");
+  }
+  {
+    auto properties = vertex.Properties(View::NEW).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "haihai");
+  }
+
+  // Set property 5 to null
+  ASSERT_FALSE(vertex.SetProperty(property, PropertyValue())->IsNull());
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_FALSE(vertex.HasLabel(label, View::OLD).GetValue());
+  ASSERT_FALSE(vertex.HasLabel(label, View::NEW).GetValue());
+  ASSERT_EQ(vertex.Labels(View::OLD)->size(), 0);
+  ASSERT_EQ(vertex.Labels(View::NEW)->size(), 0);
+  ASSERT_EQ(vertex.GetProperty(property, View::OLD)->ValueString(), "haihai");
+  ASSERT_TRUE(vertex.GetProperty(property, View::NEW)->IsNull());
+  {
+    auto properties = vertex.Properties(View::OLD).GetValue();
+    ASSERT_EQ(properties.size(), 1);
+    ASSERT_EQ(properties[property].ValueString(), "haihai");
+  }
+  ASSERT_EQ(vertex.Properties(View::NEW)->size(), 0);
+
+  // Advance command
+  acc.AdvanceCommand();
+
+  // Check whether label 5 and property 5 exist
+  ASSERT_FALSE(vertex.HasLabel(label, View::OLD).GetValue());
+  ASSERT_FALSE(vertex.HasLabel(label, View::NEW).GetValue());
+  ASSERT_EQ(vertex.Labels(View::OLD)->size(), 0);
+  ASSERT_EQ(vertex.Labels(View::NEW)->size(), 0);
+  ASSERT_TRUE(vertex.GetProperty(property, View::NEW)->IsNull());
+  ASSERT_TRUE(vertex.GetProperty(property, View::NEW)->IsNull());
+  ASSERT_EQ(vertex.Properties(View::OLD)->size(), 0);
+  ASSERT_EQ(vertex.Properties(View::NEW)->size(), 0);
+
+  ASSERT_FALSE(acc.Commit().HasError());
+}
+
+TEST_F(StorageV3, VertexPropertyClear) {
+  Gid gid;
+  auto property1 = store.NameToProperty("property1");
+  auto property2 = store.NameToProperty("property2");
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+
+    auto old_value = vertex.SetProperty(property1, PropertyValue("value"));
+    ASSERT_TRUE(old_value.HasValue());
+    ASSERT_TRUE(old_value->IsNull());
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    ASSERT_EQ(vertex->GetProperty(property1, View::OLD)->ValueString(), "value");
+    ASSERT_TRUE(vertex->GetProperty(property2, View::OLD)->IsNull());
+    ASSERT_THAT(vertex->Properties(View::OLD).GetValue(),
+                UnorderedElementsAre(std::pair(property1, PropertyValue("value"))));
+
+    {
+      auto old_values = vertex->ClearProperties();
+      ASSERT_TRUE(old_values.HasValue());
+      ASSERT_FALSE(old_values->empty());
+    }
+
+    ASSERT_TRUE(vertex->GetProperty(property1, View::NEW)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW).GetValue().size(), 0);
+
+    {
+      auto old_values = vertex->ClearProperties();
+      ASSERT_TRUE(old_values.HasValue());
+      ASSERT_TRUE(old_values->empty());
+    }
+
+    ASSERT_TRUE(vertex->GetProperty(property1, View::NEW)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW).GetValue().size(), 0);
+
+    acc.Abort();
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    auto old_value = vertex->SetProperty(property2, PropertyValue(42));
+    ASSERT_TRUE(old_value.HasValue());
+    ASSERT_TRUE(old_value->IsNull());
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    ASSERT_EQ(vertex->GetProperty(property1, View::OLD)->ValueString(), "value");
+    ASSERT_EQ(vertex->GetProperty(property2, View::OLD)->ValueInt(), 42);
+    ASSERT_THAT(
+        vertex->Properties(View::OLD).GetValue(),
+        UnorderedElementsAre(std::pair(property1, PropertyValue("value")), std::pair(property2, PropertyValue(42))));
+
+    {
+      auto old_values = vertex->ClearProperties();
+      ASSERT_TRUE(old_values.HasValue());
+      ASSERT_FALSE(old_values->empty());
+    }
+
+    ASSERT_TRUE(vertex->GetProperty(property1, View::NEW)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW).GetValue().size(), 0);
+
+    {
+      auto old_values = vertex->ClearProperties();
+      ASSERT_TRUE(old_values.HasValue());
+      ASSERT_TRUE(old_values->empty());
+    }
+
+    ASSERT_TRUE(vertex->GetProperty(property1, View::NEW)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW).GetValue().size(), 0);
+
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+  {
+    auto acc = store.Access();
+    auto vertex = acc.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    ASSERT_TRUE(vertex->GetProperty(property1, View::NEW)->IsNull());
+    ASSERT_TRUE(vertex->GetProperty(property2, View::NEW)->IsNull());
+    ASSERT_EQ(vertex->Properties(View::NEW).GetValue().size(), 0);
+
+    acc.Abort();
+  }
+}
+
+TEST_F(StorageV3, VertexNonexistentLabelPropertyEdgeAPI) {
+  auto label = store.NameToLabel("label");
+  auto property = store.NameToProperty("property");
+
+  auto acc = store.Access();
+  auto vertex = acc.CreateVertex();
+
+  // Check state before (OLD view).
+  ASSERT_EQ(vertex.Labels(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.HasLabel(label, View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.Properties(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.GetProperty(property, View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.InEdges(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.OutEdges(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.InDegree(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.OutDegree(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+
+  // Check state before (NEW view).
+  ASSERT_EQ(vertex.Labels(View::NEW)->size(), 0);
+  ASSERT_EQ(*vertex.HasLabel(label, View::NEW), false);
+  ASSERT_EQ(vertex.Properties(View::NEW)->size(), 0);
+  ASSERT_EQ(*vertex.GetProperty(property, View::NEW), PropertyValue());
+  ASSERT_EQ(vertex.InEdges(View::NEW)->size(), 0);
+  ASSERT_EQ(vertex.OutEdges(View::NEW)->size(), 0);
+  ASSERT_EQ(*vertex.InDegree(View::NEW), 0);
+  ASSERT_EQ(*vertex.OutDegree(View::NEW), 0);
+
+  // Modify vertex.
+  ASSERT_TRUE(vertex.AddLabel(label).HasValue());
+  ASSERT_TRUE(vertex.SetProperty(property, PropertyValue("value")).HasValue());
+  ASSERT_TRUE(acc.CreateEdge(&vertex, &vertex, acc.NameToEdgeType("edge")).HasValue());
+
+  // Check state after (OLD view).
+  ASSERT_EQ(vertex.Labels(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.HasLabel(label, View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.Properties(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.GetProperty(property, View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.InEdges(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.OutEdges(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.InDegree(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+  ASSERT_EQ(vertex.OutDegree(View::OLD).GetError(), Error::NONEXISTENT_OBJECT);
+
+  // Check state after (NEW view).
+  ASSERT_EQ(vertex.Labels(View::NEW)->size(), 1);
+  ASSERT_EQ(*vertex.HasLabel(label, View::NEW), true);
+  ASSERT_EQ(vertex.Properties(View::NEW)->size(), 1);
+  ASSERT_EQ(*vertex.GetProperty(property, View::NEW), PropertyValue("value"));
+  ASSERT_EQ(vertex.InEdges(View::NEW)->size(), 1);
+  ASSERT_EQ(vertex.OutEdges(View::NEW)->size(), 1);
+  ASSERT_EQ(*vertex.InDegree(View::NEW), 1);
+  ASSERT_EQ(*vertex.OutDegree(View::NEW), 1);
+
+  ASSERT_FALSE(acc.Commit().HasError());
+}
+
+TEST_F(StorageV3, VertexVisibilitySingleTransaction) {
+  auto acc1 = store.Access();
+  auto acc2 = store.Access();
+
+  auto vertex = acc1.CreateVertex();
+  auto gid = vertex.Gid();
+
+  EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+  EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::NEW));
+
+  ASSERT_TRUE(vertex.AddLabel(acc1.NameToLabel("label")).HasValue());
+
+  EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+  EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::NEW));
+
+  ASSERT_TRUE(vertex.SetProperty(acc1.NameToProperty("meaning"), PropertyValue(42)).HasValue());
+
+  auto acc3 = store.Access();
+
+  EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+  EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::NEW));
+  EXPECT_FALSE(acc3.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc3.FindVertex(gid, View::NEW));
+
+  ASSERT_TRUE(acc1.DeleteVertex(&vertex).HasValue());
+
+  EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::NEW));
+  EXPECT_FALSE(acc3.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc3.FindVertex(gid, View::NEW));
+
+  acc1.AdvanceCommand();
+  acc3.AdvanceCommand();
+
+  EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc2.FindVertex(gid, View::NEW));
+  EXPECT_FALSE(acc3.FindVertex(gid, View::OLD));
+  EXPECT_FALSE(acc3.FindVertex(gid, View::NEW));
+
+  acc1.Abort();
+  acc2.Abort();
+  acc3.Abort();
+}
+
+TEST_F(StorageV3, VertexVisibilityMultipleTransactions) {
+  Gid gid;
+
+  {
+    auto acc1 = store.Access();
+    auto acc2 = store.Access();
+
+    auto vertex = acc1.CreateVertex();
+    gid = vertex.Gid();
+
+    EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_FALSE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc2.FindVertex(gid, View::NEW));
+
+    acc2.AdvanceCommand();
+
+    EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_FALSE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc2.FindVertex(gid, View::NEW));
+
+    acc1.AdvanceCommand();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_FALSE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc2.FindVertex(gid, View::NEW));
+
+    ASSERT_FALSE(acc1.Commit().HasError());
+    ASSERT_FALSE(acc2.Commit().HasError());
+  }
+
+  {
+    auto acc1 = store.Access();
+    auto acc2 = store.Access();
+
+    auto vertex = acc1.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+
+    ASSERT_TRUE(vertex->AddLabel(acc1.NameToLabel("label")).HasValue());
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+
+    acc1.AdvanceCommand();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+
+    acc2.AdvanceCommand();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+
+    ASSERT_TRUE(vertex->SetProperty(acc1.NameToProperty("meaning"), PropertyValue(42)).HasValue());
+
+    auto acc3 = store.Access();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc1.AdvanceCommand();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc2.AdvanceCommand();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc3.AdvanceCommand();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    ASSERT_FALSE(acc1.Commit().HasError());
+    ASSERT_FALSE(acc2.Commit().HasError());
+    ASSERT_FALSE(acc3.Commit().HasError());
+  }
+
+  {
+    auto acc1 = store.Access();
+    auto acc2 = store.Access();
+
+    auto vertex = acc1.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    ASSERT_TRUE(acc1.DeleteVertex(&*vertex).HasValue());
+
+    auto acc3 = store.Access();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc2.AdvanceCommand();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc1.AdvanceCommand();
+
+    EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc3.AdvanceCommand();
+
+    EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc1.Abort();
+    acc2.Abort();
+    acc3.Abort();
+  }
+
+  {
+    auto acc = store.Access();
+
+    EXPECT_TRUE(acc.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc.FindVertex(gid, View::NEW));
+
+    acc.AdvanceCommand();
+
+    EXPECT_TRUE(acc.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc.FindVertex(gid, View::NEW));
+
+    acc.Abort();
+  }
+
+  {
+    auto acc1 = store.Access();
+    auto acc2 = store.Access();
+
+    auto vertex = acc1.FindVertex(gid, View::OLD);
+    ASSERT_TRUE(vertex);
+
+    ASSERT_TRUE(acc1.DeleteVertex(&*vertex).HasValue());
+
+    auto acc3 = store.Access();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc2.AdvanceCommand();
+
+    EXPECT_TRUE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc1.AdvanceCommand();
+
+    EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    acc3.AdvanceCommand();
+
+    EXPECT_FALSE(acc1.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc1.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc2.FindVertex(gid, View::NEW));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::OLD));
+    EXPECT_TRUE(acc3.FindVertex(gid, View::NEW));
+
+    ASSERT_FALSE(acc1.Commit().HasError());
+    ASSERT_FALSE(acc2.Commit().HasError());
+    ASSERT_FALSE(acc3.Commit().HasError());
+  }
+
+  {
+    auto acc = store.Access();
+
+    EXPECT_FALSE(acc.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc.FindVertex(gid, View::NEW));
+
+    acc.AdvanceCommand();
+
+    EXPECT_FALSE(acc.FindVertex(gid, View::OLD));
+    EXPECT_FALSE(acc.FindVertex(gid, View::NEW));
+
+    acc.Abort();
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(StorageV3, DeletedVertexAccessor) {
+  const auto property = store.NameToProperty("property");
+  const PropertyValue property_value{"property_value"};
+
+  std::optional<Gid> gid;
+  // Create the vertex
+  {
+    auto acc = store.Access();
+    auto vertex = acc.CreateVertex();
+    gid = vertex.Gid();
+    ASSERT_FALSE(vertex.SetProperty(property, property_value).HasError());
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  auto acc = store.Access();
+  auto vertex = acc.FindVertex(*gid, View::OLD);
+  ASSERT_TRUE(vertex);
+  auto maybe_deleted_vertex = acc.DeleteVertex(&*vertex);
+  ASSERT_FALSE(maybe_deleted_vertex.HasError());
+
+  auto deleted_vertex = maybe_deleted_vertex.GetValue();
+  ASSERT_TRUE(deleted_vertex);
+  // you cannot modify deleted vertex
+  ASSERT_TRUE(deleted_vertex->ClearProperties().HasError());
+
+  // you can call read only methods
+  const auto maybe_property = deleted_vertex->GetProperty(property, View::OLD);
+  ASSERT_FALSE(maybe_property.HasError());
+  ASSERT_EQ(property_value, *maybe_property);
+  ASSERT_FALSE(acc.Commit().HasError());
+
+  {
+    // you can call read only methods and get valid results even after the
+    // transaction which deleted the vertex committed, but only if the transaction
+    // accessor is still alive
+    const auto maybe_property = deleted_vertex->GetProperty(property, View::OLD);
+    ASSERT_FALSE(maybe_property.HasError());
+    ASSERT_EQ(property_value, *maybe_property);
+  }
+}
+}  // namespace memgraph::storage::v3::tests
diff --git a/tests/unit/storage_v3_key_store.cpp b/tests/unit/storage_v3_key_store.cpp
new file mode 100644
index 000000000..4e9ae4da1
--- /dev/null
+++ b/tests/unit/storage_v3_key_store.cpp
@@ -0,0 +1,46 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+/**
+ * gtest/gtest.h must be included before rapidcheck/gtest.h!
+ */
+#include <gtest/gtest.h>
+#include <rapidcheck.h>
+#include <rapidcheck/gtest.h>
+
+#include "storage/v3/id_types.hpp"
+#include "storage/v3/key_store.hpp"
+#include "storage/v3/property_value.hpp"
+
+namespace memgraph::storage::v3::test {
+
+RC_GTEST_PROP(KeyStore, KeyStore, (std::vector<std::string> values)) {
+  RC_PRE(!values.empty());
+
+  std::vector<PropertyValue> property_values;
+  property_values.reserve(values.size());
+  std::transform(values.begin(), values.end(), std::back_inserter(property_values),
+                 [](std::string &value) { return PropertyValue{std::move(value)}; });
+
+  KeyStore key_store{property_values};
+
+  const auto keys = key_store.Keys();
+  RC_ASSERT(keys.size() == property_values.size());
+  for (int i = 0; i < keys.size(); ++i) {
+    RC_ASSERT(keys[i] == property_values[i]);
+    RC_ASSERT(key_store.GetKey(i) == property_values[i]);
+  }
+}
+}  // namespace memgraph::storage::v3::test
diff --git a/tests/unit/storage_v3_property_store.cpp b/tests/unit/storage_v3_property_store.cpp
new file mode 100644
index 000000000..2e4a1da19
--- /dev/null
+++ b/tests/unit/storage_v3_property_store.cpp
@@ -0,0 +1,622 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include <array>
+#include <limits>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "storage/v3/property_store.hpp"
+#include "storage/v3/property_value.hpp"
+#include "storage/v3/temporal.hpp"
+
+namespace memgraph::storage::v3::tests {
+
+class StorageV3PropertyStore : public ::testing::Test {
+ protected:
+  PropertyStore props;
+
+  const std::array<PropertyValue, 24> kSampleValues = {
+      PropertyValue(),
+      PropertyValue(false),
+      PropertyValue(true),
+      PropertyValue(0),
+      PropertyValue(33),
+      PropertyValue(-33),
+      PropertyValue(-3137),
+      PropertyValue(3137),
+      PropertyValue(310000007),
+      PropertyValue(-310000007),
+      PropertyValue(3100000000007L),
+      PropertyValue(-3100000000007L),
+      PropertyValue(0.0),
+      PropertyValue(33.33),
+      PropertyValue(-33.33),
+      PropertyValue(3137.3137),
+      PropertyValue(-3137.3137),
+      PropertyValue("sample"),
+      PropertyValue(std::string(404, 'n')),
+      PropertyValue(
+          std::vector<PropertyValue>{PropertyValue(33), PropertyValue(std::string("sample")), PropertyValue(-33.33)}),
+      PropertyValue(std::vector<PropertyValue>{PropertyValue(), PropertyValue(false)}),
+      PropertyValue(std::map<std::string, PropertyValue>{{"sample", PropertyValue()}, {"key", PropertyValue(false)}}),
+      PropertyValue(std::map<std::string, PropertyValue>{
+          {"test", PropertyValue(33)}, {"map", PropertyValue(std::string("sample"))}, {"item", PropertyValue(-33.33)}}),
+      PropertyValue(TemporalData(TemporalType::Date, 23)),
+  };
+
+  void AssertPropertyIsEqual(const PropertyStore &store, PropertyId property, const PropertyValue &value) {
+    ASSERT_TRUE(store.IsPropertyEqual(property, value));
+    for (const auto &sample : kSampleValues) {
+      if (sample == value) {
+        ASSERT_TRUE(store.IsPropertyEqual(property, sample));
+      } else {
+        ASSERT_FALSE(store.IsPropertyEqual(property, sample));
+      }
+    }
+  }
+};
+
+using testing::UnorderedElementsAre;
+
+TEST_F(StorageV3PropertyStore, StoreTwoProperties) {
+  const auto make_prop = [](int64_t prop_id_and_value) {
+    auto prop = PropertyId::FromInt(prop_id_and_value);
+    auto value = PropertyValue(prop_id_and_value);
+    return std::make_pair(prop, value);
+  };
+
+  const auto first_prop_and_value = make_prop(42);
+  const auto second_prop_and_value = make_prop(43);
+  ASSERT_TRUE(props.SetProperty(first_prop_and_value.first, first_prop_and_value.second));
+  ASSERT_TRUE(props.SetProperty(second_prop_and_value.first, second_prop_and_value.second));
+  ASSERT_THAT(props.Properties(), UnorderedElementsAre(first_prop_and_value, second_prop_and_value));
+}
+
+TEST_F(StorageV3PropertyStore, Simple) {
+  auto prop = PropertyId::FromInt(42);
+  auto value = PropertyValue(42);
+  ASSERT_TRUE(props.SetProperty(prop, value));
+  ASSERT_EQ(props.GetProperty(prop), value);
+  ASSERT_TRUE(props.HasProperty(prop));
+  AssertPropertyIsEqual(props, prop, value);
+  ASSERT_THAT(props.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+
+  ASSERT_FALSE(props.SetProperty(prop, PropertyValue()));
+  ASSERT_TRUE(props.GetProperty(prop).IsNull());
+  ASSERT_FALSE(props.HasProperty(prop));
+  AssertPropertyIsEqual(props, prop, PropertyValue());
+  ASSERT_EQ(props.Properties().size(), 0);
+}
+
+TEST_F(StorageV3PropertyStore, SimpleLarge) {
+  auto prop = PropertyId::FromInt(42);
+  {
+    auto value = PropertyValue(std::string(10000, 'a'));
+    ASSERT_TRUE(props.SetProperty(prop, value));
+    ASSERT_EQ(props.GetProperty(prop), value);
+    ASSERT_TRUE(props.HasProperty(prop));
+    AssertPropertyIsEqual(props, prop, value);
+    ASSERT_THAT(props.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  }
+  {
+    auto value = PropertyValue(TemporalData(TemporalType::Date, 23));
+    ASSERT_FALSE(props.SetProperty(prop, value));
+    ASSERT_EQ(props.GetProperty(prop), value);
+    ASSERT_TRUE(props.HasProperty(prop));
+    AssertPropertyIsEqual(props, prop, value);
+    ASSERT_THAT(props.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  }
+
+  ASSERT_FALSE(props.SetProperty(prop, PropertyValue()));
+  ASSERT_TRUE(props.GetProperty(prop).IsNull());
+  ASSERT_FALSE(props.HasProperty(prop));
+  AssertPropertyIsEqual(props, prop, PropertyValue());
+  ASSERT_EQ(props.Properties().size(), 0);
+}
+
+TEST_F(StorageV3PropertyStore, EmptySetToNull) {
+  auto prop = PropertyId::FromInt(42);
+  ASSERT_TRUE(props.SetProperty(prop, PropertyValue()));
+  ASSERT_TRUE(props.GetProperty(prop).IsNull());
+  ASSERT_FALSE(props.HasProperty(prop));
+  AssertPropertyIsEqual(props, prop, PropertyValue());
+  ASSERT_EQ(props.Properties().size(), 0);
+}
+
+TEST_F(StorageV3PropertyStore, Clear) {
+  auto prop = PropertyId::FromInt(42);
+  auto value = PropertyValue(42);
+  ASSERT_TRUE(props.SetProperty(prop, value));
+  ASSERT_EQ(props.GetProperty(prop), value);
+  ASSERT_TRUE(props.HasProperty(prop));
+  AssertPropertyIsEqual(props, prop, value);
+  ASSERT_THAT(props.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  ASSERT_TRUE(props.ClearProperties());
+  ASSERT_TRUE(props.GetProperty(prop).IsNull());
+  ASSERT_FALSE(props.HasProperty(prop));
+  AssertPropertyIsEqual(props, prop, PropertyValue());
+  ASSERT_EQ(props.Properties().size(), 0);
+}
+
+TEST_F(StorageV3PropertyStore, EmptyClear) {
+  ASSERT_FALSE(props.ClearProperties());
+  ASSERT_EQ(props.Properties().size(), 0);
+}
+
+TEST_F(StorageV3PropertyStore, MoveConstruct) {
+  PropertyStore props1;
+  auto prop = PropertyId::FromInt(42);
+  auto value = PropertyValue(42);
+  ASSERT_TRUE(props1.SetProperty(prop, value));
+  ASSERT_EQ(props1.GetProperty(prop), value);
+  ASSERT_TRUE(props1.HasProperty(prop));
+  AssertPropertyIsEqual(props1, prop, value);
+  ASSERT_THAT(props1.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  {
+    PropertyStore props2(std::move(props1));
+    ASSERT_EQ(props2.GetProperty(prop), value);
+    ASSERT_TRUE(props2.HasProperty(prop));
+    AssertPropertyIsEqual(props2, prop, value);
+    ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  }
+  // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved)
+  ASSERT_TRUE(props1.GetProperty(prop).IsNull());
+  ASSERT_FALSE(props1.HasProperty(prop));
+  AssertPropertyIsEqual(props1, prop, PropertyValue());
+  ASSERT_EQ(props1.Properties().size(), 0);
+}
+
+TEST_F(StorageV3PropertyStore, MoveConstructLarge) {
+  PropertyStore props1;
+  auto prop = PropertyId::FromInt(42);
+  auto value = PropertyValue(std::string(10000, 'a'));
+  ASSERT_TRUE(props1.SetProperty(prop, value));
+  ASSERT_EQ(props1.GetProperty(prop), value);
+  ASSERT_TRUE(props1.HasProperty(prop));
+  AssertPropertyIsEqual(props1, prop, value);
+  ASSERT_THAT(props1.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  {
+    PropertyStore props2(std::move(props1));
+    ASSERT_EQ(props2.GetProperty(prop), value);
+    ASSERT_TRUE(props2.HasProperty(prop));
+    AssertPropertyIsEqual(props2, prop, value);
+    ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  }
+  // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved)
+  ASSERT_TRUE(props1.GetProperty(prop).IsNull());
+  ASSERT_FALSE(props1.HasProperty(prop));
+  AssertPropertyIsEqual(props1, prop, PropertyValue());
+  ASSERT_EQ(props1.Properties().size(), 0);
+}
+
+TEST_F(StorageV3PropertyStore, MoveAssign) {
+  PropertyStore props1;
+  auto prop = PropertyId::FromInt(42);
+  auto value = PropertyValue(42);
+  ASSERT_TRUE(props1.SetProperty(prop, value));
+  ASSERT_EQ(props1.GetProperty(prop), value);
+  ASSERT_TRUE(props1.HasProperty(prop));
+  AssertPropertyIsEqual(props1, prop, value);
+  ASSERT_THAT(props1.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  {
+    auto value2 = PropertyValue(68);
+    PropertyStore props2;
+    ASSERT_TRUE(props2.SetProperty(prop, value2));
+    ASSERT_EQ(props2.GetProperty(prop), value2);
+    ASSERT_TRUE(props2.HasProperty(prop));
+    AssertPropertyIsEqual(props2, prop, value2);
+    ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value2)));
+    props2 = std::move(props1);
+    ASSERT_EQ(props2.GetProperty(prop), value);
+    ASSERT_TRUE(props2.HasProperty(prop));
+    AssertPropertyIsEqual(props2, prop, value);
+    ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  }
+  // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved)
+  ASSERT_TRUE(props1.GetProperty(prop).IsNull());
+  ASSERT_FALSE(props1.HasProperty(prop));
+  AssertPropertyIsEqual(props1, prop, PropertyValue());
+  ASSERT_EQ(props1.Properties().size(), 0);
+}
+
+TEST_F(StorageV3PropertyStore, MoveAssignLarge) {
+  PropertyStore props1;
+  auto prop = PropertyId::FromInt(42);
+  auto value = PropertyValue(std::string(10000, 'a'));
+  ASSERT_TRUE(props1.SetProperty(prop, value));
+  ASSERT_EQ(props1.GetProperty(prop), value);
+  ASSERT_TRUE(props1.HasProperty(prop));
+  AssertPropertyIsEqual(props1, prop, value);
+  ASSERT_THAT(props1.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  {
+    auto value2 = PropertyValue(std::string(10000, 'b'));
+    PropertyStore props2;
+    ASSERT_TRUE(props2.SetProperty(prop, value2));
+    ASSERT_EQ(props2.GetProperty(prop), value2);
+    ASSERT_TRUE(props2.HasProperty(prop));
+    AssertPropertyIsEqual(props2, prop, value2);
+    ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value2)));
+    props2 = std::move(props1);
+    ASSERT_EQ(props2.GetProperty(prop), value);
+    ASSERT_TRUE(props2.HasProperty(prop));
+    AssertPropertyIsEqual(props2, prop, value);
+    ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+  }
+  // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved)
+  ASSERT_TRUE(props1.GetProperty(prop).IsNull());
+  ASSERT_FALSE(props1.HasProperty(prop));
+  AssertPropertyIsEqual(props1, prop, PropertyValue());
+  ASSERT_EQ(props1.Properties().size(), 0);
+}
+
+TEST_F(StorageV3PropertyStore, EmptySet) {
+  std::vector<PropertyValue> vec{PropertyValue(true), PropertyValue(123), PropertyValue()};
+  std::map<std::string, PropertyValue> map{{"nandare", PropertyValue(false)}};
+  const TemporalData temporal{TemporalType::LocalDateTime, 23};
+  std::vector<PropertyValue> data{PropertyValue(true),      PropertyValue(123), PropertyValue(123.5),
+                                  PropertyValue("nandare"), PropertyValue(vec), PropertyValue(map),
+                                  PropertyValue(temporal)};
+
+  auto prop = PropertyId::FromInt(42);
+  for (const auto &value : data) {
+    PropertyStore local_props;
+
+    ASSERT_TRUE(local_props.SetProperty(prop, value));
+    ASSERT_EQ(local_props.GetProperty(prop), value);
+    ASSERT_TRUE(local_props.HasProperty(prop));
+    AssertPropertyIsEqual(local_props, prop, value);
+    ASSERT_THAT(local_props.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+    ASSERT_FALSE(local_props.SetProperty(prop, value));
+    ASSERT_EQ(local_props.GetProperty(prop), value);
+    ASSERT_TRUE(local_props.HasProperty(prop));
+    AssertPropertyIsEqual(local_props, prop, value);
+    ASSERT_THAT(local_props.Properties(), UnorderedElementsAre(std::pair(prop, value)));
+    ASSERT_FALSE(local_props.SetProperty(prop, PropertyValue()));
+    ASSERT_TRUE(local_props.GetProperty(prop).IsNull());
+    ASSERT_FALSE(local_props.HasProperty(prop));
+    AssertPropertyIsEqual(local_props, prop, PropertyValue());
+    ASSERT_EQ(local_props.Properties().size(), 0);
+    ASSERT_TRUE(local_props.SetProperty(prop, PropertyValue()));
+    ASSERT_TRUE(local_props.GetProperty(prop).IsNull());
+    ASSERT_FALSE(local_props.HasProperty(prop));
+    AssertPropertyIsEqual(local_props, prop, PropertyValue());
+    ASSERT_EQ(local_props.Properties().size(), 0);
+  }
+}
+
+TEST_F(StorageV3PropertyStore, FullSet) {
+  std::vector<PropertyValue> vec{PropertyValue(true), PropertyValue(123), PropertyValue()};
+  std::map<std::string, PropertyValue> map{{"nandare", PropertyValue(false)}};
+  const TemporalData temporal{TemporalType::LocalDateTime, 23};
+  std::map<PropertyId, PropertyValue> data{
+      {PropertyId::FromInt(1), PropertyValue(true)},    {PropertyId::FromInt(2), PropertyValue(123)},
+      {PropertyId::FromInt(3), PropertyValue(123.5)},   {PropertyId::FromInt(4), PropertyValue("nandare")},
+      {PropertyId::FromInt(5), PropertyValue(vec)},     {PropertyId::FromInt(6), PropertyValue(map)},
+      {PropertyId::FromInt(7), PropertyValue(temporal)}};
+
+  std::vector<PropertyValue> alt{PropertyValue(),
+                                 PropertyValue(std::string()),
+                                 PropertyValue(std::string(10, 'a')),
+                                 PropertyValue(std::string(100, 'a')),
+                                 PropertyValue(std::string(1000, 'a')),
+                                 PropertyValue(std::string(10000, 'a')),
+                                 PropertyValue(std::string(100000, 'a'))};
+
+  for (const auto &target : data) {
+    for (const auto &item : data) {
+      ASSERT_TRUE(props.SetProperty(item.first, item.second));
+    }
+
+    for (size_t i = 0; i < alt.size(); ++i) {
+      if (i == 1) {
+        ASSERT_TRUE(props.SetProperty(target.first, alt[i]));
+      } else {
+        ASSERT_FALSE(props.SetProperty(target.first, alt[i]));
+      }
+      for (const auto &item : data) {
+        if (item.first == target.first) {
+          ASSERT_EQ(props.GetProperty(item.first), alt[i]);
+          if (alt[i].IsNull()) {
+            ASSERT_FALSE(props.HasProperty(item.first));
+          } else {
+            ASSERT_TRUE(props.HasProperty(item.first));
+          }
+          AssertPropertyIsEqual(props, item.first, alt[i]);
+        } else {
+          ASSERT_EQ(props.GetProperty(item.first), item.second);
+          ASSERT_TRUE(props.HasProperty(item.first));
+          AssertPropertyIsEqual(props, item.first, item.second);
+        }
+      }
+      auto current = data;
+      if (alt[i].IsNull()) {
+        current.erase(target.first);
+      } else {
+        current[target.first] = alt[i];
+      }
+      ASSERT_EQ(props.Properties(), current);
+    }
+
+    for (ssize_t i = alt.size() - 1; i >= 0; --i) {
+      ASSERT_FALSE(props.SetProperty(target.first, alt[i]));
+      for (const auto &item : data) {
+        if (item.first == target.first) {
+          ASSERT_EQ(props.GetProperty(item.first), alt[i]);
+          if (alt[i].IsNull()) {
+            ASSERT_FALSE(props.HasProperty(item.first));
+          } else {
+            ASSERT_TRUE(props.HasProperty(item.first));
+          }
+          AssertPropertyIsEqual(props, item.first, alt[i]);
+        } else {
+          ASSERT_EQ(props.GetProperty(item.first), item.second);
+          ASSERT_TRUE(props.HasProperty(item.first));
+          AssertPropertyIsEqual(props, item.first, item.second);
+        }
+      }
+      auto current = data;
+      if (alt[i].IsNull()) {
+        current.erase(target.first);
+      } else {
+        current[target.first] = alt[i];
+      }
+      ASSERT_EQ(props.Properties(), current);
+    }
+
+    ASSERT_TRUE(props.SetProperty(target.first, target.second));
+    ASSERT_EQ(props.GetProperty(target.first), target.second);
+    ASSERT_TRUE(props.HasProperty(target.first));
+    AssertPropertyIsEqual(props, target.first, target.second);
+
+    props.ClearProperties();
+    ASSERT_EQ(props.Properties().size(), 0);
+    for (const auto &item : data) {
+      ASSERT_TRUE(props.GetProperty(item.first).IsNull());
+      ASSERT_FALSE(props.HasProperty(item.first));
+      AssertPropertyIsEqual(props, item.first, PropertyValue());
+    }
+  }
+}
+
+TEST_F(StorageV3PropertyStore, IntEncoding) {
+  std::map<PropertyId, PropertyValue> data{
+      // {PropertyId::FromUint(0UL),
+      //  PropertyValue(std::numeric_limits<int64_t>::min())},
+      // {PropertyId::FromUint(10UL), PropertyValue(-137438953472L)},
+      // {PropertyId::FromUint(std::numeric_limits<uint8_t>::max()),
+      //  PropertyValue(-4294967297L)},
+      // {PropertyId::FromUint(256UL),
+      //  PropertyValue(std::numeric_limits<int32_t>::min())},
+      // {PropertyId::FromUint(1024UL), PropertyValue(-1048576L)},
+      // {PropertyId::FromUint(1025UL), PropertyValue(-65537L)},
+      // {PropertyId::FromUint(1026UL),
+      //  PropertyValue(std::numeric_limits<int16_t>::min())},
+      // {PropertyId::FromUint(1027UL), PropertyValue(-1024L)},
+      // {PropertyId::FromUint(2000UL), PropertyValue(-257L)},
+      // {PropertyId::FromUint(3000UL),
+      //  PropertyValue(std::numeric_limits<int8_t>::min())},
+      // {PropertyId::FromUint(4000UL), PropertyValue(-1L)},
+      // {PropertyId::FromUint(10000UL), PropertyValue(0L)},
+      // {PropertyId::FromUint(20000UL), PropertyValue(1L)},
+      // {PropertyId::FromUint(30000UL),
+      //  PropertyValue(std::numeric_limits<int8_t>::max())},
+      // {PropertyId::FromUint(40000UL), PropertyValue(256L)},
+      // {PropertyId::FromUint(50000UL), PropertyValue(1024L)},
+      // {PropertyId::FromUint(std::numeric_limits<uint16_t>::max()),
+      //  PropertyValue(std::numeric_limits<int16_t>::max())},
+      // {PropertyId::FromUint(65536UL), PropertyValue(65536L)},
+      // {PropertyId::FromUint(1048576UL), PropertyValue(1048576L)},
+      // {PropertyId::FromUint(std::numeric_limits<uint32_t>::max()),
+      //  PropertyValue(std::numeric_limits<int32_t>::max())},
+      {PropertyId::FromUint(4294967296UL), PropertyValue(4294967296L)},
+      {PropertyId::FromUint(137438953472UL), PropertyValue(137438953472L)},
+      {PropertyId::FromUint(std::numeric_limits<uint64_t>::max()), PropertyValue(std::numeric_limits<int64_t>::max())}};
+
+  for (const auto &item : data) {
+    ASSERT_TRUE(props.SetProperty(item.first, item.second));
+    ASSERT_EQ(props.GetProperty(item.first), item.second);
+    ASSERT_TRUE(props.HasProperty(item.first));
+    AssertPropertyIsEqual(props, item.first, item.second);
+  }
+  for (auto it = data.rbegin(); it != data.rend(); ++it) {
+    const auto &item = *it;
+    ASSERT_FALSE(props.SetProperty(item.first, item.second)) << item.first.AsInt();
+    ASSERT_EQ(props.GetProperty(item.first), item.second);
+    ASSERT_TRUE(props.HasProperty(item.first));
+    AssertPropertyIsEqual(props, item.first, item.second);
+  }
+
+  ASSERT_EQ(props.Properties(), data);
+
+  props.ClearProperties();
+  ASSERT_EQ(props.Properties().size(), 0);
+  for (const auto &item : data) {
+    ASSERT_TRUE(props.GetProperty(item.first).IsNull());
+    ASSERT_FALSE(props.HasProperty(item.first));
+    AssertPropertyIsEqual(props, item.first, PropertyValue());
+  }
+}
+
+TEST_F(StorageV3PropertyStore, IsPropertyEqualIntAndDouble) {
+  auto prop = PropertyId::FromInt(42);
+
+  ASSERT_TRUE(props.SetProperty(prop, PropertyValue(42)));
+
+  std::vector<std::pair<PropertyValue, PropertyValue>> tests{
+      {PropertyValue(0), PropertyValue(0.0)},
+      {PropertyValue(123), PropertyValue(123.0)},
+      {PropertyValue(12345), PropertyValue(12345.0)},
+      {PropertyValue(12345678), PropertyValue(12345678.0)},
+      {PropertyValue(1234567890123L), PropertyValue(1234567890123.0)},
+  };
+
+  // Test equality with raw values.
+  for (auto test : tests) {
+    ASSERT_EQ(test.first, test.second);
+
+    // Test first, second
+    ASSERT_FALSE(props.SetProperty(prop, test.first));
+    ASSERT_EQ(props.GetProperty(prop), test.first);
+    ASSERT_TRUE(props.HasProperty(prop));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.first));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.second));
+
+    // Test second, first
+    ASSERT_FALSE(props.SetProperty(prop, test.second));
+    ASSERT_EQ(props.GetProperty(prop), test.second);
+    ASSERT_TRUE(props.HasProperty(prop));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.second));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.first));
+
+    // Make both negative
+    test.first = PropertyValue(test.first.ValueInt() * -1);
+    test.second = PropertyValue(test.second.ValueDouble() * -1.0);
+    ASSERT_EQ(test.first, test.second);
+
+    // Test -first, -second
+    ASSERT_FALSE(props.SetProperty(prop, test.first));
+    ASSERT_EQ(props.GetProperty(prop), test.first);
+    ASSERT_TRUE(props.HasProperty(prop));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.first));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.second));
+
+    // Test -second, -first
+    ASSERT_FALSE(props.SetProperty(prop, test.second));
+    ASSERT_EQ(props.GetProperty(prop), test.second);
+    ASSERT_TRUE(props.HasProperty(prop));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.second));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.first));
+  }
+
+  // Test equality with values wrapped in lists.
+  for (auto test : tests) {
+    test.first = PropertyValue(std::vector<PropertyValue>{PropertyValue(test.first.ValueInt())});
+    test.second = PropertyValue(std::vector<PropertyValue>{PropertyValue(test.second.ValueDouble())});
+    ASSERT_EQ(test.first, test.second);
+
+    // Test first, second
+    ASSERT_FALSE(props.SetProperty(prop, test.first));
+    ASSERT_EQ(props.GetProperty(prop), test.first);
+    ASSERT_TRUE(props.HasProperty(prop));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.first));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.second));
+
+    // Test second, first
+    ASSERT_FALSE(props.SetProperty(prop, test.second));
+    ASSERT_EQ(props.GetProperty(prop), test.second);
+    ASSERT_TRUE(props.HasProperty(prop));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.second));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.first));
+
+    // Make both negative
+    test.first = PropertyValue(std::vector<PropertyValue>{PropertyValue(test.first.ValueList()[0].ValueInt() * -1)});
+    test.second =
+        PropertyValue(std::vector<PropertyValue>{PropertyValue(test.second.ValueList()[0].ValueDouble() * -1.0)});
+    ASSERT_EQ(test.first, test.second);
+
+    // Test -first, -second
+    ASSERT_FALSE(props.SetProperty(prop, test.first));
+    ASSERT_EQ(props.GetProperty(prop), test.first);
+    ASSERT_TRUE(props.HasProperty(prop));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.first));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.second));
+
+    // Test -second, -first
+    ASSERT_FALSE(props.SetProperty(prop, test.second));
+    ASSERT_EQ(props.GetProperty(prop), test.second);
+    ASSERT_TRUE(props.HasProperty(prop));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.second));
+    ASSERT_TRUE(props.IsPropertyEqual(prop, test.first));
+  }
+}
+
+TEST_F(StorageV3PropertyStore, IsPropertyEqualString) {
+  auto prop = PropertyId::FromInt(42);
+  ASSERT_TRUE(props.SetProperty(prop, PropertyValue("test")));
+  ASSERT_TRUE(props.IsPropertyEqual(prop, PropertyValue("test")));
+
+  // Different length.
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue("helloworld")));
+
+  // Same length, different value.
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue("asdf")));
+
+  // Shortened and extended.
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue("tes")));
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue("testt")));
+}
+
+TEST_F(StorageV3PropertyStore, IsPropertyEqualList) {
+  auto prop = PropertyId::FromInt(42);
+  ASSERT_TRUE(
+      props.SetProperty(prop, PropertyValue(std::vector<PropertyValue>{PropertyValue(42), PropertyValue("test")})));
+  ASSERT_TRUE(
+      props.IsPropertyEqual(prop, PropertyValue(std::vector<PropertyValue>{PropertyValue(42), PropertyValue("test")})));
+
+  // Different length.
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue(std::vector<PropertyValue>{PropertyValue(24)})));
+
+  // Same length, different value.
+  ASSERT_FALSE(
+      props.IsPropertyEqual(prop, PropertyValue(std::vector<PropertyValue>{PropertyValue(42), PropertyValue("asdf")})));
+
+  // Shortened and extended.
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue(std::vector<PropertyValue>{PropertyValue(42)})));
+  ASSERT_FALSE(props.IsPropertyEqual(
+      prop, PropertyValue(std::vector<PropertyValue>{PropertyValue(42), PropertyValue("test"), PropertyValue(true)})));
+}
+
+TEST_F(StorageV3PropertyStore, IsPropertyEqualMap) {
+  auto prop = PropertyId::FromInt(42);
+  ASSERT_TRUE(props.SetProperty(prop, PropertyValue(std::map<std::string, PropertyValue>{
+                                          {"abc", PropertyValue(42)}, {"zyx", PropertyValue("test")}})));
+  ASSERT_TRUE(props.IsPropertyEqual(prop, PropertyValue(std::map<std::string, PropertyValue>{
+                                              {"abc", PropertyValue(42)}, {"zyx", PropertyValue("test")}})));
+
+  // Different length.
+  ASSERT_FALSE(
+      props.IsPropertyEqual(prop, PropertyValue(std::map<std::string, PropertyValue>{{"fgh", PropertyValue(24)}})));
+
+  // Same length, different value.
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue(std::map<std::string, PropertyValue>{
+                                               {"abc", PropertyValue(42)}, {"zyx", PropertyValue("testt")}})));
+
+  // Same length, different key (different length).
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue(std::map<std::string, PropertyValue>{
+                                               {"abc", PropertyValue(42)}, {"zyxw", PropertyValue("test")}})));
+
+  // Same length, different key (same length).
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue(std::map<std::string, PropertyValue>{
+                                               {"abc", PropertyValue(42)}, {"zyw", PropertyValue("test")}})));
+
+  // Shortened and extended.
+  ASSERT_FALSE(
+      props.IsPropertyEqual(prop, PropertyValue(std::map<std::string, PropertyValue>{{"abc", PropertyValue(42)}})));
+  ASSERT_FALSE(props.IsPropertyEqual(
+      prop, PropertyValue(std::map<std::string, PropertyValue>{
+                {"abc", PropertyValue(42)}, {"sdf", PropertyValue(true)}, {"zyx", PropertyValue("test")}})));
+}
+
+TEST_F(StorageV3PropertyStore, IsPropertyEqualTemporalData) {
+  auto prop = PropertyId::FromInt(42);
+  const TemporalData temporal{TemporalType::Date, 23};
+  ASSERT_TRUE(props.SetProperty(prop, PropertyValue(temporal)));
+  ASSERT_TRUE(props.IsPropertyEqual(prop, PropertyValue(temporal)));
+
+  // Different type.
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue(TemporalData{TemporalType::Duration, 23})));
+
+  // Same type, different value.
+  ASSERT_FALSE(props.IsPropertyEqual(prop, PropertyValue(TemporalData{TemporalType::Date, 30})));
+}
+}  // namespace memgraph::storage::v3::tests
diff --git a/tests/unit/storage_v3_test_utils.cpp b/tests/unit/storage_v3_test_utils.cpp
new file mode 100644
index 000000000..b1a59b7aa
--- /dev/null
+++ b/tests/unit/storage_v3_test_utils.cpp
@@ -0,0 +1,23 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include "storage_v3_test_utils.hpp"
+
+namespace memgraph::storage::v3::tests {
+
+size_t CountVertices(Storage::Accessor &storage_accessor, View view) {
+  auto vertices = storage_accessor.Vertices(view);
+  size_t count = 0U;
+  for (auto it = vertices.begin(); it != vertices.end(); ++it, ++count)
+    ;
+  return count;
+}
+}  // namespace memgraph::storage::v3::tests
diff --git a/tests/unit/storage_v3_test_utils.hpp b/tests/unit/storage_v3_test_utils.hpp
new file mode 100644
index 000000000..1cc772036
--- /dev/null
+++ b/tests/unit/storage_v3_test_utils.hpp
@@ -0,0 +1,21 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include "storage/v3/storage.hpp"
+#include "storage/v3/view.hpp"
+
+namespace memgraph::storage::v3::tests {
+
+size_t CountVertices(Storage::Accessor &storage_accessor, View view);
+
+}  // namespace memgraph::storage::v3::tests