diff --git a/src/storage/v3/key_store.cpp b/src/storage/v3/key_store.cpp
index 7f8e69ec9..4e65f5fd1 100644
--- a/src/storage/v3/key_store.cpp
+++ b/src/storage/v3/key_store.cpp
@@ -13,6 +13,7 @@
 #include <iterator>
 #include <ranges>
 
+#include "storage/v3/id_types.hpp"
 #include "storage/v3/key_store.hpp"
 #include "storage/v3/property_value.hpp"
 
@@ -27,6 +28,8 @@ KeyStore::KeyStore(const PrimaryKey &key_values) {
 
 PropertyValue KeyStore::GetKey(const size_t index) const { return store_.GetProperty(PropertyId::FromUint(index)); }
 
+PropertyValue KeyStore::GetKey(const PropertyId property_id) const { return store_.GetProperty(property_id); }
+
 PrimaryKey KeyStore::Keys() const {
   auto keys_map = store_.Properties();
   PrimaryKey keys;
diff --git a/src/storage/v3/key_store.hpp b/src/storage/v3/key_store.hpp
index 034e6c280..c65132d9a 100644
--- a/src/storage/v3/key_store.hpp
+++ b/src/storage/v3/key_store.hpp
@@ -15,6 +15,7 @@
 #include <compare>
 #include <functional>
 
+#include "storage/v3/id_types.hpp"
 #include "storage/v3/property_store.hpp"
 #include "storage/v3/property_value.hpp"
 
@@ -36,6 +37,8 @@ class KeyStore {
 
   PropertyValue GetKey(size_t index) const;
 
+  PropertyValue GetKey(PropertyId property) const;
+
   PrimaryKey Keys() const;
 
   friend bool operator<(const KeyStore &lhs, const KeyStore &rhs) {
diff --git a/src/storage/v3/shard.cpp b/src/storage/v3/shard.cpp
index 48040a80c..bd90327c9 100644
--- a/src/storage/v3/shard.cpp
+++ b/src/storage/v3/shard.cpp
@@ -494,23 +494,31 @@ ResultSchema<VertexAccessor> Shard::Accessor::CreateVertexAndValidate(
           return property_pair.first == property_id;
         })->second);
   }
-
-  // Get secondary properties
-  std::vector<std::pair<PropertyId, PropertyValue>> secondary_properties;
-  for (const auto &[property_id, property_value] : properties) {
-    if (!shard_->schemas_.IsPropertyKey(primary_label, property_id)) {
-      secondary_properties.emplace_back(property_id, property_value);
-    }
-  }
-
   auto acc = shard_->vertices_.access();
   auto *delta = CreateDeleteObjectDelta(&transaction_);
-  auto [it, inserted] = acc.insert({Vertex{delta, primary_label, primary_properties, labels, secondary_properties}});
+  auto [it, inserted] = acc.insert({Vertex{delta, primary_label, primary_properties}});
+
+  VertexAccessor vertex_acc{&it->vertex,           &transaction_, &shard_->indices_,
+                            &shard_->constraints_, config_,       shard_->schema_validator_};
   MG_ASSERT(inserted, "The vertex must be inserted here!");
   MG_ASSERT(it != acc.end(), "Invalid Vertex accessor!");
+
+  // TODO(jbajic) Improve, maybe delay index update
+  for (const auto &[property_id, property_value] : properties) {
+    if (!shard_->schemas_.IsPropertyKey(primary_label, property_id)) {
+      if (const auto err = vertex_acc.SetProperty(property_id, property_value); err.HasError()) {
+        return {err.GetError()};
+      }
+    }
+  }
+  // Set secondary labels
+  for (auto label : labels) {
+    if (const auto err = vertex_acc.AddLabel(label); err.HasError()) {
+      return {err.GetError()};
+    }
+  }
   delta->prev.Set(&it->vertex);
-  return VertexAccessor{&it->vertex,           &transaction_, &shard_->indices_,
-                        &shard_->constraints_, config_,       shard_->schema_validator_};
+  return vertex_acc;
 }
 
 std::optional<VertexAccessor> Shard::Accessor::FindVertex(std::vector<PropertyValue> primary_key, View view) {
@@ -1065,8 +1073,10 @@ const std::string &Shard::EdgeTypeToName(EdgeTypeId edge_type) const {
 }
 
 bool Shard::CreateIndex(LabelId label, const std::optional<uint64_t> desired_commit_timestamp) {
-  // TODO Fix Index
-  return false;
+  // TODO(jbajic) response should be different when label == primary_label
+  if (label == primary_label_ || !indices_.label_index.CreateIndex(label, vertices_.access())) {
+    return false;
+  }
   const auto commit_timestamp = CommitTimestamp(desired_commit_timestamp);
   AppendToWal(durability::StorageGlobalOperation::LABEL_INDEX_CREATE, label, {}, commit_timestamp);
   commit_log_->MarkFinished(commit_timestamp);
@@ -1075,9 +1085,15 @@ bool Shard::CreateIndex(LabelId label, const std::optional<uint64_t> desired_com
 }
 
 bool Shard::CreateIndex(LabelId label, PropertyId property, const std::optional<uint64_t> desired_commit_timestamp) {
-  // TODO Fix Index
-  // if (!indices_.label_property_index.CreateIndex(label, property, labelspace.access())) return false;
-  return false;
+  // TODO(jbajic) response should be different when index conflicts with schema
+  if (label == primary_label_ && schemas_.GetSchema(primary_label_)->second.size() == 1 &&
+      schemas_.GetSchema(primary_label_)->second[0].property_id == property) {
+    // Index already exists on primary key
+    return false;
+  }
+  if (!indices_.label_property_index.CreateIndex(label, property, vertices_.access())) {
+    return false;
+  }
   const auto commit_timestamp = CommitTimestamp(desired_commit_timestamp);
   AppendToWal(durability::StorageGlobalOperation::LABEL_PROPERTY_INDEX_CREATE, label, {property}, commit_timestamp);
   commit_log_->MarkFinished(commit_timestamp);
diff --git a/src/storage/v3/vertex.hpp b/src/storage/v3/vertex.hpp
index 50cb0333d..08ae747b3 100644
--- a/src/storage/v3/vertex.hpp
+++ b/src/storage/v3/vertex.hpp
@@ -34,32 +34,11 @@ struct Vertex {
               "Vertex must be created with an initial DELETE_OBJECT delta!");
   }
 
-  Vertex(Delta *delta, LabelId primary_label, const std::vector<PropertyValue> &primary_properties,
-         const std::vector<LabelId> &secondary_labels,
-         const std::vector<std::pair<PropertyId, PropertyValue>> &secondary_properties)
-      : primary_label{primary_label}, keys{primary_properties}, labels{secondary_labels}, delta{delta} {
-    MG_ASSERT(delta == nullptr || delta->action == Delta::Action::DELETE_OBJECT,
-              "Vertex must be created with an initial DELETE_OBJECT delta!");
-    for (const auto &[property_id, property_value] : secondary_properties) {
-      properties.SetProperty(property_id, property_value);
-    }
-  }
-
   Vertex(LabelId primary_label, const std::vector<PropertyValue> &primary_properties)
       : primary_label{primary_label}, keys(primary_properties) {
     MG_ASSERT(delta == nullptr || delta->action == Delta::Action::DELETE_OBJECT,
               "Vertex must be created with an initial DELETE_OBJECT delta!");
   }
-  Vertex(LabelId primary_label, const std::vector<PropertyValue> &primary_properties,
-         const std::vector<LabelId> &secondary_labels,
-         const std::vector<std::pair<PropertyId, PropertyValue>> &secondary_properties)
-      : primary_label{primary_label}, keys{primary_properties}, labels{secondary_labels} {
-    MG_ASSERT(delta == nullptr || delta->action == Delta::Action::DELETE_OBJECT,
-              "Vertex must be created with an initial DELETE_OBJECT delta!");
-    for (const auto &[property_id, property_value] : secondary_properties) {
-      properties.SetProperty(property_id, property_value);
-    }
-  }
 
   LabelId primary_label;
   KeyStore keys;
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index c112fe9b2..5a9d41f98 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -334,6 +334,9 @@ 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)
 
+add_unit_test(storage_v3_indices.cpp)
+target_link_libraries(${test_prefix}storage_v3_indices mg-storage-v3)
+
 add_unit_test(storage_v3_vertex_accessors.cpp)
 target_link_libraries(${test_prefix}storage_v3_vertex_accessors mg-storage-v3)
 
diff --git a/tests/unit/storage_v3_indices.cpp b/tests/unit/storage_v3_indices.cpp
new file mode 100644
index 000000000..708fae156
--- /dev/null
+++ b/tests/unit/storage_v3_indices.cpp
@@ -0,0 +1,989 @@
+// 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 <gmock/gmock-matchers.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <cstdint>
+
+#include "storage/v3/id_types.hpp"
+#include "storage/v3/name_id_mapper.hpp"
+#include "storage/v3/property_value.hpp"
+#include "storage/v3/storage.hpp"
+#include "storage/v3/temporal.hpp"
+
+// NOLINTNEXTLINE(google-build-using-namespace)
+
+using testing::IsEmpty;
+using testing::Pair;
+using testing::UnorderedElementsAre;
+
+// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
+#define ASSERT_NO_ERROR(result) ASSERT_FALSE((result).HasError())
+
+namespace memgraph::storage::v3::tests {
+
+class IndexTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    ASSERT_TRUE(
+        storage.CreateSchema(primary_label, {storage::v3::SchemaProperty{primary_property, common::SchemaType::INT}}));
+  }
+
+  NameIdMapper id_mapper;
+  const std::vector<PropertyValue> pk{PropertyValue{0}};
+  const LabelId primary_label{NameToLabelId("label")};
+  Shard storage{primary_label, pk, std::nullopt};
+  const PropertyId primary_property{NameToPropertyId("property")};
+
+  const PropertyId prop_id{NameToPropertyId("id")};
+  const PropertyId prop_val{NameToPropertyId("val")};
+  const LabelId label1{NameToLabelId("label1")};
+  const LabelId label2{NameToLabelId("label2")};
+  int primary_key_id{0};
+  int vertex_id{0};
+
+  LabelId NameToLabelId(std::string_view label_name) { return LabelId::FromUint(id_mapper.NameToId(label_name)); }
+
+  PropertyId NameToPropertyId(std::string_view property_name) {
+    return PropertyId::FromUint(id_mapper.NameToId(property_name));
+  }
+
+  VertexAccessor CreateVertex(Shard::Accessor *accessor) {
+    auto vertex = *accessor->CreateVertexAndValidate(
+        primary_label, {},
+        {{primary_property, PropertyValue(primary_key_id++)}, {prop_id, PropertyValue(vertex_id++)}});
+    return vertex;
+  }
+
+  template <class TIterable>
+  std::vector<int64_t> GetIds(TIterable iterable, View view = View::OLD) {
+    std::vector<int64_t> ret;
+    for (auto vertex : iterable) {
+      ret.push_back(vertex.GetProperty(prop_id, view)->ValueInt());
+    }
+    return ret;
+  }
+
+  template <class TIterable>
+  std::vector<int64_t> GetPrimaryKeyIds(TIterable iterable, View view = View::OLD) {
+    std::vector<int64_t> ret;
+    for (auto vertex : iterable) {
+      EXPECT_TRUE(vertex.PrimaryKey(view).HasValue());
+      const auto pk = vertex.PrimaryKey(view).GetValue();
+      EXPECT_EQ(pk.size(), 1);
+      ret.push_back(pk[0].ValueInt());
+    }
+    return ret;
+  }
+};
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelIndexCreate) {
+  {
+    auto acc = storage.Access();
+    EXPECT_FALSE(acc.LabelIndexExists(label1));
+  }
+  EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+
+  {
+    auto acc = storage.Access();
+    for (int i = 0; i < 10; ++i) {
+      auto vertex = CreateVertex(&acc);
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(i % 2 ? label1 : label2));
+    }
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+
+  EXPECT_TRUE(storage.CreateIndex(label1));
+
+  {
+    auto acc = storage.Access();
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), UnorderedElementsAre(1, 3, 5, 7, 9));
+  }
+
+  {
+    auto acc = storage.Access();
+    for (int i = 10; i < 20; ++i) {
+      auto vertex = CreateVertex(&acc);
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(i % 2 ? label1 : label2));
+    }
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+
+    acc.AdvanceCommand();
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+
+    acc.Abort();
+  }
+
+  {
+    auto acc = storage.Access();
+    for (int i = 10; i < 20; ++i) {
+      auto vertex = CreateVertex(&acc);
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(i % 2 ? label1 : label2));
+    }
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+    acc.AdvanceCommand();
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+
+  {
+    auto acc = storage.Access();
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+    acc.AdvanceCommand();
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelIndexDrop) {
+  {
+    auto acc = storage.Access();
+    EXPECT_FALSE(acc.LabelIndexExists(label1));
+  }
+  EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+
+  {
+    auto acc = storage.Access();
+    for (int i = 0; i < 10; ++i) {
+      auto vertex = CreateVertex(&acc);
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(i % 2 ? label1 : label2));
+    }
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+
+  EXPECT_TRUE(storage.CreateIndex(label1));
+
+  {
+    auto acc = storage.Access();
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), UnorderedElementsAre(1, 3, 5, 7, 9));
+  }
+
+  EXPECT_TRUE(storage.DropIndex(label1));
+  {
+    auto acc = storage.Access();
+    EXPECT_FALSE(acc.LabelIndexExists(label1));
+  }
+  EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+
+  EXPECT_FALSE(storage.DropIndex(label1));
+  {
+    auto acc = storage.Access();
+    EXPECT_FALSE(acc.LabelIndexExists(label1));
+  }
+  EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+
+  {
+    auto acc = storage.Access();
+    for (int i = 10; i < 20; ++i) {
+      auto vertex = CreateVertex(&acc);
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(i % 2 ? label1 : label2));
+    }
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+
+  EXPECT_TRUE(storage.CreateIndex(label1));
+  {
+    auto acc = storage.Access();
+    EXPECT_TRUE(acc.LabelIndexExists(label1));
+  }
+  EXPECT_THAT(storage.ListAllIndices().label, UnorderedElementsAre(label1));
+
+  {
+    auto acc = storage.Access();
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+
+    acc.AdvanceCommand();
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelIndexBasic) {
+  // The following steps are performed and index correctness is validated after
+  // each step:
+  // 1. Create 10 vertices numbered from 0 to 9.
+  // 2. Add Label1 to odd numbered, and Label2 to even numbered vertices.
+  // 3. Remove Label1 from odd numbered vertices, and add it to even numbered
+  //    vertices.
+  // 4. Delete even numbered vertices.
+  EXPECT_TRUE(storage.CreateIndex(label1));
+  EXPECT_TRUE(storage.CreateIndex(label2));
+
+  auto acc = storage.Access();
+  EXPECT_THAT(storage.ListAllIndices().label, UnorderedElementsAre(label1, label2));
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::NEW), View::NEW), IsEmpty());
+
+  for (int i = 0; i < 10; ++i) {
+    auto vertex = CreateVertex(&acc);
+    ASSERT_NO_ERROR(vertex.AddLabelAndValidate(i % 2 ? label1 : label2));
+  }
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::NEW), View::NEW), UnorderedElementsAre(0, 2, 4, 6, 8));
+
+  acc.AdvanceCommand();
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::OLD), View::OLD), UnorderedElementsAre(0, 2, 4, 6, 8));
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::NEW), View::NEW), UnorderedElementsAre(0, 2, 4, 6, 8));
+
+  for (auto vertex : acc.Vertices(View::OLD)) {
+    int64_t id = vertex.GetProperty(prop_id, View::OLD)->ValueInt();
+    if (id % 2) {
+      ASSERT_NO_ERROR(vertex.RemoveLabelAndValidate(label1));
+    } else {
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(label1));
+    }
+  }
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::OLD), View::OLD), UnorderedElementsAre(0, 2, 4, 6, 8));
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), UnorderedElementsAre(0, 2, 4, 6, 8));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::NEW), View::NEW), UnorderedElementsAre(0, 2, 4, 6, 8));
+
+  for (auto vertex : acc.Vertices(View::OLD)) {
+    int64_t id = vertex.GetProperty(prop_id, View::OLD)->ValueInt();
+    if (id % 2 == 0) {
+      ASSERT_NO_ERROR(acc.DeleteVertex(&vertex));
+    }
+  }
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::OLD), View::OLD), UnorderedElementsAre(0, 2, 4, 6, 8));
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::NEW), View::NEW), IsEmpty());
+
+  acc.AdvanceCommand();
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, View::NEW), View::NEW), IsEmpty());
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelIndexDuplicateVersions) {
+  // By removing labels and adding them again we create duplicate entries for
+  // the same vertex in the index (they only differ by the timestamp). This test
+  // checks that duplicates are properly filtered out.
+  EXPECT_TRUE(storage.CreateIndex(label1));
+  EXPECT_TRUE(storage.CreateIndex(label2));
+
+  {
+    auto acc = storage.Access();
+    for (int i = 0; i < 5; ++i) {
+      auto vertex = CreateVertex(&acc);
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(label1));
+    }
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), UnorderedElementsAre(0, 1, 2, 3, 4));
+
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+
+  {
+    auto acc = storage.Access();
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(0, 1, 2, 3, 4));
+
+    for (auto vertex : acc.Vertices(View::OLD)) {
+      ASSERT_NO_ERROR(vertex.RemoveLabelAndValidate(label1));
+    }
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(0, 1, 2, 3, 4));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), IsEmpty());
+
+    for (auto vertex : acc.Vertices(View::OLD)) {
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(label1));
+    }
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(0, 1, 2, 3, 4));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), UnorderedElementsAre(0, 1, 2, 3, 4));
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelIndexTransactionalIsolation) {
+  // Check that transactions only see entries they are supposed to see.
+  EXPECT_TRUE(storage.CreateIndex(label1));
+  EXPECT_TRUE(storage.CreateIndex(label2));
+
+  auto acc_before = storage.Access();
+  auto acc = storage.Access();
+  auto acc_after = storage.Access();
+
+  for (int i = 0; i < 5; ++i) {
+    auto vertex = CreateVertex(&acc);
+    ASSERT_NO_ERROR(vertex.AddLabelAndValidate(label1));
+  }
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, View::NEW), View::NEW), UnorderedElementsAre(0, 1, 2, 3, 4));
+  EXPECT_THAT(GetIds(acc_before.Vertices(label1, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc_after.Vertices(label1, View::NEW), View::NEW), IsEmpty());
+
+  ASSERT_NO_ERROR(acc.Commit());
+
+  auto acc_after_commit = storage.Access();
+
+  EXPECT_THAT(GetIds(acc_before.Vertices(label1, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc_after.Vertices(label1, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc_after_commit.Vertices(label1, View::NEW), View::NEW), UnorderedElementsAre(0, 1, 2, 3, 4));
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelIndexCountEstimate) {
+  EXPECT_TRUE(storage.CreateIndex(label1));
+  EXPECT_TRUE(storage.CreateIndex(label2));
+
+  auto acc = storage.Access();
+  for (int i = 0; i < 20; ++i) {
+    auto vertex = CreateVertex(&acc);
+    ASSERT_NO_ERROR(vertex.AddLabelAndValidate(i % 3 ? label1 : label2));
+  }
+
+  EXPECT_EQ(acc.ApproximateVertexCount(label1), 13);
+  EXPECT_EQ(acc.ApproximateVertexCount(label2), 7);
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelPropertyIndexCreateAndDrop) {
+  EXPECT_EQ(storage.ListAllIndices().label_property.size(), 0);
+  EXPECT_TRUE(storage.CreateIndex(label1, prop_id));
+  {
+    auto acc = storage.Access();
+    EXPECT_TRUE(acc.LabelPropertyIndexExists(label1, prop_id));
+  }
+  EXPECT_THAT(storage.ListAllIndices().label_property, UnorderedElementsAre(std::make_pair(label1, prop_id)));
+  {
+    auto acc = storage.Access();
+    EXPECT_FALSE(acc.LabelPropertyIndexExists(label2, prop_id));
+  }
+  EXPECT_FALSE(storage.CreateIndex(label1, prop_id));
+  EXPECT_THAT(storage.ListAllIndices().label_property, UnorderedElementsAre(std::make_pair(label1, prop_id)));
+
+  EXPECT_TRUE(storage.CreateIndex(label2, prop_id));
+  {
+    auto acc = storage.Access();
+    EXPECT_TRUE(acc.LabelPropertyIndexExists(label2, prop_id));
+  }
+  EXPECT_THAT(storage.ListAllIndices().label_property,
+              UnorderedElementsAre(std::make_pair(label1, prop_id), std::make_pair(label2, prop_id)));
+
+  EXPECT_TRUE(storage.DropIndex(label1, prop_id));
+  {
+    auto acc = storage.Access();
+    EXPECT_FALSE(acc.LabelPropertyIndexExists(label1, prop_id));
+  }
+  EXPECT_THAT(storage.ListAllIndices().label_property, UnorderedElementsAre(std::make_pair(label2, prop_id)));
+  EXPECT_FALSE(storage.DropIndex(label1, prop_id));
+
+  EXPECT_TRUE(storage.DropIndex(label2, prop_id));
+  {
+    auto acc = storage.Access();
+    EXPECT_FALSE(acc.LabelPropertyIndexExists(label2, prop_id));
+  }
+  EXPECT_EQ(storage.ListAllIndices().label_property.size(), 0);
+}
+
+// The following three tests are almost an exact copy-paste of the corresponding
+// label index tests. We request all vertices with given label and property from
+// the index, without range filtering. Range filtering is tested in a separate
+// test.
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelPropertyIndexBasic) {
+  storage.CreateIndex(label1, prop_val);
+  storage.CreateIndex(label2, prop_val);
+
+  auto acc = storage.Access();
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::OLD), View::OLD), IsEmpty());
+
+  for (int i = 0; i < 10; ++i) {
+    auto vertex = CreateVertex(&acc);
+    ASSERT_NO_ERROR(vertex.AddLabelAndValidate(i % 2 ? label1 : label2));
+    ASSERT_NO_ERROR(vertex.SetPropertyAndValidate(prop_val, PropertyValue(i)));
+  }
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::NEW), View::NEW), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::NEW), View::NEW), UnorderedElementsAre(0, 2, 4, 6, 8));
+
+  acc.AdvanceCommand();
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::OLD), View::OLD), UnorderedElementsAre(0, 2, 4, 6, 8));
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::NEW), View::NEW), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::NEW), View::NEW), UnorderedElementsAre(0, 2, 4, 6, 8));
+
+  for (auto vertex : acc.Vertices(View::OLD)) {
+    int64_t id = vertex.GetProperty(prop_id, View::OLD)->ValueInt();
+    if (id % 2) {
+      ASSERT_NO_ERROR(vertex.SetPropertyAndValidate(prop_val, PropertyValue()));
+    } else {
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(label1));
+    }
+  }
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::OLD), View::OLD), UnorderedElementsAre(0, 2, 4, 6, 8));
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::NEW), View::NEW), UnorderedElementsAre(0, 2, 4, 6, 8));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::NEW), View::NEW), UnorderedElementsAre(0, 2, 4, 6, 8));
+
+  for (auto vertex : acc.Vertices(View::OLD)) {
+    int64_t id = vertex.GetProperty(prop_id, View::OLD)->ValueInt();
+    if (id % 2 == 0) {
+      ASSERT_NO_ERROR(acc.DeleteVertex(&vertex));
+    }
+  }
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::OLD), View::OLD), UnorderedElementsAre(1, 3, 5, 7, 9));
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::OLD), View::OLD), UnorderedElementsAre(0, 2, 4, 6, 8));
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::NEW), View::NEW), IsEmpty());
+
+  acc.AdvanceCommand();
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::OLD), View::OLD), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc.Vertices(label2, prop_val, View::NEW), View::NEW), IsEmpty());
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelPropertyIndexDuplicateVersions) {
+  storage.CreateIndex(label1, prop_val);
+  {
+    auto acc = storage.Access();
+    for (int i = 0; i < 5; ++i) {
+      auto vertex = CreateVertex(&acc);
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(label1));
+      ASSERT_NO_ERROR(vertex.SetPropertyAndValidate(prop_val, PropertyValue(i)));
+    }
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::NEW), View::NEW), UnorderedElementsAre(0, 1, 2, 3, 4));
+
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+
+  {
+    auto acc = storage.Access();
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::OLD), View::OLD), UnorderedElementsAre(0, 1, 2, 3, 4));
+
+    for (auto vertex : acc.Vertices(View::OLD)) {
+      ASSERT_NO_ERROR(vertex.SetPropertyAndValidate(prop_val, PropertyValue()));
+    }
+
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::OLD), View::OLD), UnorderedElementsAre(0, 1, 2, 3, 4));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::NEW), View::NEW), IsEmpty());
+
+    for (auto vertex : acc.Vertices(View::OLD)) {
+      ASSERT_NO_ERROR(vertex.SetPropertyAndValidate(prop_val, PropertyValue(42)));
+    }
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::OLD), View::OLD), UnorderedElementsAre(0, 1, 2, 3, 4));
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::NEW), View::NEW), UnorderedElementsAre(0, 1, 2, 3, 4));
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelPropertyIndexTransactionalIsolation) {
+  storage.CreateIndex(label1, prop_val);
+
+  auto acc_before = storage.Access();
+  auto acc = storage.Access();
+  auto acc_after = storage.Access();
+
+  for (int i = 0; i < 5; ++i) {
+    auto vertex = CreateVertex(&acc);
+    ASSERT_NO_ERROR(vertex.AddLabelAndValidate(label1));
+    ASSERT_NO_ERROR(vertex.SetPropertyAndValidate(prop_val, PropertyValue(i)));
+  }
+
+  EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, View::NEW), View::NEW), UnorderedElementsAre(0, 1, 2, 3, 4));
+  EXPECT_THAT(GetIds(acc_before.Vertices(label1, prop_val, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc_after.Vertices(label1, prop_val, View::NEW), View::NEW), IsEmpty());
+
+  ASSERT_NO_ERROR(acc.Commit());
+
+  auto acc_after_commit = storage.Access();
+
+  EXPECT_THAT(GetIds(acc_before.Vertices(label1, prop_val, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc_after.Vertices(label1, prop_val, View::NEW), View::NEW), IsEmpty());
+  EXPECT_THAT(GetIds(acc_after_commit.Vertices(label1, prop_val, View::NEW), View::NEW),
+              UnorderedElementsAre(0, 1, 2, 3, 4));
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelPropertyIndexFiltering) {
+  // We insert vertices with values:
+  // 0 0.0 1 1.0 2 2.0 3 3.0 4 4.0
+  // Then we check all combinations of inclusive and exclusive bounds.
+  // We also have a mix of doubles and integers to verify that they are sorted
+  // properly.
+
+  storage.CreateIndex(label1, prop_val);
+
+  {
+    auto acc = storage.Access();
+
+    for (int i = 0; i < 10; ++i) {
+      auto vertex = CreateVertex(&acc);
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(label1));
+      ASSERT_NO_ERROR(vertex.SetPropertyAndValidate(prop_val, i % 2 ? PropertyValue(i / 2) : PropertyValue(i / 2.0)));
+    }
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+  {
+    auto acc = storage.Access();
+    for (int i = 0; i < 5; ++i) {
+      EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, PropertyValue(i), View::OLD)),
+                  UnorderedElementsAre(2 * i, 2 * i + 1));
+    }
+
+    // [1, +inf>
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, memgraph::utils::MakeBoundInclusive(PropertyValue(1)),
+                                    std::nullopt, View::OLD)),
+                UnorderedElementsAre(2, 3, 4, 5, 6, 7, 8, 9));
+    // <1, +inf>
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, memgraph::utils::MakeBoundExclusive(PropertyValue(1)),
+                                    std::nullopt, View::OLD)),
+                UnorderedElementsAre(4, 5, 6, 7, 8, 9));
+
+    // <-inf, 3]
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, std::nullopt,
+                                    memgraph::utils::MakeBoundInclusive(PropertyValue(3)), View::OLD)),
+                UnorderedElementsAre(0, 1, 2, 3, 4, 5, 6, 7));
+    // <-inf, 3>
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, std::nullopt,
+                                    memgraph::utils::MakeBoundExclusive(PropertyValue(3)), View::OLD)),
+                UnorderedElementsAre(0, 1, 2, 3, 4, 5));
+
+    // [1, 3]
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, memgraph::utils::MakeBoundInclusive(PropertyValue(1)),
+                                    memgraph::utils::MakeBoundInclusive(PropertyValue(3)), View::OLD)),
+                UnorderedElementsAre(2, 3, 4, 5, 6, 7));
+    // <1, 3]
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, memgraph::utils::MakeBoundExclusive(PropertyValue(1)),
+                                    memgraph::utils::MakeBoundInclusive(PropertyValue(3)), View::OLD)),
+                UnorderedElementsAre(4, 5, 6, 7));
+    // [1, 3>
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, memgraph::utils::MakeBoundInclusive(PropertyValue(1)),
+                                    memgraph::utils::MakeBoundExclusive(PropertyValue(3)), View::OLD)),
+                UnorderedElementsAre(2, 3, 4, 5));
+    // <1, 3>
+    EXPECT_THAT(GetIds(acc.Vertices(label1, prop_val, memgraph::utils::MakeBoundExclusive(PropertyValue(1)),
+                                    memgraph::utils::MakeBoundExclusive(PropertyValue(3)), View::OLD)),
+                UnorderedElementsAre(4, 5));
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(IndexTest, LabelPropertyIndexCountEstimate) {
+  storage.CreateIndex(label1, prop_val);
+
+  auto acc = storage.Access();
+  for (int i = 1; i <= 10; ++i) {
+    for (int j = 0; j < i; ++j) {
+      auto vertex = CreateVertex(&acc);
+      ASSERT_NO_ERROR(vertex.AddLabelAndValidate(label1));
+      ASSERT_NO_ERROR(vertex.SetPropertyAndValidate(prop_val, PropertyValue(i)));
+    }
+  }
+
+  EXPECT_EQ(acc.ApproximateVertexCount(label1, prop_val), 55);
+  for (int i = 1; i <= 10; ++i) {
+    EXPECT_EQ(acc.ApproximateVertexCount(label1, prop_val, PropertyValue(i)), i);
+  }
+
+  EXPECT_EQ(acc.ApproximateVertexCount(label1, prop_val, memgraph::utils::MakeBoundInclusive(PropertyValue(2)),
+                                       memgraph::utils::MakeBoundInclusive(PropertyValue(6))),
+            2 + 3 + 4 + 5 + 6);
+}
+
+TEST_F(IndexTest, LabelPropertyIndexMixedIteration) {
+  storage.CreateIndex(label1, prop_val);
+
+  const std::array temporals{TemporalData{TemporalType::Date, 23}, TemporalData{TemporalType::Date, 28},
+                             TemporalData{TemporalType::LocalDateTime, 20}};
+
+  std::vector<PropertyValue> values = {
+      PropertyValue(false),
+      PropertyValue(true),
+      PropertyValue(-std::numeric_limits<double>::infinity()),
+      PropertyValue(std::numeric_limits<int64_t>::min()),
+      PropertyValue(-1),
+      PropertyValue(-0.5),
+      PropertyValue(0),
+      PropertyValue(0.5),
+      PropertyValue(1),
+      PropertyValue(1.5),
+      PropertyValue(2),
+      PropertyValue(std::numeric_limits<int64_t>::max()),
+      PropertyValue(std::numeric_limits<double>::infinity()),
+      PropertyValue(""),
+      PropertyValue("a"),
+      PropertyValue("b"),
+      PropertyValue("c"),
+      PropertyValue(std::vector<PropertyValue>()),
+      PropertyValue(std::vector<PropertyValue>{PropertyValue(0.8)}),
+      PropertyValue(std::vector<PropertyValue>{PropertyValue(2)}),
+      PropertyValue(std::map<std::string, PropertyValue>()),
+      PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(5)}}),
+      PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(10)}}),
+      PropertyValue(temporals[0]),
+      PropertyValue(temporals[1]),
+      PropertyValue(temporals[2]),
+  };
+
+  // Create vertices, each with one of the values above.
+  {
+    auto acc = storage.Access();
+    for (const auto &value : values) {
+      auto v = acc.CreateVertexAndValidate(primary_label, {}, {{primary_property, PropertyValue(primary_key_id++)}});
+      ASSERT_TRUE(v->AddLabelAndValidate(label1).HasValue());
+      ASSERT_TRUE(v->SetPropertyAndValidate(prop_val, value).HasValue());
+    }
+    ASSERT_FALSE(acc.Commit().HasError());
+  }
+
+  // Verify that all nodes are in the index.
+  {
+    auto acc = storage.Access();
+    auto iterable = acc.Vertices(label1, prop_val, View::OLD);
+    auto it = iterable.begin();
+    for (const auto &value : values) {
+      ASSERT_NE(it, iterable.end());
+      auto vertex = *it;
+      auto maybe_value = vertex.GetProperty(prop_val, View::OLD);
+      ASSERT_TRUE(maybe_value.HasValue());
+      ASSERT_EQ(value, *maybe_value);
+      ++it;
+    }
+    ASSERT_EQ(it, iterable.end());
+  }
+
+  auto verify = [&](const std::optional<memgraph::utils::Bound<PropertyValue>> &from,
+                    const std::optional<memgraph::utils::Bound<PropertyValue>> &to,
+                    const std::vector<PropertyValue> &expected) {
+    auto acc = storage.Access();
+    auto iterable = acc.Vertices(label1, prop_val, from, to, View::OLD);
+    size_t i = 0;
+    for (auto it = iterable.begin(); it != iterable.end(); ++it, ++i) {
+      auto vertex = *it;
+      auto maybe_value = vertex.GetProperty(prop_val, View::OLD);
+      ASSERT_TRUE(maybe_value.HasValue());
+      ASSERT_EQ(*maybe_value, expected[i]);
+    }
+    ASSERT_EQ(i, expected.size());
+  };
+
+  // Range iteration with two specified bounds that have the same type should
+  // yield the naturally expected items.
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue(false)),
+         memgraph::utils::MakeBoundExclusive(PropertyValue(true)), {});
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue(false)),
+         memgraph::utils::MakeBoundInclusive(PropertyValue(true)), {PropertyValue(true)});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(false)),
+         memgraph::utils::MakeBoundExclusive(PropertyValue(true)), {PropertyValue(false)});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(false)),
+         memgraph::utils::MakeBoundInclusive(PropertyValue(true)), {PropertyValue(false), PropertyValue(true)});
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue(0)), memgraph::utils::MakeBoundExclusive(PropertyValue(1.8)),
+         {PropertyValue(0.5), PropertyValue(1), PropertyValue(1.5)});
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue(0)), memgraph::utils::MakeBoundInclusive(PropertyValue(1.8)),
+         {PropertyValue(0.5), PropertyValue(1), PropertyValue(1.5)});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(0)), memgraph::utils::MakeBoundExclusive(PropertyValue(1.8)),
+         {PropertyValue(0), PropertyValue(0.5), PropertyValue(1), PropertyValue(1.5)});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(0)), memgraph::utils::MakeBoundInclusive(PropertyValue(1.8)),
+         {PropertyValue(0), PropertyValue(0.5), PropertyValue(1), PropertyValue(1.5)});
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue("b")),
+         memgraph::utils::MakeBoundExclusive(PropertyValue("memgraph")), {PropertyValue("c")});
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue("b")),
+         memgraph::utils::MakeBoundInclusive(PropertyValue("memgraph")), {PropertyValue("c")});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue("b")),
+         memgraph::utils::MakeBoundExclusive(PropertyValue("memgraph")), {PropertyValue("b"), PropertyValue("c")});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue("b")),
+         memgraph::utils::MakeBoundInclusive(PropertyValue("memgraph")), {PropertyValue("b"), PropertyValue("c")});
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue(0.8)})),
+         memgraph::utils::MakeBoundExclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue("b")})),
+         {PropertyValue(std::vector<PropertyValue>{PropertyValue(2)})});
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue(0.8)})),
+         memgraph::utils::MakeBoundInclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue("b")})),
+         {PropertyValue(std::vector<PropertyValue>{PropertyValue(2)})});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue(0.8)})),
+         memgraph::utils::MakeBoundExclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue("b")})),
+         {PropertyValue(std::vector<PropertyValue>{PropertyValue(0.8)}),
+          PropertyValue(std::vector<PropertyValue>{PropertyValue(2)})});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue(0.8)})),
+         memgraph::utils::MakeBoundInclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue("b")})),
+         {PropertyValue(std::vector<PropertyValue>{PropertyValue(0.8)}),
+          PropertyValue(std::vector<PropertyValue>{PropertyValue(2)})});
+  verify(memgraph::utils::MakeBoundExclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(5.0)}})),
+         memgraph::utils::MakeBoundExclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue("b")}})),
+         {PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(10)}})});
+  verify(memgraph::utils::MakeBoundExclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(5.0)}})),
+         memgraph::utils::MakeBoundInclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue("b")}})),
+         {PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(10)}})});
+  verify(memgraph::utils::MakeBoundInclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(5.0)}})),
+         memgraph::utils::MakeBoundExclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue("b")}})),
+         {PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(5)}}),
+          PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(10)}})});
+  verify(memgraph::utils::MakeBoundInclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(5.0)}})),
+         memgraph::utils::MakeBoundInclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue("b")}})),
+         {PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(5)}}),
+          PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(10)}})});
+
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue(temporals[0])),
+         memgraph::utils::MakeBoundInclusive(PropertyValue(TemporalData{TemporalType::Date, 200})),
+         // LocalDateTime has a "higher" type number so it is not part of the range
+         {PropertyValue(temporals[1])});
+  verify(memgraph::utils::MakeBoundExclusive(PropertyValue(temporals[0])),
+         memgraph::utils::MakeBoundInclusive(PropertyValue(temporals[2])),
+         {PropertyValue(temporals[1]), PropertyValue(temporals[2])});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(temporals[0])),
+         memgraph::utils::MakeBoundExclusive(PropertyValue(temporals[2])),
+         {PropertyValue(temporals[0]), PropertyValue(temporals[1])});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(temporals[0])),
+         memgraph::utils::MakeBoundInclusive(PropertyValue(temporals[2])),
+         {PropertyValue(temporals[0]), PropertyValue(temporals[1]), PropertyValue(temporals[2])});
+
+  // Range iteration with one unspecified bound should only yield items that
+  // have the same type as the specified bound.
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(false)), std::nullopt,
+         {PropertyValue(false), PropertyValue(true)});
+  verify(std::nullopt, memgraph::utils::MakeBoundExclusive(PropertyValue(true)), {PropertyValue(false)});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(1)), std::nullopt,
+         {PropertyValue(1), PropertyValue(1.5), PropertyValue(2), PropertyValue(std::numeric_limits<int64_t>::max()),
+          PropertyValue(std::numeric_limits<double>::infinity())});
+  verify(std::nullopt, memgraph::utils::MakeBoundExclusive(PropertyValue(0)),
+         {PropertyValue(-std::numeric_limits<double>::infinity()), PropertyValue(std::numeric_limits<int64_t>::min()),
+          PropertyValue(-1), PropertyValue(-0.5)});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue("b")), std::nullopt,
+         {PropertyValue("b"), PropertyValue("c")});
+  verify(std::nullopt, memgraph::utils::MakeBoundExclusive(PropertyValue("b")),
+         {PropertyValue(""), PropertyValue("a")});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue(false)})),
+         std::nullopt,
+         {PropertyValue(std::vector<PropertyValue>{PropertyValue(0.8)}),
+          PropertyValue(std::vector<PropertyValue>{PropertyValue(2)})});
+  verify(std::nullopt, memgraph::utils::MakeBoundExclusive(PropertyValue(std::vector<PropertyValue>{PropertyValue(1)})),
+         {PropertyValue(std::vector<PropertyValue>()), PropertyValue(std::vector<PropertyValue>{PropertyValue(0.8)})});
+  verify(memgraph::utils::MakeBoundInclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(false)}})),
+         std::nullopt,
+         {PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(5)}}),
+          PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(10)}})});
+  verify(std::nullopt,
+         memgraph::utils::MakeBoundExclusive(
+             PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(7.5)}})),
+         {PropertyValue(std::map<std::string, PropertyValue>()),
+          PropertyValue(std::map<std::string, PropertyValue>{{"id", PropertyValue(5)}})});
+  verify(memgraph::utils::MakeBoundInclusive(PropertyValue(TemporalData(TemporalType::Date, 10))), std::nullopt,
+         {PropertyValue(temporals[0]), PropertyValue(temporals[1]), PropertyValue(temporals[2])});
+  verify(std::nullopt, memgraph::utils::MakeBoundExclusive(PropertyValue(TemporalData(TemporalType::Duration, 0))),
+         {PropertyValue(temporals[0]), PropertyValue(temporals[1]), PropertyValue(temporals[2])});
+
+  // Range iteration with two specified bounds that don't have the same type
+  // should yield no items.
+  for (size_t i = 0; i < values.size(); ++i) {
+    for (size_t j = i; j < values.size(); ++j) {
+      if (PropertyValue::AreComparableTypes(values[i].type(), values[j].type())) {
+        verify(memgraph::utils::MakeBoundInclusive(values[i]), memgraph::utils::MakeBoundInclusive(values[j]),
+               {values.begin() + i, values.begin() + j + 1});
+      } else {
+        verify(memgraph::utils::MakeBoundInclusive(values[i]), memgraph::utils::MakeBoundInclusive(values[j]), {});
+      }
+    }
+  }
+
+  // Iteration without any bounds should return all items of the index.
+  verify(std::nullopt, std::nullopt, values);
+}
+
+TEST_F(IndexTest, LabelPropertyIndexCreateWithExistingPrimaryKey) {
+  // Create index on primary label and on primary key
+  EXPECT_EQ(storage.ListAllIndices().label_property.size(), 0);
+  EXPECT_FALSE(storage.CreateIndex(primary_label, primary_property));
+  EXPECT_EQ(storage.ListAllIndices().label_property.size(), 0);
+  EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+
+  // Create index on primary label and on secondary property
+  EXPECT_TRUE(storage.CreateIndex(primary_label, prop_id));
+  EXPECT_EQ(storage.ListAllIndices().label_property.size(), 1);
+  EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+  {
+    auto acc = storage.Access();
+    EXPECT_TRUE(acc.LabelPropertyIndexExists(primary_label, prop_id));
+  }
+  EXPECT_THAT(storage.ListAllIndices().label_property, UnorderedElementsAre(Pair(primary_label, prop_id)));
+
+  // Create index on primary label
+  EXPECT_FALSE(storage.CreateIndex(primary_label));
+  EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+  EXPECT_EQ(storage.ListAllIndices().label_property.size(), 1);
+
+  // Create index on secondary label
+  EXPECT_TRUE(storage.CreateIndex(label1));
+  EXPECT_EQ(storage.ListAllIndices().label.size(), 1);
+  EXPECT_EQ(storage.ListAllIndices().label_property.size(), 1);
+}
+
+TEST_F(IndexTest, LabelIndexCreateVertexAndValidate) {
+  {
+    auto acc = storage.Access();
+    EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+    EXPECT_EQ(storage.ListAllIndices().label_property.size(), 0);
+  }
+  {
+    auto acc = storage.Access();
+
+    // Create vertices with CreateVertexAndValidate
+    for (int i = 0; i < 5; ++i) {
+      auto vertex =
+          acc.CreateVertexAndValidate(primary_label, {label1}, {{primary_property, PropertyValue(primary_key_id++)}});
+      ASSERT_TRUE(vertex.HasValue());
+    }
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+  {
+    EXPECT_TRUE(storage.CreateIndex(label1));
+    {
+      auto acc = storage.Access();
+      EXPECT_THAT(GetPrimaryKeyIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(0, 1, 2, 3, 4));
+    }
+  }
+  {
+    EXPECT_TRUE(storage.DropIndex(label1));
+    {
+      auto acc = storage.Access();
+      EXPECT_FALSE(acc.LabelIndexExists(label1));
+    }
+    EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+  }
+  {
+    auto acc = storage.Access();
+    EXPECT_TRUE(storage.CreateIndex(label1));
+    EXPECT_THAT(GetPrimaryKeyIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(0, 1, 2, 3, 4));
+
+    for (int i = 0; i < 5; ++i) {
+      auto vertex =
+          acc.CreateVertexAndValidate(primary_label, {label1}, {{primary_property, PropertyValue(primary_key_id++)}});
+      ASSERT_TRUE(vertex.HasValue());
+    }
+
+    EXPECT_THAT(GetPrimaryKeyIds(acc.Vertices(label1, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
+    EXPECT_THAT(GetPrimaryKeyIds(acc.Vertices(label1, View::OLD), View::OLD), UnorderedElementsAre(0, 1, 2, 3, 4));
+  }
+}
+
+TEST_F(IndexTest, LabelPropertyIndexCreateVertexAndValidate) {
+  {
+    auto acc = storage.Access();
+    EXPECT_EQ(storage.ListAllIndices().label.size(), 0);
+    EXPECT_EQ(storage.ListAllIndices().label_property.size(), 0);
+  }
+  {
+    auto acc = storage.Access();
+
+    // Create vertices with CreateVertexAndValidate
+    for (int i = 0; i < 5; ++i) {
+      auto vertex = acc.CreateVertexAndValidate(
+          primary_label, {label1},
+          {{primary_property, PropertyValue(primary_key_id++)}, {prop_id, PropertyValue(vertex_id++)}});
+      ASSERT_TRUE(vertex.HasValue());
+    }
+    ASSERT_NO_ERROR(acc.Commit());
+  }
+  {
+    EXPECT_TRUE(storage.CreateIndex(label1, prop_id));
+    {
+      auto acc = storage.Access();
+      EXPECT_THAT(GetPrimaryKeyIds(acc.Vertices(label1, prop_id, View::OLD), View::OLD),
+                  UnorderedElementsAre(0, 1, 2, 3, 4));
+    }
+  }
+  {
+    EXPECT_TRUE(storage.DropIndex(label1, prop_id));
+    {
+      auto acc = storage.Access();
+      EXPECT_FALSE(acc.LabelPropertyIndexExists(label1, prop_id));
+    }
+    EXPECT_EQ(storage.ListAllIndices().label_property.size(), 0);
+  }
+  {
+    auto acc = storage.Access();
+    EXPECT_TRUE(storage.CreateIndex(label1, prop_id));
+    EXPECT_THAT(GetPrimaryKeyIds(acc.Vertices(label1, prop_id, View::OLD), View::OLD),
+                UnorderedElementsAre(0, 1, 2, 3, 4));
+
+    for (int i = 0; i < 5; ++i) {
+      auto vertex = acc.CreateVertexAndValidate(
+          primary_label, {label1},
+          {{primary_property, PropertyValue(primary_key_id++)}, {prop_id, PropertyValue(vertex_id++)}});
+      ASSERT_TRUE(vertex.HasValue());
+    }
+
+    EXPECT_THAT(GetPrimaryKeyIds(acc.Vertices(label1, prop_id, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
+    EXPECT_THAT(GetPrimaryKeyIds(acc.Vertices(label1, prop_id, View::OLD), View::OLD),
+                UnorderedElementsAre(0, 1, 2, 3, 4));
+  }
+}
+}  // namespace memgraph::storage::v3::tests