From 619b01f3f81072ef6621bbae6e1192ca76b91fea Mon Sep 17 00:00:00 2001
From: gvolfing <107616712+gvolfing@users.noreply.github.com>
Date: Fri, 8 Mar 2024 08:44:48 +0100
Subject: [PATCH] Implement edge type indices (#1542)

 Implement edge type indices (#1542 )
---
 src/communication/result_stream_faker.hpp     |   2 +-
 src/dbms/database_handler.hpp                 |   2 +-
 src/dbms/inmemory/replication_handlers.cpp    |  14 +
 src/glue/communication.hpp                    |   2 +-
 src/query/db_accessor.hpp                     |  70 +++
 src/query/dump.cpp                            |  35 +-
 src/query/dump.hpp                            |   1 +
 src/query/frontend/ast/ast.cpp                |   3 +
 src/query/frontend/ast/ast.hpp                |  28 +
 src/query/frontend/ast/ast_visitor.hpp        |  13 +-
 .../frontend/ast/cypher_main_visitor.cpp      |  21 +
 .../frontend/ast/cypher_main_visitor.hpp      |  15 +
 .../opencypher/grammar/MemgraphCypher.g4      |   7 +
 .../frontend/semantic/required_privileges.cpp |   2 +
 src/query/frontend/semantic/symbol.hpp        |   2 +
 src/query/interpreter.cpp                     |  82 ++-
 src/query/plan/hint_provider.hpp              |   3 +
 src/query/plan/operator.cpp                   |  82 +++
 src/query/plan/operator.hpp                   |  45 +-
 src/query/plan/operator_type_info.cpp         |   2 +
 src/query/plan/planner.hpp                    |   8 +-
 src/query/plan/pretty_print.cpp               |  20 +
 src/query/plan/pretty_print.hpp               |   2 +
 .../plan/rewrite/edge_type_index_lookup.hpp   | 534 ++++++++++++++++++
 src/query/plan/vertex_count_cache.hpp         |   2 +
 src/query/procedure/module.hpp                |   2 +-
 src/query/procedure/py_module.hpp             |   2 +-
 src/storage/v2/CMakeLists.txt                 |   3 +
 src/storage/v2/disk/edge_type_index.cpp       |  49 ++
 src/storage/v2/disk/edge_type_index.hpp       |  35 ++
 src/storage/v2/disk/storage.cpp               |  33 ++
 src/storage/v2/disk/storage.hpp               |  10 +
 src/storage/v2/durability/durability.cpp      |  14 +-
 src/storage/v2/durability/marker.hpp          |   9 +-
 src/storage/v2/durability/metadata.hpp        |   3 +-
 src/storage/v2/durability/serialization.cpp   |   8 +-
 src/storage/v2/durability/snapshot.cpp        | 410 +++++++++++++-
 src/storage/v2/durability/snapshot.hpp        |   3 +-
 .../durability/storage_global_operation.hpp   |   4 +-
 src/storage/v2/durability/version.hpp         |   4 +-
 src/storage/v2/durability/wal.cpp             |  72 +++
 src/storage/v2/durability/wal.hpp             |  17 +-
 src/storage/v2/edges_iterable.cpp             | 149 +++++
 src/storage/v2/edges_iterable.hpp             |  73 +++
 src/storage/v2/indices/edge_type_index.hpp    |  46 ++
 src/storage/v2/indices/indices.cpp            |  11 +
 src/storage/v2/indices/indices.hpp            |   5 +
 src/storage/v2/inmemory/edge_type_index.cpp   | 318 +++++++++++
 src/storage/v2/inmemory/edge_type_index.hpp   | 113 ++++
 src/storage/v2/inmemory/storage.cpp           |  60 +-
 src/storage/v2/inmemory/storage.hpp           |  34 +-
 src/storage/v2/metadata_delta.hpp             |  17 +-
 .../v2/replication/replication_client.cpp     |   6 +
 .../v2/replication/replication_client.hpp     |   3 +
 .../replication/replication_storage_state.cpp |  10 +
 .../replication/replication_storage_state.hpp |   2 +
 src/storage/v2/storage.hpp                    |  12 +
 src/storage/v2/vertices_iterable.cpp          |   3 +-
 src/storage/v2/vertices_iterable.hpp          |   2 +-
 src/utils/atomic_memory_block.hpp             |   2 +-
 src/utils/event_counter.cpp                   |   1 +
 src/utils/settings.cpp                        |   2 +-
 src/utils/typeinfo.hpp                        |   2 +
 .../tests/v17/test_all/create_dataset.cypher  |  22 +
 .../v17/test_all/expected_snapshot.cypher     |  19 +
 .../tests/v17/test_all/expected_wal.cypher    |  19 +
 .../tests/v17/test_all/snapshot.bin           | Bin 0 -> 2067 bytes
 .../durability/tests/v17/test_all/wal.bin     | Bin 0 -> 3582 bytes
 .../test_constraints/create_dataset.cypher    |   6 +
 .../test_constraints/expected_snapshot.cypher |   6 +
 .../v17/test_constraints/expected_wal.cypher  |   6 +
 .../tests/v17/test_constraints/snapshot.bin   | Bin 0 -> 625 bytes
 .../tests/v17/test_constraints/wal.bin        | Bin 0 -> 460 bytes
 .../v17/test_edges/create_dataset.cypher      |  60 ++
 .../v17/test_edges/expected_snapshot.cypher   |  58 ++
 .../tests/v17/test_edges/expected_wal.cypher  |  58 ++
 .../tests/v17/test_edges/snapshot.bin         | Bin 0 -> 4297 bytes
 .../durability/tests/v17/test_edges/wal.bin   | Bin 0 -> 6616 bytes
 .../v17/test_indices/create_dataset.cypher    |   6 +
 .../v17/test_indices/expected_snapshot.cypher |   5 +
 .../v17/test_indices/expected_wal.cypher      |   5 +
 .../tests/v17/test_indices/snapshot.bin       | Bin 0 -> 731 bytes
 .../durability/tests/v17/test_indices/wal.bin | Bin 0 -> 847 bytes
 .../v17/test_vertices/create_dataset.cypher   |  18 +
 .../test_vertices/expected_snapshot.cypher    |  16 +
 .../v17/test_vertices/expected_wal.cypher     |  16 +
 .../tests/v17/test_vertices/snapshot.bin      | Bin 0 -> 1739 bytes
 .../tests/v17/test_vertices/wal.bin           | Bin 0 -> 4355 bytes
 tests/manual/interactive_planning.cpp         |   2 +
 tests/unit/dbms_database.cpp                  |   2 +-
 tests/unit/query_plan.cpp                     |  59 +-
 tests/unit/query_plan_checker.hpp             |  19 +-
 tests/unit/storage_v2_decoder_encoder.cpp     |   5 +-
 tests/unit/storage_v2_durability_inmemory.cpp |  75 ++-
 tests/unit/storage_v2_indices.cpp             | 385 ++++++++++++-
 tests/unit/storage_v2_wal_file.cpp            |  39 ++
 96 files changed, 3390 insertions(+), 62 deletions(-)
 create mode 100644 src/query/plan/rewrite/edge_type_index_lookup.hpp
 create mode 100644 src/storage/v2/disk/edge_type_index.cpp
 create mode 100644 src/storage/v2/disk/edge_type_index.hpp
 create mode 100644 src/storage/v2/edges_iterable.cpp
 create mode 100644 src/storage/v2/edges_iterable.hpp
 create mode 100644 src/storage/v2/indices/edge_type_index.hpp
 create mode 100644 src/storage/v2/inmemory/edge_type_index.cpp
 create mode 100644 src/storage/v2/inmemory/edge_type_index.hpp
 create mode 100644 tests/integration/durability/tests/v17/test_all/create_dataset.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_all/expected_snapshot.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_all/expected_wal.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_all/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v17/test_all/wal.bin
 create mode 100644 tests/integration/durability/tests/v17/test_constraints/create_dataset.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_constraints/expected_snapshot.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_constraints/expected_wal.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_constraints/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v17/test_constraints/wal.bin
 create mode 100644 tests/integration/durability/tests/v17/test_edges/create_dataset.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_edges/expected_snapshot.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_edges/expected_wal.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_edges/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v17/test_edges/wal.bin
 create mode 100644 tests/integration/durability/tests/v17/test_indices/create_dataset.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_indices/expected_snapshot.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_indices/expected_wal.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_indices/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v17/test_indices/wal.bin
 create mode 100644 tests/integration/durability/tests/v17/test_vertices/create_dataset.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_vertices/expected_snapshot.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_vertices/expected_wal.cypher
 create mode 100644 tests/integration/durability/tests/v17/test_vertices/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v17/test_vertices/wal.bin

diff --git a/src/communication/result_stream_faker.hpp b/src/communication/result_stream_faker.hpp
index 779d039cc..c0a40cecf 100644
--- a/src/communication/result_stream_faker.hpp
+++ b/src/communication/result_stream_faker.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
diff --git a/src/dbms/database_handler.hpp b/src/dbms/database_handler.hpp
index de5f813ba..cae54088e 100644
--- a/src/dbms/database_handler.hpp
+++ b/src/dbms/database_handler.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
diff --git a/src/dbms/inmemory/replication_handlers.cpp b/src/dbms/inmemory/replication_handlers.cpp
index 6a78977bb..3e4a31884 100644
--- a/src/dbms/inmemory/replication_handlers.cpp
+++ b/src/dbms/inmemory/replication_handlers.cpp
@@ -840,6 +840,20 @@ uint64_t InMemoryReplicationHandlers::ReadAndApplyDelta(storage::InMemoryStorage
         transaction->DeleteLabelPropertyIndexStats(storage->NameToLabel(info.label));
         break;
       }
+      case WalDeltaData::Type::EDGE_INDEX_CREATE: {
+        spdlog::trace("       Create edge index on :{}", delta.operation_edge_type.edge_type);
+        auto *transaction = get_transaction(timestamp, kUniqueAccess);
+        if (transaction->CreateIndex(storage->NameToEdgeType(delta.operation_label.label)).HasError())
+          throw utils::BasicException("Invalid transaction! Please raise an issue, {}:{}", __FILE__, __LINE__);
+        break;
+      }
+      case WalDeltaData::Type::EDGE_INDEX_DROP: {
+        spdlog::trace("       Drop edge index on :{}", delta.operation_edge_type.edge_type);
+        auto *transaction = get_transaction(timestamp, kUniqueAccess);
+        if (transaction->DropIndex(storage->NameToEdgeType(delta.operation_label.label)).HasError())
+          throw utils::BasicException("Invalid transaction! Please raise an issue, {}:{}", __FILE__, __LINE__);
+        break;
+      }
       case WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE: {
         spdlog::trace("       Create existence constraint on :{} ({})", delta.operation_label_property.label,
                       delta.operation_label_property.property);
diff --git a/src/glue/communication.hpp b/src/glue/communication.hpp
index 737f32db2..a448b05fc 100644
--- a/src/glue/communication.hpp
+++ b/src/glue/communication.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
diff --git a/src/query/db_accessor.hpp b/src/query/db_accessor.hpp
index e10102ee5..915ea9936 100644
--- a/src/query/db_accessor.hpp
+++ b/src/query/db_accessor.hpp
@@ -371,6 +371,62 @@ class VerticesIterable final {
   }
 };
 
+class EdgesIterable final {
+  std::variant<storage::EdgesIterable, std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                                          utils::Allocator<EdgeAccessor>> *>
+      iterable_;
+
+ public:
+  class Iterator final {
+    std::variant<storage::EdgesIterable::Iterator,
+                 std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                    utils::Allocator<EdgeAccessor>>::iterator>
+        it_;
+
+   public:
+    explicit Iterator(storage::EdgesIterable::Iterator it) : it_(std::move(it)) {}
+    explicit Iterator(std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                         utils::Allocator<EdgeAccessor>>::iterator it)
+        : it_(it) {}
+
+    EdgeAccessor operator*() const {
+      return std::visit([](auto &it_) { return EdgeAccessor(*it_); }, it_);
+    }
+
+    Iterator &operator++() {
+      std::visit([](auto &it_) { ++it_; }, it_);
+      return *this;
+    }
+
+    bool operator==(const Iterator &other) const { return it_ == other.it_; }
+
+    bool operator!=(const Iterator &other) const { return !(other == *this); }
+  };
+
+  explicit EdgesIterable(storage::EdgesIterable iterable) : iterable_(std::move(iterable)) {}
+  explicit EdgesIterable(std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                            utils::Allocator<EdgeAccessor>> *edges)
+      : iterable_(edges) {}
+
+  Iterator begin() {
+    return std::visit(
+        memgraph::utils::Overloaded{
+            [](storage::EdgesIterable &iterable_) { return Iterator(iterable_.begin()); },
+            [](std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                  utils::Allocator<EdgeAccessor>> *iterable_) { return Iterator(iterable_->begin()); }},
+        iterable_);
+  }
+
+  Iterator end() {
+    return std::visit(
+        memgraph::utils::Overloaded{
+            [](storage::EdgesIterable &iterable_) { return Iterator(iterable_.end()); },
+            [](std::unordered_set<EdgeAccessor, std::hash<EdgeAccessor>, std::equal_to<void>,
+                                  utils::Allocator<EdgeAccessor>> *iterable_) { return Iterator(iterable_->end()); }},
+        iterable_);
+  }
+};
+
 class DbAccessor final {
   storage::Storage::Accessor *accessor_;
 
@@ -416,6 +472,10 @@ class DbAccessor final {
     return VerticesIterable(accessor_->Vertices(label, property, lower, upper, view));
   }
 
+  EdgesIterable Edges(storage::View view, storage::EdgeTypeId edge_type) {
+    return EdgesIterable(accessor_->Edges(edge_type, view));
+  }
+
   VertexAccessor InsertVertex() { return VertexAccessor(accessor_->CreateVertex()); }
 
   storage::Result<EdgeAccessor> InsertEdge(VertexAccessor *from, VertexAccessor *to,
@@ -572,6 +632,8 @@ class DbAccessor final {
     return accessor_->LabelPropertyIndexExists(label, prop);
   }
 
+  bool EdgeTypeIndexExists(storage::EdgeTypeId edge_type) const { return accessor_->EdgeTypeIndexExists(edge_type); }
+
   std::optional<storage::LabelIndexStats> GetIndexStats(const storage::LabelId &label) const {
     return accessor_->GetIndexStats(label);
   }
@@ -638,6 +700,10 @@ class DbAccessor final {
     return accessor_->CreateIndex(label, property);
   }
 
+  utils::BasicResult<storage::StorageIndexDefinitionError, void> CreateIndex(storage::EdgeTypeId edge_type) {
+    return accessor_->CreateIndex(edge_type);
+  }
+
   utils::BasicResult<storage::StorageIndexDefinitionError, void> DropIndex(storage::LabelId label) {
     return accessor_->DropIndex(label);
   }
@@ -647,6 +713,10 @@ class DbAccessor final {
     return accessor_->DropIndex(label, property);
   }
 
+  utils::BasicResult<storage::StorageIndexDefinitionError, void> DropIndex(storage::EdgeTypeId edge_type) {
+    return accessor_->DropIndex(edge_type);
+  }
+
   utils::BasicResult<storage::StorageExistenceConstraintDefinitionError, void> CreateExistenceConstraint(
       storage::LabelId label, storage::PropertyId property) {
     return accessor_->CreateExistenceConstraint(label, property);
diff --git a/src/query/dump.cpp b/src/query/dump.cpp
index 2925023fb..f1dd08c8d 100644
--- a/src/query/dump.cpp
+++ b/src/query/dump.cpp
@@ -242,6 +242,10 @@ void DumpLabelIndex(std::ostream *os, query::DbAccessor *dba, const storage::Lab
   *os << "CREATE INDEX ON :" << EscapeName(dba->LabelToName(label)) << ";";
 }
 
+void DumpEdgeTypeIndex(std::ostream *os, query::DbAccessor *dba, const storage::EdgeTypeId edge_type) {
+  *os << "CREATE EDGE INDEX ON :" << EscapeName(dba->EdgeTypeToName(edge_type)) << ";";
+}
+
 void DumpLabelPropertyIndex(std::ostream *os, query::DbAccessor *dba, storage::LabelId label,
                             storage::PropertyId property) {
   *os << "CREATE INDEX ON :" << EscapeName(dba->LabelToName(label)) << "(" << EscapeName(dba->PropertyToName(property))
@@ -297,7 +301,9 @@ PullPlanDump::PullPlanDump(DbAccessor *dba, dbms::DatabaseAccess db_acc)
                    // Internal index cleanup
                    CreateInternalIndexCleanupPullChunk(),
                    // Dump all triggers
-                   CreateTriggersPullChunk()} {}
+                   CreateTriggersPullChunk(),
+                   // Dump all edge-type indices
+                   CreateEdgeTypeIndicesPullChunk()} {}
 
 bool PullPlanDump::Pull(AnyStream *stream, std::optional<int> n) {
   // Iterate all functions that stream some results.
@@ -352,6 +358,33 @@ PullPlanDump::PullChunk PullPlanDump::CreateLabelIndicesPullChunk() {
   };
 }
 
+PullPlanDump::PullChunk PullPlanDump::CreateEdgeTypeIndicesPullChunk() {
+  // Dump all label indices
+  return [this, global_index = 0U](AnyStream *stream, std::optional<int> n) mutable -> std::optional<size_t> {
+    // Delay the construction of indices vectors
+    if (!indices_info_) {
+      indices_info_.emplace(dba_->ListAllIndices());
+    }
+    const auto &edge_type = indices_info_->edge_type;
+
+    size_t local_counter = 0;
+    while (global_index < edge_type.size() && (!n || local_counter < *n)) {
+      std::ostringstream os;
+      DumpEdgeTypeIndex(&os, dba_, edge_type[global_index]);
+      stream->Result({TypedValue(os.str())});
+
+      ++global_index;
+      ++local_counter;
+    }
+
+    if (global_index == edge_type.size()) {
+      return local_counter;
+    }
+
+    return std::nullopt;
+  };
+}
+
 PullPlanDump::PullChunk PullPlanDump::CreateLabelPropertyIndicesPullChunk() {
   return [this, global_index = 0U](AnyStream *stream, std::optional<int> n) mutable -> std::optional<size_t> {
     // Delay the construction of indices vectors
diff --git a/src/query/dump.hpp b/src/query/dump.hpp
index a9d68d45c..05bd42967 100644
--- a/src/query/dump.hpp
+++ b/src/query/dump.hpp
@@ -63,5 +63,6 @@ struct PullPlanDump {
   PullChunk CreateDropInternalIndexPullChunk();
   PullChunk CreateInternalIndexCleanupPullChunk();
   PullChunk CreateTriggersPullChunk();
+  PullChunk CreateEdgeTypeIndicesPullChunk();
 };
 }  // namespace memgraph::query
diff --git a/src/query/frontend/ast/ast.cpp b/src/query/frontend/ast/ast.cpp
index 57d5398ab..7da5c09a0 100644
--- a/src/query/frontend/ast/ast.cpp
+++ b/src/query/frontend/ast/ast.cpp
@@ -186,6 +186,9 @@ constexpr utils::TypeInfo query::ProfileQuery::kType{utils::TypeId::AST_PROFILE_
 
 constexpr utils::TypeInfo query::IndexQuery::kType{utils::TypeId::AST_INDEX_QUERY, "IndexQuery", &query::Query::kType};
 
+constexpr utils::TypeInfo query::EdgeIndexQuery::kType{utils::TypeId::AST_EDGE_INDEX_QUERY, "EdgeIndexQuery",
+                                                       &query::Query::kType};
+
 constexpr utils::TypeInfo query::Create::kType{utils::TypeId::AST_CREATE, "Create", &query::Clause::kType};
 
 constexpr utils::TypeInfo query::CallProcedure::kType{utils::TypeId::AST_CALL_PROCEDURE, "CallProcedure",
diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp
index ed354f6ca..b8d8c9e1a 100644
--- a/src/query/frontend/ast/ast.hpp
+++ b/src/query/frontend/ast/ast.hpp
@@ -2224,6 +2224,34 @@ class IndexQuery : public memgraph::query::Query {
   friend class AstStorage;
 };
 
+class EdgeIndexQuery : public memgraph::query::Query {
+ public:
+  static const utils::TypeInfo kType;
+  const utils::TypeInfo &GetTypeInfo() const override { return kType; }
+
+  enum class Action { CREATE, DROP };
+
+  EdgeIndexQuery() = default;
+
+  DEFVISITABLE(QueryVisitor<void>);
+
+  memgraph::query::EdgeIndexQuery::Action action_;
+  memgraph::query::EdgeTypeIx edge_type_;
+
+  EdgeIndexQuery *Clone(AstStorage *storage) const override {
+    EdgeIndexQuery *object = storage->Create<EdgeIndexQuery>();
+    object->action_ = action_;
+    object->edge_type_ = storage->GetEdgeTypeIx(edge_type_.name);
+    return object;
+  }
+
+ protected:
+  EdgeIndexQuery(Action action, EdgeTypeIx edge_type) : action_(action), edge_type_(edge_type) {}
+
+ private:
+  friend class AstStorage;
+};
+
 class Create : public memgraph::query::Clause {
  public:
   static const utils::TypeInfo kType;
diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp
index 5d463d3ee..bf11878da 100644
--- a/src/query/frontend/ast/ast_visitor.hpp
+++ b/src/query/frontend/ast/ast_visitor.hpp
@@ -82,6 +82,7 @@ class AuthQuery;
 class ExplainQuery;
 class ProfileQuery;
 class IndexQuery;
+class EdgeIndexQuery;
 class DatabaseInfoQuery;
 class SystemInfoQuery;
 class ConstraintQuery;
@@ -143,11 +144,11 @@ class ExpressionVisitor
 
 template <class TResult>
 class QueryVisitor
-    : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery, DatabaseInfoQuery,
-                            SystemInfoQuery, ConstraintQuery, DumpQuery, ReplicationQuery, LockPathQuery,
-                            FreeMemoryQuery, TriggerQuery, IsolationLevelQuery, CreateSnapshotQuery, StreamQuery,
-                            SettingQuery, VersionQuery, ShowConfigQuery, TransactionQueueQuery, StorageModeQuery,
-                            AnalyzeGraphQuery, MultiDatabaseQuery, ShowDatabasesQuery, EdgeImportModeQuery,
-                            CoordinatorQuery> {};
+    : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, EdgeIndexQuery, AuthQuery,
+                            DatabaseInfoQuery, SystemInfoQuery, ConstraintQuery, DumpQuery, ReplicationQuery,
+                            LockPathQuery, FreeMemoryQuery, TriggerQuery, IsolationLevelQuery, CreateSnapshotQuery,
+                            StreamQuery, SettingQuery, VersionQuery, ShowConfigQuery, TransactionQueueQuery,
+                            StorageModeQuery, AnalyzeGraphQuery, MultiDatabaseQuery, ShowDatabasesQuery,
+                            EdgeImportModeQuery, CoordinatorQuery> {};
 
 }  // namespace memgraph::query
diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp
index d3747bc3f..467c73125 100644
--- a/src/query/frontend/ast/cypher_main_visitor.cpp
+++ b/src/query/frontend/ast/cypher_main_visitor.cpp
@@ -265,6 +265,27 @@ antlrcpp::Any CypherMainVisitor::visitDropIndex(MemgraphCypher::DropIndexContext
   return index_query;
 }
 
+antlrcpp::Any CypherMainVisitor::visitEdgeIndexQuery(MemgraphCypher::EdgeIndexQueryContext *ctx) {
+  MG_ASSERT(ctx->children.size() == 1, "EdgeIndexQuery should have exactly one child!");
+  auto *index_query = std::any_cast<EdgeIndexQuery *>(ctx->children[0]->accept(this));
+  query_ = index_query;
+  return index_query;
+}
+
+antlrcpp::Any CypherMainVisitor::visitCreateEdgeIndex(MemgraphCypher::CreateEdgeIndexContext *ctx) {
+  auto *index_query = storage_->Create<EdgeIndexQuery>();
+  index_query->action_ = EdgeIndexQuery::Action::CREATE;
+  index_query->edge_type_ = AddEdgeType(std::any_cast<std::string>(ctx->labelName()->accept(this)));
+  return index_query;
+}
+
+antlrcpp::Any CypherMainVisitor::visitDropEdgeIndex(MemgraphCypher::DropEdgeIndexContext *ctx) {
+  auto *index_query = storage_->Create<EdgeIndexQuery>();
+  index_query->action_ = EdgeIndexQuery::Action::DROP;
+  index_query->edge_type_ = AddEdgeType(std::any_cast<std::string>(ctx->labelName()->accept(this)));
+  return index_query;
+}
+
 antlrcpp::Any CypherMainVisitor::visitAuthQuery(MemgraphCypher::AuthQueryContext *ctx) {
   MG_ASSERT(ctx->children.size() == 1, "AuthQuery should have exactly one child!");
   auto *auth_query = std::any_cast<AuthQuery *>(ctx->children[0]->accept(this));
diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp
index 6d66e6d7e..8c65345c8 100644
--- a/src/query/frontend/ast/cypher_main_visitor.hpp
+++ b/src/query/frontend/ast/cypher_main_visitor.hpp
@@ -148,6 +148,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
    */
   antlrcpp::Any visitIndexQuery(MemgraphCypher::IndexQueryContext *ctx) override;
 
+  /**
+   * @return IndexQuery*
+   */
+  antlrcpp::Any visitEdgeIndexQuery(MemgraphCypher::EdgeIndexQueryContext *ctx) override;
+
   /**
    * @return ExplainQuery*
    */
@@ -499,6 +504,16 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
    */
   antlrcpp::Any visitDropIndex(MemgraphCypher::DropIndexContext *ctx) override;
 
+  /**
+   * @return EdgeIndexQuery*
+   */
+  antlrcpp::Any visitCreateEdgeIndex(MemgraphCypher::CreateEdgeIndexContext *ctx) override;
+
+  /**
+   * @return DropEdgeIndex*
+   */
+  antlrcpp::Any visitDropEdgeIndex(MemgraphCypher::DropEdgeIndexContext *ctx) override;
+
   /**
    * @return AuthQuery*
    */
diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4
index d24480b0a..0147bba04 100644
--- a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4
+++ b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4
@@ -133,6 +133,7 @@ symbolicName : UnescapedSymbolicName
 
 query : cypherQuery
       | indexQuery
+      | edgeIndexQuery
       | explainQuery
       | profileQuery
       | databaseInfoQuery
@@ -527,3 +528,9 @@ showDatabase : SHOW DATABASE ;
 showDatabases : SHOW DATABASES ;
 
 edgeImportModeQuery : EDGE IMPORT MODE ( ACTIVE | INACTIVE ) ;
+
+createEdgeIndex : CREATE EDGE INDEX ON ':' labelName ;
+
+dropEdgeIndex : DROP EDGE INDEX ON ':' labelName ;
+
+edgeIndexQuery : createEdgeIndex | dropEdgeIndex ;
diff --git a/src/query/frontend/semantic/required_privileges.cpp b/src/query/frontend/semantic/required_privileges.cpp
index ef66a75ac..15726e3e2 100644
--- a/src/query/frontend/semantic/required_privileges.cpp
+++ b/src/query/frontend/semantic/required_privileges.cpp
@@ -27,6 +27,8 @@ class PrivilegeExtractor : public QueryVisitor<void>, public HierarchicalTreeVis
 
   void Visit(IndexQuery & /*unused*/) override { AddPrivilege(AuthQuery::Privilege::INDEX); }
 
+  void Visit(EdgeIndexQuery & /*unused*/) override { AddPrivilege(AuthQuery::Privilege::INDEX); }
+
   void Visit(AnalyzeGraphQuery & /*unused*/) override { AddPrivilege(AuthQuery::Privilege::INDEX); }
 
   void Visit(AuthQuery & /*unused*/) override { AddPrivilege(AuthQuery::Privilege::AUTH); }
diff --git a/src/query/frontend/semantic/symbol.hpp b/src/query/frontend/semantic/symbol.hpp
index 77557b6fe..0cfb86608 100644
--- a/src/query/frontend/semantic/symbol.hpp
+++ b/src/query/frontend/semantic/symbol.hpp
@@ -53,6 +53,8 @@ class Symbol {
   bool user_declared() const { return user_declared_; }
   int token_position() const { return token_position_; }
 
+  bool IsSymbolAnonym() const { return name_.substr(0U, 4U) == "anon"; }
+
   std::string name_;
   int64_t position_;
   bool user_declared_{true};
diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp
index ecec4fccb..ce74586d3 100644
--- a/src/query/interpreter.cpp
+++ b/src/query/interpreter.cpp
@@ -2679,6 +2679,75 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
       RWType::W};
 }
 
+PreparedQuery PrepareEdgeIndexQuery(ParsedQuery parsed_query, bool in_explicit_transaction,
+                                    std::vector<Notification> *notifications, CurrentDB &current_db) {
+  if (in_explicit_transaction) {
+    throw IndexInMulticommandTxException();
+  }
+
+  auto *index_query = utils::Downcast<EdgeIndexQuery>(parsed_query.query);
+  std::function<void(Notification &)> handler;
+
+  MG_ASSERT(current_db.db_acc_, "Index query expects a current DB");
+  auto &db_acc = *current_db.db_acc_;
+
+  MG_ASSERT(current_db.db_transactional_accessor_, "Index query expects a current DB transaction");
+  auto *dba = &*current_db.execution_db_accessor_;
+
+  auto invalidate_plan_cache = [plan_cache = db_acc->plan_cache()] {
+    plan_cache->WithLock([&](auto &cache) { cache.reset(); });
+  };
+
+  auto *storage = db_acc->storage();
+  auto edge_type = storage->NameToEdgeType(index_query->edge_type_.name);
+
+  Notification index_notification(SeverityLevel::INFO);
+  switch (index_query->action_) {
+    case EdgeIndexQuery::Action::CREATE: {
+      index_notification.code = NotificationCode::CREATE_INDEX;
+      index_notification.title = fmt::format("Created index on edge-type {}.", index_query->edge_type_.name);
+
+      handler = [dba, edge_type, label_name = index_query->edge_type_.name,
+                 invalidate_plan_cache = std::move(invalidate_plan_cache)](Notification &index_notification) {
+        auto maybe_index_error = dba->CreateIndex(edge_type);
+        utils::OnScopeExit invalidator(invalidate_plan_cache);
+
+        if (maybe_index_error.HasError()) {
+          index_notification.code = NotificationCode::EXISTENT_INDEX;
+          index_notification.title = fmt::format("Index on edge-type {} already exists.", label_name);
+        }
+      };
+      break;
+    }
+    case EdgeIndexQuery::Action::DROP: {
+      index_notification.code = NotificationCode::DROP_INDEX;
+      index_notification.title = fmt::format("Dropped index on edge-type {}.", index_query->edge_type_.name);
+      handler = [dba, edge_type, label_name = index_query->edge_type_.name,
+                 invalidate_plan_cache = std::move(invalidate_plan_cache)](Notification &index_notification) {
+        auto maybe_index_error = dba->DropIndex(edge_type);
+        utils::OnScopeExit invalidator(invalidate_plan_cache);
+
+        if (maybe_index_error.HasError()) {
+          index_notification.code = NotificationCode::NONEXISTENT_INDEX;
+          index_notification.title = fmt::format("Index on edge-type {} doesn't exist.", label_name);
+        }
+      };
+      break;
+    }
+  }
+
+  return PreparedQuery{
+      {},
+      std::move(parsed_query.required_privileges),
+      [handler = std::move(handler), notifications, index_notification = std::move(index_notification)](
+          AnyStream * /*stream*/, std::optional<int> /*unused*/) mutable {
+        handler(index_notification);
+        notifications->push_back(index_notification);
+        return QueryHandlerResult::COMMIT;
+      },
+      RWType::W};
+}
+
 PreparedQuery PrepareAuthQuery(ParsedQuery parsed_query, bool in_explicit_transaction,
                                InterpreterContext *interpreter_context, Interpreter &interpreter) {
   if (in_explicit_transaction) {
@@ -3483,6 +3552,7 @@ PreparedQuery PrepareDatabaseInfoQuery(ParsedQuery parsed_query, bool in_explici
         auto *storage = database->storage();
         const std::string_view label_index_mark{"label"};
         const std::string_view label_property_index_mark{"label+property"};
+        const std::string_view edge_type_index_mark{"edge-type"};
         auto info = dba->ListAllIndices();
         auto storage_acc = database->Access();
         std::vector<std::vector<TypedValue>> results;
@@ -3497,6 +3567,10 @@ PreparedQuery PrepareDatabaseInfoQuery(ParsedQuery parsed_query, bool in_explici
                TypedValue(storage->PropertyToName(item.second)),
                TypedValue(static_cast<int>(storage_acc->ApproximateVertexCount(item.first, item.second)))});
         }
+        for (const auto &item : info.edge_type) {
+          results.push_back({TypedValue(edge_type_index_mark), TypedValue(storage->EdgeTypeToName(item)), TypedValue(),
+                             TypedValue(static_cast<int>(storage_acc->ApproximateEdgeCount(item)))});
+        }
         std::sort(results.begin(), results.end(), [&label_index_mark](const auto &record_1, const auto &record_2) {
           const auto type_1 = record_1[0].ValueString();
           const auto type_2 = record_2[0].ValueString();
@@ -4283,13 +4357,14 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
         utils::Downcast<CypherQuery>(parsed_query.query) || utils::Downcast<ExplainQuery>(parsed_query.query) ||
         utils::Downcast<ProfileQuery>(parsed_query.query) || utils::Downcast<DumpQuery>(parsed_query.query) ||
         utils::Downcast<TriggerQuery>(parsed_query.query) || utils::Downcast<AnalyzeGraphQuery>(parsed_query.query) ||
-        utils::Downcast<IndexQuery>(parsed_query.query) || utils::Downcast<DatabaseInfoQuery>(parsed_query.query) ||
-        utils::Downcast<ConstraintQuery>(parsed_query.query);
+        utils::Downcast<IndexQuery>(parsed_query.query) || utils::Downcast<EdgeIndexQuery>(parsed_query.query) ||
+        utils::Downcast<DatabaseInfoQuery>(parsed_query.query) || utils::Downcast<ConstraintQuery>(parsed_query.query);
 
     if (!in_explicit_transaction_ && requires_db_transaction) {
       // TODO: ATM only a single database, will change when we have multiple database transactions
       bool could_commit = utils::Downcast<CypherQuery>(parsed_query.query) != nullptr;
       bool unique = utils::Downcast<IndexQuery>(parsed_query.query) != nullptr ||
+                    utils::Downcast<EdgeIndexQuery>(parsed_query.query) != nullptr ||
                     utils::Downcast<ConstraintQuery>(parsed_query.query) != nullptr ||
                     upper_case_query.find(kSchemaAssert) != std::string::npos;
       SetupDatabaseTransaction(could_commit, unique);
@@ -4326,6 +4401,9 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
     } else if (utils::Downcast<IndexQuery>(parsed_query.query)) {
       prepared_query = PrepareIndexQuery(std::move(parsed_query), in_explicit_transaction_,
                                          &query_execution->notifications, current_db_);
+    } else if (utils::Downcast<EdgeIndexQuery>(parsed_query.query)) {
+      prepared_query = PrepareEdgeIndexQuery(std::move(parsed_query), in_explicit_transaction_,
+                                             &query_execution->notifications, current_db_);
     } else if (utils::Downcast<AnalyzeGraphQuery>(parsed_query.query)) {
       prepared_query = PrepareAnalyzeGraphQuery(std::move(parsed_query), in_explicit_transaction_, current_db_);
     } else if (utils::Downcast<AuthQuery>(parsed_query.query)) {
diff --git a/src/query/plan/hint_provider.hpp b/src/query/plan/hint_provider.hpp
index b70de9aaf..3c8510561 100644
--- a/src/query/plan/hint_provider.hpp
+++ b/src/query/plan/hint_provider.hpp
@@ -114,6 +114,9 @@ class PlanHintsProvider final : public HierarchicalLogicalOperatorVisitor {
   bool PreVisit(ScanAllById & /*unused*/) override { return true; }
   bool PostVisit(ScanAllById & /*unused*/) override { return true; }
 
+  bool PreVisit(ScanAllByEdgeType & /*unused*/) override { return true; }
+  bool PostVisit(ScanAllByEdgeType & /*unused*/) override { return true; }
+
   bool PreVisit(ConstructNamedPath & /*unused*/) override { return true; }
   bool PostVisit(ConstructNamedPath & /*unused*/) override { return true; }
 
diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp
index ba421b653..7cd506050 100644
--- a/src/query/plan/operator.cpp
+++ b/src/query/plan/operator.cpp
@@ -105,6 +105,7 @@ extern const Event ScanAllByLabelPropertyRangeOperator;
 extern const Event ScanAllByLabelPropertyValueOperator;
 extern const Event ScanAllByLabelPropertyOperator;
 extern const Event ScanAllByIdOperator;
+extern const Event ScanAllByEdgeTypeOperator;
 extern const Event ExpandOperator;
 extern const Event ExpandVariableOperator;
 extern const Event ConstructNamedPathOperator;
@@ -517,6 +518,60 @@ class ScanAllCursor : public Cursor {
   const char *op_name_;
 };
 
+template <typename TEdgesFun>
+class ScanAllByEdgeTypeCursor : public Cursor {
+ public:
+  explicit ScanAllByEdgeTypeCursor(const ScanAllByEdgeType &self, Symbol output_symbol, UniqueCursorPtr input_cursor,
+                                   storage::View view, TEdgesFun get_edges, const char *op_name)
+      : self_(self),
+        output_symbol_(std::move(output_symbol)),
+        input_cursor_(std::move(input_cursor)),
+        view_(view),
+        get_edges_(std::move(get_edges)),
+        op_name_(op_name) {}
+
+  bool Pull(Frame &frame, ExecutionContext &context) override {
+    OOMExceptionEnabler oom_exception;
+    SCOPED_PROFILE_OP_BY_REF(self_);
+
+    AbortCheck(context);
+
+    while (!vertices_ || vertices_it_.value() == vertices_end_it_.value()) {
+      if (!input_cursor_->Pull(frame, context)) return false;
+      auto next_vertices = get_edges_(frame, context);
+      if (!next_vertices) continue;
+
+      vertices_.emplace(std::move(next_vertices.value()));
+      vertices_it_.emplace(vertices_.value().begin());
+      vertices_end_it_.emplace(vertices_.value().end());
+    }
+
+    frame[output_symbol_] = *vertices_it_.value();
+    ++vertices_it_.value();
+    return true;
+  }
+
+  void Shutdown() override { input_cursor_->Shutdown(); }
+
+  void Reset() override {
+    input_cursor_->Reset();
+    vertices_ = std::nullopt;
+    vertices_it_ = std::nullopt;
+    vertices_end_it_ = std::nullopt;
+  }
+
+ private:
+  const ScanAllByEdgeType &self_;
+  const Symbol output_symbol_;
+  const UniqueCursorPtr input_cursor_;
+  storage::View view_;
+  TEdgesFun get_edges_;
+  std::optional<typename std::result_of<TEdgesFun(Frame &, ExecutionContext &)>::type::value_type> vertices_;
+  std::optional<decltype(vertices_.value().begin())> vertices_it_;
+  std::optional<decltype(vertices_.value().end())> vertices_end_it_;
+  const char *op_name_;
+};
+
 ScanAll::ScanAll(const std::shared_ptr<LogicalOperator> &input, Symbol output_symbol, storage::View view)
     : input_(input ? input : std::make_shared<Once>()), output_symbol_(std::move(output_symbol)), view_(view) {}
 
@@ -556,6 +611,33 @@ UniqueCursorPtr ScanAllByLabel::MakeCursor(utils::MemoryResource *mem) const {
                                                                 view_, std::move(vertices), "ScanAllByLabel");
 }
 
+ScanAllByEdgeType::ScanAllByEdgeType(const std::shared_ptr<LogicalOperator> &input, Symbol output_symbol,
+                                     storage::EdgeTypeId edge_type, storage::View view)
+    : input_(input ? input : std::make_shared<Once>()),
+      output_symbol_(std::move(output_symbol)),
+      view_(view),
+      edge_type_(edge_type) {}
+
+ACCEPT_WITH_INPUT(ScanAllByEdgeType)
+
+UniqueCursorPtr ScanAllByEdgeType::MakeCursor(utils::MemoryResource *mem) const {
+  memgraph::metrics::IncrementCounter(memgraph::metrics::ScanAllByEdgeTypeOperator);
+
+  auto edges = [this](Frame &, ExecutionContext &context) {
+    auto *db = context.db_accessor;
+    return std::make_optional(db->Edges(view_, edge_type_));
+  };
+
+  return MakeUniqueCursorPtr<ScanAllByEdgeTypeCursor<decltype(edges)>>(
+      mem, *this, output_symbol_, input_->MakeCursor(mem), view_, std::move(edges), "ScanAllByEdgeType");
+}
+
+std::vector<Symbol> ScanAllByEdgeType::ModifiedSymbols(const SymbolTable &table) const {
+  auto symbols = input_->ModifiedSymbols(table);
+  symbols.emplace_back(output_symbol_);
+  return symbols;
+}
+
 // TODO(buda): Implement ScanAllByLabelProperty operator to iterate over
 // vertices that have the label and some value for the given property.
 
diff --git a/src/query/plan/operator.hpp b/src/query/plan/operator.hpp
index cdaca2875..6563c2bb0 100644
--- a/src/query/plan/operator.hpp
+++ b/src/query/plan/operator.hpp
@@ -99,6 +99,7 @@ class ScanAllByLabelPropertyRange;
 class ScanAllByLabelPropertyValue;
 class ScanAllByLabelProperty;
 class ScanAllById;
+class ScanAllByEdgeType;
 class Expand;
 class ExpandVariable;
 class ConstructNamedPath;
@@ -134,10 +135,10 @@ class RollUpApply;
 
 using LogicalOperatorCompositeVisitor =
     utils::CompositeVisitor<Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel, ScanAllByLabelPropertyRange,
-                            ScanAllByLabelPropertyValue, ScanAllByLabelProperty, ScanAllById, Expand, ExpandVariable,
-                            ConstructNamedPath, Filter, Produce, Delete, SetProperty, SetProperties, SetLabels,
-                            RemoveProperty, RemoveLabels, EdgeUniquenessFilter, Accumulate, Aggregate, Skip, Limit,
-                            OrderBy, Merge, Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv,
+                            ScanAllByLabelPropertyValue, ScanAllByLabelProperty, ScanAllById, ScanAllByEdgeType, Expand,
+                            ExpandVariable, ConstructNamedPath, Filter, Produce, Delete, SetProperty, SetProperties,
+                            SetLabels, RemoveProperty, RemoveLabels, EdgeUniquenessFilter, Accumulate, Aggregate, Skip,
+                            Limit, OrderBy, Merge, Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv,
                             Foreach, EmptyResult, EvaluatePatternFilter, Apply, IndexedJoin, HashJoin, RollUpApply>;
 
 using LogicalOperatorLeafVisitor = utils::LeafVisitor<Once>;
@@ -592,6 +593,42 @@ class ScanAllByLabel : public memgraph::query::plan::ScanAll {
   }
 };
 
+class ScanAllByEdgeType : public memgraph::query::plan::LogicalOperator {
+ public:
+  static const utils::TypeInfo kType;
+  const utils::TypeInfo &GetTypeInfo() const override { return kType; }
+
+  ScanAllByEdgeType() = default;
+  ScanAllByEdgeType(const std::shared_ptr<LogicalOperator> &input, Symbol output_symbol, storage::EdgeTypeId edge_type,
+                    storage::View view = storage::View::OLD);
+  bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
+  UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override;
+  std::vector<Symbol> ModifiedSymbols(const SymbolTable &) const override;
+
+  bool HasSingleInput() const override { return true; }
+  std::shared_ptr<LogicalOperator> input() const override { return input_; }
+  void set_input(std::shared_ptr<LogicalOperator> input) override { input_ = input; }
+
+  std::string ToString() const override {
+    return fmt::format("ScanAllByEdgeType ({} :{})", output_symbol_.name(), dba_->EdgeTypeToName(edge_type_));
+  }
+
+  std::shared_ptr<memgraph::query::plan::LogicalOperator> input_;
+  Symbol output_symbol_;
+  storage::View view_;
+
+  storage::EdgeTypeId edge_type_;
+
+  std::unique_ptr<LogicalOperator> Clone(AstStorage *storage) const override {
+    auto object = std::make_unique<ScanAllByEdgeType>();
+    object->input_ = input_ ? input_->Clone(storage) : nullptr;
+    object->output_symbol_ = output_symbol_;
+    object->view_ = view_;
+    object->edge_type_ = edge_type_;
+    return object;
+  }
+};
+
 /// Behaves like @c ScanAll, but produces only vertices with given label and
 /// property value which is inside a range (inclusive or exlusive).
 ///
diff --git a/src/query/plan/operator_type_info.cpp b/src/query/plan/operator_type_info.cpp
index 168137552..6b0a28313 100644
--- a/src/query/plan/operator_type_info.cpp
+++ b/src/query/plan/operator_type_info.cpp
@@ -49,6 +49,8 @@ constexpr utils::TypeInfo query::plan::ScanAllByLabelProperty::kType{
 
 constexpr utils::TypeInfo query::plan::ScanAllById::kType{utils::TypeId::SCAN_ALL_BY_ID, "ScanAllById",
                                                           &query::plan::ScanAll::kType};
+constexpr utils::TypeInfo query::plan::ScanAllByEdgeType::kType{utils::TypeId::SCAN_ALL_BY_EDGE_TYPE,
+                                                                "ScanAllByEdgeType", &query::plan::ScanAll::kType};
 
 constexpr utils::TypeInfo query::plan::ExpandCommon::kType{utils::TypeId::EXPAND_COMMON, "ExpandCommon", nullptr};
 
diff --git a/src/query/plan/planner.hpp b/src/query/plan/planner.hpp
index e8ca80e39..3136e7271 100644
--- a/src/query/plan/planner.hpp
+++ b/src/query/plan/planner.hpp
@@ -23,6 +23,7 @@
 #include "query/plan/operator.hpp"
 #include "query/plan/preprocess.hpp"
 #include "query/plan/pretty_print.hpp"
+#include "query/plan/rewrite/edge_type_index_lookup.hpp"
 #include "query/plan/rewrite/index_lookup.hpp"
 #include "query/plan/rewrite/join.hpp"
 #include "query/plan/rule_based_planner.hpp"
@@ -54,8 +55,11 @@ class PostProcessor final {
   std::unique_ptr<LogicalOperator> Rewrite(std::unique_ptr<LogicalOperator> plan, TPlanningContext *context) {
     auto index_lookup_plan =
         RewriteWithIndexLookup(std::move(plan), context->symbol_table, context->ast_storage, context->db, index_hints_);
-    return RewriteWithJoinRewriter(std::move(index_lookup_plan), context->symbol_table, context->ast_storage,
-                                   context->db);
+    auto join_plan =
+        RewriteWithJoinRewriter(std::move(index_lookup_plan), context->symbol_table, context->ast_storage, context->db);
+    auto edge_index_plan = RewriteWithEdgeTypeIndexRewriter(std::move(join_plan), context->symbol_table,
+                                                            context->ast_storage, context->db);
+    return edge_index_plan;
   }
 
   template <class TVertexCounts>
diff --git a/src/query/plan/pretty_print.cpp b/src/query/plan/pretty_print.cpp
index 7938f9c73..eeb0c15b5 100644
--- a/src/query/plan/pretty_print.cpp
+++ b/src/query/plan/pretty_print.cpp
@@ -76,6 +76,13 @@ bool PlanPrinter::PreVisit(ScanAllById &op) {
   return true;
 }
 
+bool PlanPrinter::PreVisit(query::plan::ScanAllByEdgeType &op) {
+  op.dba_ = dba_;
+  WithPrintLn([&op](auto &out) { out << "* " << op.ToString(); });
+  op.dba_ = nullptr;
+  return true;
+}
+
 bool PlanPrinter::PreVisit(query::plan::Expand &op) {
   op.dba_ = dba_;
   WithPrintLn([&op](auto &out) { out << "* " << op.ToString(); });
@@ -464,6 +471,19 @@ bool PlanToJsonVisitor::PreVisit(ScanAllById &op) {
   return false;
 }
 
+bool PlanToJsonVisitor::PreVisit(ScanAllByEdgeType &op) {
+  json self;
+  self["name"] = "ScanAllByEdgeType";
+  self["edge_type"] = ToJson(op.edge_type_, *dba_);
+  self["output_symbol"] = ToJson(op.output_symbol_);
+
+  op.input_->Accept(*this);
+  self["input"] = PopOutput();
+
+  output_ = std::move(self);
+  return false;
+}
+
 bool PlanToJsonVisitor::PreVisit(CreateNode &op) {
   json self;
   self["name"] = "CreateNode";
diff --git a/src/query/plan/pretty_print.hpp b/src/query/plan/pretty_print.hpp
index af8429b85..d62ae6bf2 100644
--- a/src/query/plan/pretty_print.hpp
+++ b/src/query/plan/pretty_print.hpp
@@ -67,6 +67,7 @@ class PlanPrinter : public virtual HierarchicalLogicalOperatorVisitor {
   bool PreVisit(ScanAllByLabelPropertyRange &) override;
   bool PreVisit(ScanAllByLabelProperty &) override;
   bool PreVisit(ScanAllById &) override;
+  bool PreVisit(ScanAllByEdgeType &) override;
 
   bool PreVisit(Expand &) override;
   bool PreVisit(ExpandVariable &) override;
@@ -204,6 +205,7 @@ class PlanToJsonVisitor : public virtual HierarchicalLogicalOperatorVisitor {
   bool PreVisit(ScanAllByLabelPropertyValue &) override;
   bool PreVisit(ScanAllByLabelProperty &) override;
   bool PreVisit(ScanAllById &) override;
+  bool PreVisit(ScanAllByEdgeType &) override;
 
   bool PreVisit(EmptyResult &) override;
   bool PreVisit(Produce &) override;
diff --git a/src/query/plan/rewrite/edge_type_index_lookup.hpp b/src/query/plan/rewrite/edge_type_index_lookup.hpp
new file mode 100644
index 000000000..ed8666513
--- /dev/null
+++ b/src/query/plan/rewrite/edge_type_index_lookup.hpp
@@ -0,0 +1,534 @@
+// Copyright 2024 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.
+
+/// @file
+/// This file provides a plan rewriter which replaces `ScanAll` and `Expand`
+/// operations with `ScanAllByEdgeType` if possible. The public entrypoint is
+/// `RewriteWithEdgeTypeIndexRewriter`.
+
+#pragma once
+
+#include <algorithm>
+#include <memory>
+#include <optional>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include <gflags/gflags.h>
+
+#include "query/plan/operator.hpp"
+#include "query/plan/preprocess.hpp"
+#include "query/plan/rewrite/index_lookup.hpp"
+#include "utils/algorithm.hpp"
+
+namespace memgraph::query::plan {
+
+namespace impl {
+
+template <class TDbAccessor>
+class EdgeTypeIndexRewriter final : public HierarchicalLogicalOperatorVisitor {
+ public:
+  EdgeTypeIndexRewriter(SymbolTable *symbol_table, AstStorage *ast_storage, TDbAccessor *db)
+      : symbol_table_(symbol_table), ast_storage_(ast_storage), db_(db) {}
+
+  using HierarchicalLogicalOperatorVisitor::PostVisit;
+  using HierarchicalLogicalOperatorVisitor::PreVisit;
+  using HierarchicalLogicalOperatorVisitor::Visit;
+
+  bool Visit(Once &) override { return true; }
+
+  bool PreVisit(Filter &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(Filter & /*op*/) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAll &op) override {
+    prev_ops_.push_back(&op);
+
+    if (op.input()->GetTypeInfo() == Once::kType) {
+      const bool is_node_anon = op.output_symbol_.IsSymbolAnonym();
+      once_under_scanall_ = is_node_anon;
+    }
+
+    return true;
+  }
+
+  bool PostVisit(ScanAll &op) override {
+    prev_ops_.pop_back();
+
+    if (EdgeTypeIndexingPossible()) {
+      SetOnParent(op.input());
+    }
+
+    return true;
+  }
+
+  bool PreVisit(Expand &op) override {
+    prev_ops_.push_back(&op);
+
+    if (op.input()->GetTypeInfo() == ScanAll::kType) {
+      const bool only_one_edge_type = (op.common_.edge_types.size() == 1U);
+      const bool expansion_is_named = !(op.common_.edge_symbol.IsSymbolAnonym());
+      const bool expdanded_node_not_named = op.common_.node_symbol.IsSymbolAnonym();
+
+      edge_type_index_exist = only_one_edge_type ? db_->EdgeTypeIndexExists(op.common_.edge_types.front()) : false;
+
+      scanall_under_expand_ = only_one_edge_type && expansion_is_named && expdanded_node_not_named;
+    }
+
+    return true;
+  }
+
+  bool PostVisit(Expand &op) override {
+    prev_ops_.pop_back();
+
+    if (EdgeTypeIndexingPossible()) {
+      auto indexed_scan = GenEdgeTypeScan(op);
+      SetOnParent(std::move(indexed_scan));
+    }
+
+    return true;
+  }
+
+  bool PreVisit(ExpandVariable &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(ExpandVariable &expand) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Merge &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.merge_match_);
+    return false;
+  }
+
+  bool PostVisit(Merge &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Optional &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.optional_);
+    return false;
+  }
+
+  bool PostVisit(Optional &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Cartesian &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(Cartesian &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(IndexedJoin &op) override {
+    prev_ops_.push_back(&op);
+    RewriteBranch(&op.main_branch_);
+    RewriteBranch(&op.sub_branch_);
+    return false;
+  }
+
+  bool PostVisit(IndexedJoin &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(HashJoin &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(HashJoin &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Union &op) override {
+    prev_ops_.push_back(&op);
+    RewriteBranch(&op.left_op_);
+    RewriteBranch(&op.right_op_);
+    return false;
+  }
+
+  bool PostVisit(Union &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(CreateNode &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(CreateNode &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(CreateExpand &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(CreateExpand &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByLabel &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByLabel &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByLabelPropertyRange &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByLabelPropertyRange &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByLabelPropertyValue &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByLabelPropertyValue &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByLabelProperty &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByLabelProperty &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllById &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllById &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ScanAllByEdgeType &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ScanAllByEdgeType &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(ConstructNamedPath &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(ConstructNamedPath &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Produce &op) override {
+    prev_ops_.push_back(&op);
+
+    if (op.input()->GetTypeInfo() == Expand::kType) {
+      expand_under_produce_ = true;
+    }
+
+    return true;
+  }
+  bool PostVisit(Produce &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(EmptyResult &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(EmptyResult &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Delete &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Delete &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(SetProperty &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(SetProperty &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(SetProperties &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(SetProperties &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(SetLabels &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(SetLabels &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(RemoveProperty &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(RemoveProperty &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(RemoveLabels &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(RemoveLabels &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(EdgeUniquenessFilter &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(EdgeUniquenessFilter &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Accumulate &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Accumulate &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Aggregate &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Aggregate &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Skip &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Skip &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Limit &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Limit &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(OrderBy &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(OrderBy &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Unwind &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Unwind &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Distinct &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(Distinct &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(CallProcedure &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+  bool PostVisit(CallProcedure &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Foreach &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.update_clauses_);
+    return false;
+  }
+
+  bool PostVisit(Foreach &) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(EvaluatePatternFilter &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(EvaluatePatternFilter & /*op*/) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(Apply &op) override {
+    prev_ops_.push_back(&op);
+    op.input()->Accept(*this);
+    RewriteBranch(&op.subquery_);
+    return false;
+  }
+
+  bool PostVisit(Apply & /*op*/) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  bool PreVisit(LoadCsv &op) override {
+    prev_ops_.push_back(&op);
+    return true;
+  }
+
+  bool PostVisit(LoadCsv & /*op*/) override {
+    prev_ops_.pop_back();
+    return true;
+  }
+
+  std::shared_ptr<LogicalOperator> new_root_;
+
+ private:
+  SymbolTable *symbol_table_;
+  AstStorage *ast_storage_;
+  TDbAccessor *db_;
+  // Collected filters, pending for examination if they can be used for advanced
+  // lookup operations (by index, node ID, ...).
+  Filters filters_;
+  // Expressions which no longer need a plain Filter operator.
+  std::unordered_set<Expression *> filter_exprs_for_removal_;
+  std::vector<LogicalOperator *> prev_ops_;
+  std::unordered_set<Symbol> cartesian_symbols_;
+
+  bool EdgeTypeIndexingPossible() const {
+    return expand_under_produce_ && scanall_under_expand_ && once_under_scanall_ && edge_type_index_exist;
+  }
+  bool expand_under_produce_ = false;
+  bool scanall_under_expand_ = false;
+  bool once_under_scanall_ = false;
+  bool edge_type_index_exist = false;
+
+  bool DefaultPreVisit() override {
+    throw utils::NotYetImplemented("Operator not yet covered by EdgeTypeIndexRewriter");
+  }
+
+  std::unique_ptr<ScanAllByEdgeType> GenEdgeTypeScan(const Expand &expand) {
+    const auto &input = expand.input();
+    const auto &output_symbol = expand.common_.edge_symbol;
+    const auto &view = expand.view_;
+
+    // Extract edge_type from symbol
+    auto edge_type = expand.common_.edge_types.front();
+    return std::make_unique<ScanAllByEdgeType>(input, output_symbol, edge_type, view);
+  }
+
+  void SetOnParent(const std::shared_ptr<LogicalOperator> &input) {
+    MG_ASSERT(input);
+    if (prev_ops_.empty()) {
+      MG_ASSERT(!new_root_);
+      new_root_ = input;
+      return;
+    }
+    prev_ops_.back()->set_input(input);
+  }
+
+  void RewriteBranch(std::shared_ptr<LogicalOperator> *branch) {
+    EdgeTypeIndexRewriter<TDbAccessor> rewriter(symbol_table_, ast_storage_, db_);
+    (*branch)->Accept(rewriter);
+    if (rewriter.new_root_) {
+      *branch = rewriter.new_root_;
+    }
+  }
+};
+
+}  // namespace impl
+
+template <class TDbAccessor>
+std::unique_ptr<LogicalOperator> RewriteWithEdgeTypeIndexRewriter(std::unique_ptr<LogicalOperator> root_op,
+                                                                  SymbolTable *symbol_table, AstStorage *ast_storage,
+                                                                  TDbAccessor *db) {
+  impl::EdgeTypeIndexRewriter<TDbAccessor> rewriter(symbol_table, ast_storage, db);
+  root_op->Accept(rewriter);
+  return root_op;
+}
+
+}  // namespace memgraph::query::plan
diff --git a/src/query/plan/vertex_count_cache.hpp b/src/query/plan/vertex_count_cache.hpp
index 4cfb2486b..802f4e09f 100644
--- a/src/query/plan/vertex_count_cache.hpp
+++ b/src/query/plan/vertex_count_cache.hpp
@@ -78,6 +78,8 @@ class VertexCountCache {
     return db_->LabelPropertyIndexExists(label, property);
   }
 
+  bool EdgeTypeIndexExists(storage::EdgeTypeId edge_type) { return db_->EdgeTypeIndexExists(edge_type); }
+
   std::optional<storage::LabelIndexStats> GetIndexStats(const storage::LabelId &label) const {
     return db_->GetIndexStats(label);
   }
diff --git a/src/query/procedure/module.hpp b/src/query/procedure/module.hpp
index 41cda0ca6..f5027dafa 100644
--- a/src/query/procedure/module.hpp
+++ b/src/query/procedure/module.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
diff --git a/src/query/procedure/py_module.hpp b/src/query/procedure/py_module.hpp
index 9cb22fe2c..fe93b5c51 100644
--- a/src/query/procedure/py_module.hpp
+++ b/src/query/procedure/py_module.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
diff --git a/src/storage/v2/CMakeLists.txt b/src/storage/v2/CMakeLists.txt
index 150a02cc7..ec5108d63 100644
--- a/src/storage/v2/CMakeLists.txt
+++ b/src/storage/v2/CMakeLists.txt
@@ -21,8 +21,10 @@ add_library(mg-storage-v2 STATIC
         storage.cpp
         indices/indices.cpp
         all_vertices_iterable.cpp
+        edges_iterable.cpp
         vertices_iterable.cpp
         inmemory/storage.cpp
+        inmemory/edge_type_index.cpp
         inmemory/label_index.cpp
         inmemory/label_property_index.cpp
         inmemory/unique_constraints.cpp
@@ -30,6 +32,7 @@ add_library(mg-storage-v2 STATIC
         disk/edge_import_mode_cache.cpp
         disk/storage.cpp
         disk/rocksdb_storage.cpp
+        disk/edge_type_index.cpp
         disk/label_index.cpp
         disk/label_property_index.cpp
         disk/unique_constraints.cpp
diff --git a/src/storage/v2/disk/edge_type_index.cpp b/src/storage/v2/disk/edge_type_index.cpp
new file mode 100644
index 000000000..d11eb6caf
--- /dev/null
+++ b/src/storage/v2/disk/edge_type_index.cpp
@@ -0,0 +1,49 @@
+// Copyright 2024 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 "edge_type_index.hpp"
+
+#include "utils/exceptions.hpp"
+
+namespace memgraph::storage {
+
+bool DiskEdgeTypeIndex::DropIndex(EdgeTypeId /*edge_type*/) {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return true;
+}
+
+bool DiskEdgeTypeIndex::IndexExists(EdgeTypeId /*edge_type*/) const {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return false;
+}
+
+std::vector<EdgeTypeId> DiskEdgeTypeIndex::ListIndices() const {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return {};
+}
+
+uint64_t DiskEdgeTypeIndex::ApproximateEdgeCount(EdgeTypeId /*edge_type*/) const {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return 0U;
+}
+
+void DiskEdgeTypeIndex::UpdateOnEdgeCreation(Vertex * /*from*/, Vertex * /*to*/, EdgeRef /*edge_ref*/,
+                                             EdgeTypeId /*edge_type*/, const Transaction & /*tx*/) {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
+void DiskEdgeTypeIndex::UpdateOnEdgeModification(Vertex * /*old_from*/, Vertex * /*old_to*/, Vertex * /*new_from*/,
+                                                 Vertex * /*new_to*/, EdgeRef /*edge_ref*/, EdgeTypeId /*edge_type*/,
+                                                 const Transaction & /*tx*/) {
+  spdlog::warn("Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/disk/edge_type_index.hpp b/src/storage/v2/disk/edge_type_index.hpp
new file mode 100644
index 000000000..fe79b2690
--- /dev/null
+++ b/src/storage/v2/disk/edge_type_index.hpp
@@ -0,0 +1,35 @@
+// Copyright 2024 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/v2/indices/edge_type_index.hpp"
+
+namespace memgraph::storage {
+
+class DiskEdgeTypeIndex : public storage::EdgeTypeIndex {
+ public:
+  bool DropIndex(EdgeTypeId edge_type) override;
+
+  bool IndexExists(EdgeTypeId edge_type) const override;
+
+  std::vector<EdgeTypeId> ListIndices() const override;
+
+  uint64_t ApproximateEdgeCount(EdgeTypeId edge_type) const override;
+
+  void UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                            const Transaction &tx) override;
+
+  void UpdateOnEdgeModification(Vertex *old_from, Vertex *old_to, Vertex *new_from, Vertex *new_to, EdgeRef edge_ref,
+                                EdgeTypeId edge_type, const Transaction &tx) override;
+};
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/disk/storage.cpp b/src/storage/v2/disk/storage.cpp
index f9cd2ac13..21ae7755e 100644
--- a/src/storage/v2/disk/storage.cpp
+++ b/src/storage/v2/disk/storage.cpp
@@ -41,6 +41,7 @@
 #include "storage/v2/edge_accessor.hpp"
 #include "storage/v2/edge_import_mode.hpp"
 #include "storage/v2/edge_ref.hpp"
+#include "storage/v2/edges_iterable.hpp"
 #include "storage/v2/id_types.hpp"
 #include "storage/v2/modified_edge.hpp"
 #include "storage/v2/mvcc.hpp"
@@ -807,11 +808,21 @@ void DiskStorage::LoadVerticesFromDiskLabelPropertyIndexForIntervalSearch(
   }
 }
 
+EdgesIterable DiskStorage::DiskAccessor::Edges(EdgeTypeId /*edge_type*/, View /*view*/) {
+  throw utils::NotYetImplemented(
+      "Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
 uint64_t DiskStorage::DiskAccessor::ApproximateVertexCount() const {
   auto *disk_storage = static_cast<DiskStorage *>(storage_);
   return disk_storage->vertex_count_.load(std::memory_order_acquire);
 }
 
+uint64_t DiskStorage::DiskAccessor::ApproximateEdgeCount(EdgeTypeId /*edge_type*/) const {
+  spdlog::info("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return 0U;
+}
+
 uint64_t DiskStorage::GetDiskSpaceUsage() const {
   uint64_t main_disk_storage_size = utils::GetDirDiskUsage(config_.disk.main_storage_directory);
   uint64_t index_disk_storage_size = utils::GetDirDiskUsage(config_.disk.label_index_directory) +
@@ -1629,6 +1640,9 @@ utils::BasicResult<StorageManipulationError, void> DiskStorage::DiskAccessor::Co
             return StorageManipulationError{PersistenceError{}};
           }
         } break;
+        case MetadataDelta::Action::EDGE_INDEX_CREATE: {
+          throw utils::NotYetImplemented("Edge-type indexing is not yet implemented on on-disk storage mode.");
+        }
         case MetadataDelta::Action::LABEL_INDEX_DROP: {
           if (!disk_storage->durable_metadata_.PersistLabelIndexDeletion(md_delta.label)) {
             return StorageManipulationError{PersistenceError{}};
@@ -1641,6 +1655,9 @@ utils::BasicResult<StorageManipulationError, void> DiskStorage::DiskAccessor::Co
             return StorageManipulationError{PersistenceError{}};
           }
         } break;
+        case MetadataDelta::Action::EDGE_INDEX_DROP: {
+          throw utils::NotYetImplemented("Edge-type indexing is not yet implemented on on-disk storage mode.");
+        }
         case MetadataDelta::Action::LABEL_INDEX_STATS_SET: {
           throw utils::NotYetImplemented("SetIndexStats(stats) is not implemented for DiskStorage.");
         } break;
@@ -1917,6 +1934,11 @@ utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor:
   return {};
 }
 
+utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor::CreateIndex(EdgeTypeId /*edge_type*/) {
+  throw utils::NotYetImplemented(
+      "Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
 utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor::DropIndex(LabelId label) {
   MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!");
   auto *on_disk = static_cast<DiskStorage *>(storage_);
@@ -1945,6 +1967,11 @@ utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor:
   return {};
 }
 
+utils::BasicResult<StorageIndexDefinitionError, void> DiskStorage::DiskAccessor::DropIndex(EdgeTypeId /*edge_type*/) {
+  throw utils::NotYetImplemented(
+      "Edge-type index related operations are not yet supported using on-disk storage mode.");
+}
+
 utils::BasicResult<StorageExistenceConstraintDefinitionError, void>
 DiskStorage::DiskAccessor::CreateExistenceConstraint(LabelId label, PropertyId property) {
   MG_ASSERT(unique_guard_.owns_lock(), "Create existence constraint requires a unique access to the storage!");
@@ -2053,6 +2080,12 @@ std::unique_ptr<Storage::Accessor> DiskStorage::UniqueAccess(
   return std::unique_ptr<DiskAccessor>(
       new DiskAccessor{Storage::Accessor::unique_access, this, isolation_level, storage_mode_});
 }
+
+bool DiskStorage::DiskAccessor::EdgeTypeIndexExists(EdgeTypeId /*edge_type*/) const {
+  spdlog::info("Edge-type index related operations are not yet supported using on-disk storage mode.");
+  return false;
+}
+
 IndicesInfo DiskStorage::DiskAccessor::ListAllIndices() const {
   auto *on_disk = static_cast<DiskStorage *>(storage_);
   auto *disk_label_index = static_cast<DiskLabelIndex *>(on_disk->indices_.label_index_.get());
diff --git a/src/storage/v2/disk/storage.hpp b/src/storage/v2/disk/storage.hpp
index 4d71fd10b..349a7454a 100644
--- a/src/storage/v2/disk/storage.hpp
+++ b/src/storage/v2/disk/storage.hpp
@@ -72,6 +72,8 @@ class DiskStorage final : public Storage {
                               const std::optional<utils::Bound<PropertyValue>> &lower_bound,
                               const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) override;
 
+    EdgesIterable Edges(EdgeTypeId edge_type, View view) override;
+
     uint64_t ApproximateVertexCount() const override;
 
     uint64_t ApproximateVertexCount(LabelId /*label*/) const override { return 10; }
@@ -89,6 +91,8 @@ class DiskStorage final : public Storage {
       return 10;
     }
 
+    uint64_t ApproximateEdgeCount(EdgeTypeId edge_type) const override;
+
     std::optional<storage::LabelIndexStats> GetIndexStats(const storage::LabelId & /*label*/) const override {
       return {};
     }
@@ -140,6 +144,8 @@ class DiskStorage final : public Storage {
       return disk_storage->indices_.label_property_index_->IndexExists(label, property);
     }
 
+    bool EdgeTypeIndexExists(EdgeTypeId edge_type) const override;
+
     IndicesInfo ListAllIndices() const override;
 
     ConstraintsInfo ListAllConstraints() const override;
@@ -158,10 +164,14 @@ class DiskStorage final : public Storage {
 
     utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(LabelId label, PropertyId property) override;
 
+    utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(EdgeTypeId edge_type) override;
+
     utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label) override;
 
     utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label, PropertyId property) override;
 
+    utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(EdgeTypeId edge_type) override;
+
     utils::BasicResult<StorageExistenceConstraintDefinitionError, void> CreateExistenceConstraint(
         LabelId label, PropertyId property) override;
 
diff --git a/src/storage/v2/durability/durability.cpp b/src/storage/v2/durability/durability.cpp
index a83313820..fbbedbee5 100644
--- a/src/storage/v2/durability/durability.cpp
+++ b/src/storage/v2/durability/durability.cpp
@@ -31,6 +31,7 @@
 #include "storage/v2/durability/paths.hpp"
 #include "storage/v2/durability/snapshot.hpp"
 #include "storage/v2/durability/wal.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
 #include "storage/v2/inmemory/label_index.hpp"
 #include "storage/v2/inmemory/label_property_index.hpp"
 #include "storage/v2/inmemory/unique_constraints.hpp"
@@ -199,9 +200,18 @@ void RecoverIndicesAndStats(const RecoveredIndicesAndConstraints::IndicesMetadat
   }
   spdlog::info("Label+property indices statistics are recreated.");
 
-  spdlog::info("Indices are recreated.");
+  // Recover edge-type indices.
+  spdlog::info("Recreating {} edge-type indices from metadata.", indices_metadata.edge.size());
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(indices->edge_type_index_.get());
+  for (const auto &item : indices_metadata.edge) {
+    if (!mem_edge_type_index->CreateIndex(item, vertices->access())) {
+      throw RecoveryFailure("The edge-type index must be created here!");
+    }
+    spdlog::info("Index on :{} is recreated from metadata", name_id_mapper->IdToName(item.AsUint()));
+  }
+  spdlog::info("Edge-type indices are recreated.");
 
-  spdlog::info("Recreating constraints from metadata.");
+  spdlog::info("Indices are recreated.");
 }
 
 void RecoverExistenceConstraints(const RecoveredIndicesAndConstraints::ConstraintsMetadata &constraints_metadata,
diff --git a/src/storage/v2/durability/marker.hpp b/src/storage/v2/durability/marker.hpp
index 8f00d435d..ac0cc074d 100644
--- a/src/storage/v2/durability/marker.hpp
+++ b/src/storage/v2/durability/marker.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -37,6 +37,8 @@ enum class Marker : uint8_t {
   SECTION_CONSTRAINTS = 0x25,
   SECTION_DELTA = 0x26,
   SECTION_EPOCH_HISTORY = 0x27,
+  SECTION_EDGE_INDICES = 0x28,
+
   SECTION_OFFSETS = 0x42,
 
   DELTA_VERTEX_CREATE = 0x50,
@@ -60,6 +62,8 @@ enum class Marker : uint8_t {
   DELTA_LABEL_INDEX_STATS_CLEAR = 0x62,
   DELTA_LABEL_PROPERTY_INDEX_STATS_SET = 0x63,
   DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR = 0x64,
+  DELTA_EDGE_TYPE_INDEX_CREATE = 0x65,
+  DELTA_EDGE_TYPE_INDEX_DROP = 0x66,
 
   VALUE_FALSE = 0x00,
   VALUE_TRUE = 0xff,
@@ -85,6 +89,7 @@ static const Marker kMarkersAll[] = {
     Marker::SECTION_CONSTRAINTS,
     Marker::SECTION_DELTA,
     Marker::SECTION_EPOCH_HISTORY,
+    Marker::SECTION_EDGE_INDICES,
     Marker::SECTION_OFFSETS,
     Marker::DELTA_VERTEX_CREATE,
     Marker::DELTA_VERTEX_DELETE,
@@ -103,6 +108,8 @@ static const Marker kMarkersAll[] = {
     Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR,
     Marker::DELTA_LABEL_PROPERTY_INDEX_CREATE,
     Marker::DELTA_LABEL_PROPERTY_INDEX_DROP,
+    Marker::DELTA_EDGE_TYPE_INDEX_CREATE,
+    Marker::DELTA_EDGE_TYPE_INDEX_DROP,
     Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE,
     Marker::DELTA_EXISTENCE_CONSTRAINT_DROP,
     Marker::DELTA_UNIQUE_CONSTRAINT_CREATE,
diff --git a/src/storage/v2/durability/metadata.hpp b/src/storage/v2/durability/metadata.hpp
index 42e24e723..c8ee27b2f 100644
--- a/src/storage/v2/durability/metadata.hpp
+++ b/src/storage/v2/durability/metadata.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -43,6 +43,7 @@ struct RecoveredIndicesAndConstraints {
     std::vector<std::pair<LabelId, PropertyId>> label_property;
     std::vector<std::pair<LabelId, LabelIndexStats>> label_stats;
     std::vector<std::pair<LabelId, std::pair<PropertyId, LabelPropertyIndexStats>>> label_property_stats;
+    std::vector<EdgeTypeId> edge;
   } indices;
 
   struct ConstraintsMetadata {
diff --git a/src/storage/v2/durability/serialization.cpp b/src/storage/v2/durability/serialization.cpp
index 6b13d9d00..28ba64943 100644
--- a/src/storage/v2/durability/serialization.cpp
+++ b/src/storage/v2/durability/serialization.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -332,6 +332,7 @@ std::optional<PropertyValue> Decoder::ReadPropertyValue() {
     case Marker::SECTION_CONSTRAINTS:
     case Marker::SECTION_DELTA:
     case Marker::SECTION_EPOCH_HISTORY:
+    case Marker::SECTION_EDGE_INDICES:
     case Marker::SECTION_OFFSETS:
     case Marker::DELTA_VERTEX_CREATE:
     case Marker::DELTA_VERTEX_DELETE:
@@ -350,6 +351,8 @@ std::optional<PropertyValue> Decoder::ReadPropertyValue() {
     case Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR:
     case Marker::DELTA_LABEL_PROPERTY_INDEX_CREATE:
     case Marker::DELTA_LABEL_PROPERTY_INDEX_DROP:
+    case Marker::DELTA_EDGE_TYPE_INDEX_CREATE:
+    case Marker::DELTA_EDGE_TYPE_INDEX_DROP:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
     case Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
@@ -435,6 +438,7 @@ bool Decoder::SkipPropertyValue() {
     case Marker::SECTION_CONSTRAINTS:
     case Marker::SECTION_DELTA:
     case Marker::SECTION_EPOCH_HISTORY:
+    case Marker::SECTION_EDGE_INDICES:
     case Marker::SECTION_OFFSETS:
     case Marker::DELTA_VERTEX_CREATE:
     case Marker::DELTA_VERTEX_DELETE:
@@ -453,6 +457,8 @@ bool Decoder::SkipPropertyValue() {
     case Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR:
     case Marker::DELTA_LABEL_PROPERTY_INDEX_CREATE:
     case Marker::DELTA_LABEL_PROPERTY_INDEX_DROP:
+    case Marker::DELTA_EDGE_TYPE_INDEX_CREATE:
+    case Marker::DELTA_EDGE_TYPE_INDEX_DROP:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
     case Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
diff --git a/src/storage/v2/durability/snapshot.cpp b/src/storage/v2/durability/snapshot.cpp
index eee099870..5fea3dfa5 100644
--- a/src/storage/v2/durability/snapshot.cpp
+++ b/src/storage/v2/durability/snapshot.cpp
@@ -153,6 +153,11 @@ SnapshotInfo ReadSnapshotInfo(const std::filesystem::path &path) {
     info.offset_edges = read_offset();
     info.offset_vertices = read_offset();
     info.offset_indices = read_offset();
+    if (*version >= 17) {
+      info.offset_edge_indices = read_offset();
+    } else {
+      info.offset_edge_indices = 0U;
+    }
     info.offset_constraints = read_offset();
     info.offset_mapper = read_offset();
     info.offset_epoch_history = read_offset();
@@ -1379,10 +1384,11 @@ RecoveredSnapshot LoadSnapshotVersion15(const std::filesystem::path &path, utils
   return {info, recovery_info, std::move(indices_constraints)};
 }
 
-RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipList<Vertex> *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, const Config &config) {
+RecoveredSnapshot LoadSnapshotVersion16(const std::filesystem::path &path, utils::SkipList<Vertex> *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,
+                                        const Config &config) {
   RecoveryInfo recovery_info;
   RecoveredIndicesAndConstraints indices_constraints;
 
@@ -1391,13 +1397,7 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
   if (!version) throw RecoveryFailure("Couldn't read snapshot magic and/or version!");
 
   if (!IsVersionSupported(*version)) throw RecoveryFailure(fmt::format("Invalid snapshot version {}", *version));
-  if (*version == 14U) {
-    return LoadSnapshotVersion14(path, vertices, edges, epoch_history, name_id_mapper, edge_count,
-                                 config.salient.items);
-  }
-  if (*version == 15U) {
-    return LoadSnapshotVersion15(path, vertices, edges, epoch_history, name_id_mapper, edge_count, config);
-  }
+  if (*version != 16U) throw RecoveryFailure(fmt::format("Expected snapshot version is 16, but got {}", *version));
 
   // Cleanup of loaded data in case of failure.
   bool success = false;
@@ -1727,6 +1727,380 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipLis
   return {info, recovery_info, std::move(indices_constraints)};
 }
 
+RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, utils::SkipList<Vertex> *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, const Config &config) {
+  RecoveryInfo recovery_info;
+  RecoveredIndicesAndConstraints indices_constraints;
+
+  Decoder snapshot;
+  const auto version = snapshot.Initialize(path, kSnapshotMagic);
+  if (!version) throw RecoveryFailure("Couldn't read snapshot magic and/or version!");
+
+  if (!IsVersionSupported(*version)) throw RecoveryFailure(fmt::format("Invalid snapshot version {}", *version));
+  if (*version == 14U) {
+    return LoadSnapshotVersion14(path, vertices, edges, epoch_history, name_id_mapper, edge_count,
+                                 config.salient.items);
+  }
+  if (*version == 15U) {
+    return LoadSnapshotVersion15(path, vertices, edges, epoch_history, name_id_mapper, edge_count, config);
+  }
+  if (*version == 16U) {
+    return LoadSnapshotVersion16(path, vertices, edges, epoch_history, name_id_mapper, edge_count, config);
+  }
+
+  // Cleanup of loaded data in case of failure.
+  bool success = false;
+  utils::OnScopeExit cleanup([&] {
+    if (!success) {
+      edges->clear();
+      vertices->clear();
+      epoch_history->clear();
+    }
+  });
+
+  // Read snapshot info.
+  const auto info = ReadSnapshotInfo(path);
+  spdlog::info("Recovering {} vertices and {} edges.", info.vertices_count, info.edges_count);
+  // Check for edges.
+  bool snapshot_has_edges = info.offset_edges != 0;
+
+  // Recover mapper.
+  std::unordered_map<uint64_t, uint64_t> snapshot_id_map;
+  {
+    spdlog::info("Recovering mapper metadata.");
+    if (!snapshot.SetPosition(info.offset_mapper)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    auto marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_MAPPER) throw RecoveryFailure("Failed to read section mapper!");
+
+    auto size = snapshot.ReadUint();
+    if (!size) throw RecoveryFailure("Failed to read name-id mapper size!");
+
+    for (uint64_t i = 0; i < *size; ++i) {
+      auto id = snapshot.ReadUint();
+      if (!id) throw RecoveryFailure("Failed to read id for name-id mapper!");
+      auto name = snapshot.ReadString();
+      if (!name) throw RecoveryFailure("Failed to read name for name-id mapper!");
+      auto my_id = name_id_mapper->NameToId(*name);
+      snapshot_id_map.emplace(*id, my_id);
+      SPDLOG_TRACE("Mapping \"{}\"from snapshot id {} to actual id {}.", *name, *id, my_id);
+    }
+  }
+  auto get_label_from_id = [&snapshot_id_map](uint64_t label_id) {
+    auto it = snapshot_id_map.find(label_id);
+    if (it == snapshot_id_map.end()) throw RecoveryFailure("Couldn't find label id in snapshot_id_map!");
+    return LabelId::FromUint(it->second);
+  };
+  auto get_property_from_id = [&snapshot_id_map](uint64_t property_id) {
+    auto it = snapshot_id_map.find(property_id);
+    if (it == snapshot_id_map.end()) throw RecoveryFailure("Couldn't find property id in snapshot_id_map!");
+    return PropertyId::FromUint(it->second);
+  };
+  auto get_edge_type_from_id = [&snapshot_id_map](uint64_t edge_type_id) {
+    auto it = snapshot_id_map.find(edge_type_id);
+    if (it == snapshot_id_map.end()) throw RecoveryFailure("Couldn't find edge type id in snapshot_id_map!");
+    return EdgeTypeId::FromUint(it->second);
+  };
+
+  // Reset current edge count.
+  edge_count->store(0, std::memory_order_release);
+
+  {
+    spdlog::info("Recovering edges.");
+    // Recover edges.
+    if (snapshot_has_edges) {
+      // We don't need to check whether we store properties on edge or not, because `LoadPartialEdges` will always
+      // iterate over the edges in the snapshot (if they exist) and the current configuration of properties on edge only
+      // affect what it does:
+      // 1. If properties are allowed on edges, then it loads the edges.
+      // 2. If properties are not allowed on edges, then it checks that none of the edges have any properties.
+      if (!snapshot.SetPosition(info.offset_edge_batches)) {
+        throw RecoveryFailure("Couldn't read data from snapshot!");
+      }
+      const auto edge_batches = ReadBatchInfos(snapshot);
+
+      RecoverOnMultipleThreads(
+          config.durability.recovery_thread_count,
+          [path, edges, items = config.salient.items, &get_property_from_id](const size_t /*batch_index*/,
+                                                                             const BatchInfo &batch) {
+            LoadPartialEdges(path, *edges, batch.offset, batch.count, items, get_property_from_id);
+          },
+          edge_batches);
+    }
+    spdlog::info("Edges are recovered.");
+
+    // Recover vertices (labels and properties).
+    spdlog::info("Recovering vertices.", info.vertices_count);
+    uint64_t last_vertex_gid{0};
+
+    if (!snapshot.SetPosition(info.offset_vertex_batches)) {
+      throw RecoveryFailure("Couldn't read data from snapshot!");
+    }
+
+    const auto vertex_batches = ReadBatchInfos(snapshot);
+    RecoverOnMultipleThreads(
+        config.durability.recovery_thread_count,
+        [path, vertices, &vertex_batches, &get_label_from_id, &get_property_from_id, &last_vertex_gid](
+            const size_t batch_index, const BatchInfo &batch) {
+          const auto last_vertex_gid_in_batch =
+              LoadPartialVertices(path, *vertices, batch.offset, batch.count, get_label_from_id, get_property_from_id);
+          if (batch_index == vertex_batches.size() - 1) {
+            last_vertex_gid = last_vertex_gid_in_batch;
+          }
+        },
+        vertex_batches);
+
+    spdlog::info("Vertices are recovered.");
+
+    // Recover vertices (in/out edges).
+    spdlog::info("Recover connectivity.");
+    recovery_info.vertex_batches.reserve(vertex_batches.size());
+    for (const auto batch : vertex_batches) {
+      recovery_info.vertex_batches.emplace_back(Gid::FromUint(0), batch.count);
+    }
+    std::atomic<uint64_t> highest_edge_gid{0};
+
+    RecoverOnMultipleThreads(
+        config.durability.recovery_thread_count,
+        [path, vertices, edges, edge_count, items = config.salient.items, snapshot_has_edges, &get_edge_type_from_id,
+         &highest_edge_gid, &recovery_info](const size_t batch_index, const BatchInfo &batch) {
+          const auto result = LoadPartialConnectivity(path, *vertices, *edges, batch.offset, batch.count, items,
+                                                      snapshot_has_edges, get_edge_type_from_id);
+          edge_count->fetch_add(result.edge_count);
+          auto known_highest_edge_gid = highest_edge_gid.load();
+          while (known_highest_edge_gid < result.highest_edge_id) {
+            highest_edge_gid.compare_exchange_weak(known_highest_edge_gid, result.highest_edge_id);
+          }
+          recovery_info.vertex_batches[batch_index].first = result.first_vertex_gid;
+        },
+        vertex_batches);
+
+    spdlog::info("Connectivity is recovered.");
+
+    // Set initial values for edge/vertex ID generators.
+    recovery_info.next_edge_id = highest_edge_gid + 1;
+    recovery_info.next_vertex_id = last_vertex_gid + 1;
+  }
+
+  // Recover indices.
+  {
+    spdlog::info("Recovering metadata of indices.");
+    if (!snapshot.SetPosition(info.offset_indices)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    auto marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_INDICES) throw RecoveryFailure("Couldn't read section indices!");
+
+    // Recover label indices.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of label indices");
+      spdlog::info("Recovering metadata of {} label indices.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label of label index!");
+        AddRecoveredIndexConstraint(&indices_constraints.indices.label, get_label_from_id(*label),
+                                    "The label index already exists!");
+        SPDLOG_TRACE("Recovered metadata of label index for :{}", name_id_mapper->IdToName(snapshot_id_map.at(*label)));
+      }
+      spdlog::info("Metadata of label indices are recovered.");
+    }
+
+    // Recover label indices statistics.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of entries for label index statistics!");
+      spdlog::info("Recovering metadata of {} label indices statistics.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        const auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label while recovering label index statistics!");
+        const auto count = snapshot.ReadUint();
+        if (!count) throw RecoveryFailure("Couldn't read count for label index statistics!");
+        const auto avg_degree = snapshot.ReadDouble();
+        if (!avg_degree) throw RecoveryFailure("Couldn't read average degree for label index statistics");
+        const auto label_id = get_label_from_id(*label);
+        indices_constraints.indices.label_stats.emplace_back(label_id, LabelIndexStats{*count, *avg_degree});
+        SPDLOG_TRACE("Recovered metadata of label index statistics for :{}",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)));
+      }
+      spdlog::info("Metadata of label indices are recovered.");
+    }
+
+    // Recover label+property indices.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't recover the number of label property indices!");
+      spdlog::info("Recovering metadata of {} label+property indices.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label for label property index!");
+        auto property = snapshot.ReadUint();
+        if (!property) throw RecoveryFailure("Couldn't read property for label property index");
+        AddRecoveredIndexConstraint(&indices_constraints.indices.label_property,
+                                    {get_label_from_id(*label), get_property_from_id(*property)},
+                                    "The label+property index already exists!");
+        SPDLOG_TRACE("Recovered metadata of label+property index for :{}({})",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)),
+                     name_id_mapper->IdToName(snapshot_id_map.at(*property)));
+      }
+      spdlog::info("Metadata of label+property indices are recovered.");
+    }
+
+    // Recover label+property indices statistics.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't recover the number of entries for label property statistics!");
+      spdlog::info("Recovering metadata of {} label+property indices statistics.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        const auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label for label property index statistics!");
+        const auto property = snapshot.ReadUint();
+        if (!property) throw RecoveryFailure("Couldn't read property for label property index statistics!");
+        const auto count = snapshot.ReadUint();
+        if (!count) throw RecoveryFailure("Couldn't read count for label property index statistics!!");
+        const auto distinct_values_count = snapshot.ReadUint();
+        if (!distinct_values_count)
+          throw RecoveryFailure("Couldn't read distinct values count for label property index statistics!");
+        const auto statistic = snapshot.ReadDouble();
+        if (!statistic) throw RecoveryFailure("Couldn't read statistics value for label-property index statistics!");
+        const auto avg_group_size = snapshot.ReadDouble();
+        if (!avg_group_size)
+          throw RecoveryFailure("Couldn't read average group size for label property index statistics!");
+        const auto avg_degree = snapshot.ReadDouble();
+        if (!avg_degree) throw RecoveryFailure("Couldn't read average degree for label property index statistics!");
+        const auto label_id = get_label_from_id(*label);
+        const auto property_id = get_property_from_id(*property);
+        indices_constraints.indices.label_property_stats.emplace_back(
+            label_id, std::make_pair(property_id, LabelPropertyIndexStats{*count, *distinct_values_count, *statistic,
+                                                                          *avg_group_size, *avg_degree}));
+        SPDLOG_TRACE("Recovered metadata of label+property index statistics for :{}({})",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)),
+                     name_id_mapper->IdToName(snapshot_id_map.at(*property)));
+      }
+      spdlog::info("Metadata of label+property indices are recovered.");
+    }
+
+    // Recover edge-type indices.
+    spdlog::info("Recovering metadata of indices.");
+    if (!snapshot.SetPosition(info.offset_edge_indices)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_EDGE_INDICES)
+      throw RecoveryFailure("Couldn't read section edge-indices!");
+
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of edge-type indices");
+      spdlog::info("Recovering metadata of {} edge-type indices.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto edge_type = snapshot.ReadUint();
+        if (!edge_type) throw RecoveryFailure("Couldn't read edge-type of edge-type index!");
+        AddRecoveredIndexConstraint(&indices_constraints.indices.edge, get_edge_type_from_id(*edge_type),
+                                    "The edge-type index already exists!");
+        SPDLOG_TRACE("Recovered metadata of edge-type index for :{}",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*edge_type)));
+      }
+      spdlog::info("Metadata of edge-type indices are recovered.");
+    }
+
+    spdlog::info("Metadata of indices are recovered.");
+  }
+
+  // Recover constraints.
+  {
+    spdlog::info("Recovering metadata of constraints.");
+    if (!snapshot.SetPosition(info.offset_constraints)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    auto marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_CONSTRAINTS)
+      throw RecoveryFailure("Couldn't read section constraints marker!");
+
+    // Recover existence constraints.
+    {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of existence constraints!");
+      spdlog::info("Recovering metadata of {} existence constraints.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label of existence constraints!");
+        auto property = snapshot.ReadUint();
+        if (!property) throw RecoveryFailure("Couldn't read property of existence constraints!");
+        AddRecoveredIndexConstraint(&indices_constraints.constraints.existence,
+                                    {get_label_from_id(*label), get_property_from_id(*property)},
+                                    "The existence constraint already exists!");
+        SPDLOG_TRACE("Recovered metadata of existence constraint for :{}({})",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)),
+                     name_id_mapper->IdToName(snapshot_id_map.at(*property)));
+      }
+      spdlog::info("Metadata of existence constraints are recovered.");
+    }
+
+    // Recover unique constraints.
+    // Snapshot version should be checked since unique constraints were
+    // implemented in later versions of snapshot.
+    if (*version >= kUniqueConstraintVersion) {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Couldn't read the number of unique constraints!");
+      spdlog::info("Recovering metadata of {} unique constraints.", *size);
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Couldn't read label of unique constraints!");
+        auto properties_count = snapshot.ReadUint();
+        if (!properties_count) throw RecoveryFailure("Couldn't read the number of properties in unique constraint!");
+        std::set<PropertyId> properties;
+        for (uint64_t j = 0; j < *properties_count; ++j) {
+          auto property = snapshot.ReadUint();
+          if (!property) throw RecoveryFailure("Couldn't read property of unique constraint!");
+          properties.insert(get_property_from_id(*property));
+        }
+        AddRecoveredIndexConstraint(&indices_constraints.constraints.unique, {get_label_from_id(*label), properties},
+                                    "The unique constraint already exists!");
+        SPDLOG_TRACE("Recovered metadata of unique constraints for :{}",
+                     name_id_mapper->IdToName(snapshot_id_map.at(*label)));
+      }
+      spdlog::info("Metadata of unique constraints are recovered.");
+    }
+    spdlog::info("Metadata of constraints are recovered.");
+  }
+
+  spdlog::info("Recovering metadata.");
+  // Recover epoch history
+  {
+    if (!snapshot.SetPosition(info.offset_epoch_history)) throw RecoveryFailure("Couldn't read data from snapshot!");
+
+    const auto marker = snapshot.ReadMarker();
+    if (!marker || *marker != Marker::SECTION_EPOCH_HISTORY)
+      throw RecoveryFailure("Couldn't read section epoch history marker!");
+
+    const auto history_size = snapshot.ReadUint();
+    if (!history_size) {
+      throw RecoveryFailure("Couldn't read history size!");
+    }
+
+    for (int i = 0; i < *history_size; ++i) {
+      auto maybe_epoch_id = snapshot.ReadString();
+      if (!maybe_epoch_id) {
+        throw RecoveryFailure("Couldn't read maybe epoch id!");
+      }
+      const auto maybe_last_commit_timestamp = snapshot.ReadUint();
+      if (!maybe_last_commit_timestamp) {
+        throw RecoveryFailure("Couldn't read maybe last commit timestamp!");
+      }
+      epoch_history->emplace_back(std::move(*maybe_epoch_id), *maybe_last_commit_timestamp);
+    }
+  }
+
+  spdlog::info("Metadata recovered.");
+  // Recover timestamp.
+  recovery_info.next_timestamp = info.start_timestamp + 1;
+
+  // Set success flag (to disable cleanup).
+  success = true;
+
+  return {info, recovery_info, std::move(indices_constraints)};
+}
+
 using OldSnapshotFiles = std::vector<std::pair<uint64_t, std::filesystem::path>>;
 void EnsureNecessaryWalFilesExist(const std::filesystem::path &wal_directory, const std::string &uuid,
                                   OldSnapshotFiles old_snapshot_files, Transaction *transaction,
@@ -1835,6 +2209,7 @@ void CreateSnapshot(Storage *storage, Transaction *transaction, const std::files
   uint64_t offset_edges = 0;
   uint64_t offset_vertices = 0;
   uint64_t offset_indices = 0;
+  uint64_t offset_edge_indices = 0;
   uint64_t offset_constraints = 0;
   uint64_t offset_mapper = 0;
   uint64_t offset_metadata = 0;
@@ -1847,6 +2222,7 @@ void CreateSnapshot(Storage *storage, Transaction *transaction, const std::files
     snapshot.WriteUint(offset_edges);
     snapshot.WriteUint(offset_vertices);
     snapshot.WriteUint(offset_indices);
+    snapshot.WriteUint(offset_edge_indices);
     snapshot.WriteUint(offset_constraints);
     snapshot.WriteUint(offset_mapper);
     snapshot.WriteUint(offset_epoch_history);
@@ -2106,6 +2482,17 @@ void CreateSnapshot(Storage *storage, Transaction *transaction, const std::files
         snapshot.SetPosition(last_pos);
       }
     }
+
+    // Write edge-type indices.
+    offset_edge_indices = snapshot.GetPosition();
+    snapshot.WriteMarker(Marker::SECTION_EDGE_INDICES);
+    {
+      auto edge_type = storage->indices_.edge_type_index_->ListIndices();
+      snapshot.WriteUint(edge_type.size());
+      for (const auto &item : edge_type) {
+        write_mapping(item);
+      }
+    }
   }
 
   // Write constraints.
@@ -2196,6 +2583,7 @@ void CreateSnapshot(Storage *storage, Transaction *transaction, const std::files
     snapshot.WriteUint(offset_edges);
     snapshot.WriteUint(offset_vertices);
     snapshot.WriteUint(offset_indices);
+    snapshot.WriteUint(offset_edge_indices);
     snapshot.WriteUint(offset_constraints);
     snapshot.WriteUint(offset_mapper);
     snapshot.WriteUint(offset_epoch_history);
diff --git a/src/storage/v2/durability/snapshot.hpp b/src/storage/v2/durability/snapshot.hpp
index 4c1aee1ce..b8c224b3f 100644
--- a/src/storage/v2/durability/snapshot.hpp
+++ b/src/storage/v2/durability/snapshot.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -34,6 +34,7 @@ struct SnapshotInfo {
   uint64_t offset_edges;
   uint64_t offset_vertices;
   uint64_t offset_indices;
+  uint64_t offset_edge_indices;
   uint64_t offset_constraints;
   uint64_t offset_mapper;
   uint64_t offset_epoch_history;
diff --git a/src/storage/v2/durability/storage_global_operation.hpp b/src/storage/v2/durability/storage_global_operation.hpp
index a4f1b043a..7dd635e9d 100644
--- a/src/storage/v2/durability/storage_global_operation.hpp
+++ b/src/storage/v2/durability/storage_global_operation.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -23,6 +23,8 @@ enum class StorageMetadataOperation {
   LABEL_PROPERTY_INDEX_DROP,
   LABEL_PROPERTY_INDEX_STATS_SET,
   LABEL_PROPERTY_INDEX_STATS_CLEAR,
+  EDGE_TYPE_INDEX_CREATE,
+  EDGE_TYPE_INDEX_DROP,
   EXISTENCE_CONSTRAINT_CREATE,
   EXISTENCE_CONSTRAINT_DROP,
   UNIQUE_CONSTRAINT_CREATE,
diff --git a/src/storage/v2/durability/version.hpp b/src/storage/v2/durability/version.hpp
index 25eb30904..58ca0364a 100644
--- a/src/storage/v2/durability/version.hpp
+++ b/src/storage/v2/durability/version.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -20,7 +20,7 @@ namespace memgraph::storage::durability {
 // The current version of snapshot and WAL encoding / decoding.
 // IMPORTANT: Please bump this version for every snapshot and/or WAL format
 // change!!!
-const uint64_t kVersion{16};
+const uint64_t kVersion{17};
 
 const uint64_t kOldestSupportedVersion{14};
 const uint64_t kUniqueConstraintVersion{13};
diff --git a/src/storage/v2/durability/wal.cpp b/src/storage/v2/durability/wal.cpp
index 52e916052..5c40ab1c5 100644
--- a/src/storage/v2/durability/wal.cpp
+++ b/src/storage/v2/durability/wal.cpp
@@ -95,6 +95,10 @@ Marker OperationToMarker(StorageMetadataOperation operation) {
       return Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_SET;
     case StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_CLEAR:
       return Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR;
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+      return Marker::DELTA_EDGE_TYPE_INDEX_CREATE;
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_DROP:
+      return Marker::DELTA_EDGE_TYPE_INDEX_DROP;
     case StorageMetadataOperation::EXISTENCE_CONSTRAINT_CREATE:
       return Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE;
     case StorageMetadataOperation::EXISTENCE_CONSTRAINT_DROP:
@@ -172,6 +176,10 @@ WalDeltaData::Type MarkerToWalDeltaDataType(Marker marker) {
       return WalDeltaData::Type::LABEL_PROPERTY_INDEX_STATS_SET;
     case Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR:
       return WalDeltaData::Type::LABEL_PROPERTY_INDEX_STATS_CLEAR;
+    case Marker::DELTA_EDGE_TYPE_INDEX_CREATE:
+      return WalDeltaData::Type::EDGE_INDEX_CREATE;
+    case Marker::DELTA_EDGE_TYPE_INDEX_DROP:
+      return WalDeltaData::Type::EDGE_INDEX_DROP;
     case Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
       return WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE;
     case Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
@@ -198,6 +206,7 @@ WalDeltaData::Type MarkerToWalDeltaDataType(Marker marker) {
     case Marker::SECTION_CONSTRAINTS:
     case Marker::SECTION_DELTA:
     case Marker::SECTION_EPOCH_HISTORY:
+    case Marker::SECTION_EDGE_INDICES:
     case Marker::SECTION_OFFSETS:
     case Marker::VALUE_FALSE:
     case Marker::VALUE_TRUE:
@@ -280,6 +289,7 @@ WalDeltaData ReadSkipWalDeltaData(BaseDecoder *decoder) {
     }
     case WalDeltaData::Type::TRANSACTION_END:
       break;
+    // NOLINTNEXTLINE(bugprone-branch-clone)
     case WalDeltaData::Type::LABEL_INDEX_CREATE:
     case WalDeltaData::Type::LABEL_INDEX_DROP:
     case WalDeltaData::Type::LABEL_INDEX_STATS_CLEAR:
@@ -295,6 +305,17 @@ WalDeltaData ReadSkipWalDeltaData(BaseDecoder *decoder) {
       }
       break;
     }
+    case WalDeltaData::Type::EDGE_INDEX_CREATE:
+    case WalDeltaData::Type::EDGE_INDEX_DROP: {
+      if constexpr (read_data) {
+        auto edge_type = decoder->ReadString();
+        if (!edge_type) throw RecoveryFailure("Invalid WAL data!");
+        delta.operation_edge_type.edge_type = std::move(*edge_type);
+      } else {
+        if (!decoder->SkipString()) throw RecoveryFailure("Invalid WAL data!");
+      }
+      break;
+    }
     case WalDeltaData::Type::LABEL_INDEX_STATS_SET: {
       if constexpr (read_data) {
         auto label = decoder->ReadString();
@@ -522,6 +543,9 @@ bool operator==(const WalDeltaData &a, const WalDeltaData &b) {
     case WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP:
       return a.operation_label_properties.label == b.operation_label_properties.label &&
              a.operation_label_properties.properties == b.operation_label_properties.properties;
+    case WalDeltaData::Type::EDGE_INDEX_CREATE:
+    case WalDeltaData::Type::EDGE_INDEX_DROP:
+      return a.operation_edge_type.edge_type == b.operation_edge_type.edge_type;
   }
 }
 bool operator!=(const WalDeltaData &a, const WalDeltaData &b) { return !(a == b); }
@@ -703,6 +727,37 @@ void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, Storage
       }
       break;
     }
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_DROP: {
+      MG_ASSERT(false, "Invalid function  call!");
+    }
+  }
+}
+
+void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, StorageMetadataOperation operation,
+                     EdgeTypeId edge_type, uint64_t timestamp) {
+  encoder->WriteMarker(Marker::SECTION_DELTA);
+  encoder->WriteUint(timestamp);
+  switch (operation) {
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+    case StorageMetadataOperation::EDGE_TYPE_INDEX_DROP: {
+      encoder->WriteMarker(OperationToMarker(operation));
+      encoder->WriteString(name_id_mapper->IdToName(edge_type.AsUint()));
+      break;
+    }
+    case StorageMetadataOperation::LABEL_INDEX_CREATE:
+    case StorageMetadataOperation::LABEL_INDEX_DROP:
+    case StorageMetadataOperation::LABEL_INDEX_STATS_CLEAR:
+    case StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_CLEAR:
+    case StorageMetadataOperation::LABEL_INDEX_STATS_SET:
+    case StorageMetadataOperation::LABEL_PROPERTY_INDEX_CREATE:
+    case StorageMetadataOperation::LABEL_PROPERTY_INDEX_DROP:
+    case StorageMetadataOperation::EXISTENCE_CONSTRAINT_CREATE:
+    case StorageMetadataOperation::EXISTENCE_CONSTRAINT_DROP:
+    case StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_SET:
+    case StorageMetadataOperation::UNIQUE_CONSTRAINT_CREATE:
+    case StorageMetadataOperation::UNIQUE_CONSTRAINT_DROP:
+      MG_ASSERT(false, "Invalid function call!");
   }
 }
 
@@ -887,6 +942,18 @@ RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConst
                                          "The label index doesn't exist!");
           break;
         }
+        case WalDeltaData::Type::EDGE_INDEX_CREATE: {
+          auto edge_type_id = EdgeTypeId::FromUint(name_id_mapper->NameToId(delta.operation_edge_type.edge_type));
+          AddRecoveredIndexConstraint(&indices_constraints->indices.edge, edge_type_id,
+                                      "The edge-type index already exists!");
+          break;
+        }
+        case WalDeltaData::Type::EDGE_INDEX_DROP: {
+          auto edge_type_id = EdgeTypeId::FromUint(name_id_mapper->NameToId(delta.operation_edge_type.edge_type));
+          RemoveRecoveredIndexConstraint(&indices_constraints->indices.edge, edge_type_id,
+                                         "The edge-type index doesn't exist!");
+          break;
+        }
         case WalDeltaData::Type::LABEL_INDEX_STATS_SET: {
           auto label_id = LabelId::FromUint(name_id_mapper->NameToId(delta.operation_label_stats.label));
           LabelIndexStats stats{};
@@ -1088,6 +1155,11 @@ void WalFile::AppendOperation(StorageMetadataOperation operation, LabelId label,
   UpdateStats(timestamp);
 }
 
+void WalFile::AppendOperation(StorageMetadataOperation operation, EdgeTypeId edge_type, uint64_t timestamp) {
+  EncodeOperation(&wal_, name_id_mapper_, operation, edge_type, timestamp);
+  UpdateStats(timestamp);
+}
+
 void WalFile::Sync() { wal_.Sync(); }
 
 uint64_t WalFile::GetSize() { return wal_.GetSize(); }
diff --git a/src/storage/v2/durability/wal.hpp b/src/storage/v2/durability/wal.hpp
index 20d88b040..516487e0d 100644
--- a/src/storage/v2/durability/wal.hpp
+++ b/src/storage/v2/durability/wal.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -67,6 +67,8 @@ struct WalDeltaData {
     LABEL_PROPERTY_INDEX_DROP,
     LABEL_PROPERTY_INDEX_STATS_SET,
     LABEL_PROPERTY_INDEX_STATS_CLEAR,
+    EDGE_INDEX_CREATE,
+    EDGE_INDEX_DROP,
     EXISTENCE_CONSTRAINT_CREATE,
     EXISTENCE_CONSTRAINT_DROP,
     UNIQUE_CONSTRAINT_CREATE,
@@ -111,6 +113,10 @@ struct WalDeltaData {
     std::set<std::string, std::less<>> properties;
   } operation_label_properties;
 
+  struct {
+    std::string edge_type;
+  } operation_edge_type;
+
   struct {
     std::string label;
     std::string stats;
@@ -155,6 +161,8 @@ constexpr bool IsWalDeltaDataTypeTransactionEndVersion15(const WalDeltaData::Typ
     case WalDeltaData::Type::LABEL_PROPERTY_INDEX_DROP:
     case WalDeltaData::Type::LABEL_PROPERTY_INDEX_STATS_SET:
     case WalDeltaData::Type::LABEL_PROPERTY_INDEX_STATS_CLEAR:
+    case WalDeltaData::Type::EDGE_INDEX_CREATE:
+    case WalDeltaData::Type::EDGE_INDEX_DROP:
     case WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE:
     case WalDeltaData::Type::EXISTENCE_CONSTRAINT_DROP:
     case WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE:
@@ -164,7 +172,7 @@ constexpr bool IsWalDeltaDataTypeTransactionEndVersion15(const WalDeltaData::Typ
 }
 
 constexpr bool IsWalDeltaDataTypeTransactionEnd(const WalDeltaData::Type type, const uint64_t version = kVersion) {
-  if (version < 16U) {
+  if (version < 17U) {
     return IsWalDeltaDataTypeTransactionEndVersion15(type);
   }
   // All deltas are now handled in a transactional scope
@@ -208,6 +216,9 @@ void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, Storage
                      LabelId label, const std::set<PropertyId> &properties, const LabelIndexStats &stats,
                      const LabelPropertyIndexStats &property_stats, uint64_t timestamp);
 
+void EncodeOperation(BaseEncoder *encoder, NameIdMapper *name_id_mapper, StorageMetadataOperation operation,
+                     EdgeTypeId edge_type, uint64_t timestamp);
+
 /// Function used to load the WAL data into the storage.
 /// @throw RecoveryFailure
 RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConstraints *indices_constraints,
@@ -240,6 +251,8 @@ class WalFile {
   void AppendOperation(StorageMetadataOperation operation, LabelId label, const std::set<PropertyId> &properties,
                        const LabelIndexStats &stats, const LabelPropertyIndexStats &property_stats, uint64_t timestamp);
 
+  void AppendOperation(StorageMetadataOperation operation, EdgeTypeId edge_type, uint64_t timestamp);
+
   void Sync();
 
   uint64_t GetSize();
diff --git a/src/storage/v2/edges_iterable.cpp b/src/storage/v2/edges_iterable.cpp
new file mode 100644
index 000000000..6acae34e3
--- /dev/null
+++ b/src/storage/v2/edges_iterable.cpp
@@ -0,0 +1,149 @@
+// Copyright 2024 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/v2/edges_iterable.hpp"
+
+namespace memgraph::storage {
+
+EdgesIterable::EdgesIterable(InMemoryEdgeTypeIndex::Iterable edges) : type_(Type::BY_EDGE_TYPE_IN_MEMORY) {
+  new (&in_memory_edges_by_edge_type_) InMemoryEdgeTypeIndex::Iterable(std::move(edges));
+}
+
+EdgesIterable::EdgesIterable(EdgesIterable &&other) noexcept : type_(other.type_) {
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          InMemoryEdgeTypeIndex::Iterable(std::move(other.in_memory_edges_by_edge_type_));
+      break;
+  }
+}
+
+EdgesIterable &EdgesIterable::operator=(EdgesIterable &&other) noexcept {
+  Destroy();
+  type_ = other.type_;
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          InMemoryEdgeTypeIndex::Iterable(std::move(other.in_memory_edges_by_edge_type_));
+      break;
+  }
+  return *this;
+}
+
+EdgesIterable::~EdgesIterable() { Destroy(); }
+
+void EdgesIterable::Destroy() noexcept {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      in_memory_edges_by_edge_type_.InMemoryEdgeTypeIndex::Iterable::~Iterable();
+      break;
+  }
+}
+
+EdgesIterable::Iterator EdgesIterable::begin() {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      return Iterator(in_memory_edges_by_edge_type_.begin());
+  }
+}
+
+EdgesIterable::Iterator EdgesIterable::end() {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      return Iterator(in_memory_edges_by_edge_type_.end());
+  }
+}
+
+EdgesIterable::Iterator::Iterator(InMemoryEdgeTypeIndex::Iterable::Iterator it) : type_(Type::BY_EDGE_TYPE_IN_MEMORY) {
+  // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
+  new (&in_memory_edges_by_edge_type_) InMemoryEdgeTypeIndex::Iterable::Iterator(std::move(it));
+}
+
+EdgesIterable::Iterator::Iterator(const EdgesIterable::Iterator &other) : type_(other.type_) {
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          InMemoryEdgeTypeIndex::Iterable::Iterator(other.in_memory_edges_by_edge_type_);
+      break;
+  }
+}
+
+// NOLINTNEXTLINE(cert-oop54-cpp)
+EdgesIterable::Iterator &EdgesIterable::Iterator::operator=(const EdgesIterable::Iterator &other) {
+  Destroy();
+  type_ = other.type_;
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          InMemoryEdgeTypeIndex::Iterable::Iterator(other.in_memory_edges_by_edge_type_);
+      break;
+  }
+  return *this;
+}
+
+EdgesIterable::Iterator::Iterator(EdgesIterable::Iterator &&other) noexcept : type_(other.type_) {
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
+          InMemoryEdgeTypeIndex::Iterable::Iterator(std::move(other.in_memory_edges_by_edge_type_));
+      break;
+  }
+}
+
+EdgesIterable::Iterator &EdgesIterable::Iterator::operator=(EdgesIterable::Iterator &&other) noexcept {
+  Destroy();
+  type_ = other.type_;
+  switch (other.type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      new (&in_memory_edges_by_edge_type_)
+          // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
+          InMemoryEdgeTypeIndex::Iterable::Iterator(std::move(other.in_memory_edges_by_edge_type_));
+      break;
+  }
+  return *this;
+}
+
+EdgesIterable::Iterator::~Iterator() { Destroy(); }
+
+void EdgesIterable::Iterator::Destroy() noexcept {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      in_memory_edges_by_edge_type_.InMemoryEdgeTypeIndex::Iterable::Iterator::~Iterator();
+      break;
+  }
+}
+
+EdgeAccessor const &EdgesIterable::Iterator::operator*() const {
+  switch (type_) {
+    ;
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      return *in_memory_edges_by_edge_type_;
+  }
+}
+
+EdgesIterable::Iterator &EdgesIterable::Iterator::operator++() {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      ++in_memory_edges_by_edge_type_;
+      break;
+  }
+  return *this;
+}
+
+bool EdgesIterable::Iterator::operator==(const Iterator &other) const {
+  switch (type_) {
+    case Type::BY_EDGE_TYPE_IN_MEMORY:
+      return in_memory_edges_by_edge_type_ == other.in_memory_edges_by_edge_type_;
+  }
+}
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/edges_iterable.hpp b/src/storage/v2/edges_iterable.hpp
new file mode 100644
index 000000000..9c9326705
--- /dev/null
+++ b/src/storage/v2/edges_iterable.hpp
@@ -0,0 +1,73 @@
+// Copyright 2024 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/v2/all_vertices_iterable.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
+
+namespace memgraph::storage {
+
+class InMemoryEdgeTypeIndex;
+
+class EdgesIterable final {
+  enum class Type { BY_EDGE_TYPE_IN_MEMORY };
+
+  Type type_;
+  union {
+    InMemoryEdgeTypeIndex::Iterable in_memory_edges_by_edge_type_;
+  };
+
+  void Destroy() noexcept;
+
+ public:
+  explicit EdgesIterable(InMemoryEdgeTypeIndex::Iterable);
+
+  EdgesIterable(const EdgesIterable &) = delete;
+  EdgesIterable &operator=(const EdgesIterable &) = delete;
+
+  EdgesIterable(EdgesIterable &&) noexcept;
+  EdgesIterable &operator=(EdgesIterable &&) noexcept;
+
+  ~EdgesIterable();
+
+  class Iterator final {
+    Type type_;
+    union {
+      InMemoryEdgeTypeIndex::Iterable::Iterator in_memory_edges_by_edge_type_;
+    };
+
+    void Destroy() noexcept;
+
+   public:
+    explicit Iterator(InMemoryEdgeTypeIndex::Iterable::Iterator);
+
+    Iterator(const Iterator &);
+    Iterator &operator=(const Iterator &);
+
+    Iterator(Iterator &&) noexcept;
+    Iterator &operator=(Iterator &&) noexcept;
+
+    ~Iterator();
+
+    EdgeAccessor const &operator*() const;
+
+    Iterator &operator++();
+
+    bool operator==(const Iterator &other) const;
+    bool operator!=(const Iterator &other) const { return !(*this == other); }
+  };
+
+  Iterator begin();
+  Iterator end();
+};
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/indices/edge_type_index.hpp b/src/storage/v2/indices/edge_type_index.hpp
new file mode 100644
index 000000000..788ccb225
--- /dev/null
+++ b/src/storage/v2/indices/edge_type_index.hpp
@@ -0,0 +1,46 @@
+// Copyright 2024 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 <vector>
+
+#include "storage/v2/transaction.hpp"
+
+namespace memgraph::storage {
+
+class EdgeTypeIndex {
+ public:
+  EdgeTypeIndex() = default;
+
+  EdgeTypeIndex(const EdgeTypeIndex &) = delete;
+  EdgeTypeIndex(EdgeTypeIndex &&) = delete;
+  EdgeTypeIndex &operator=(const EdgeTypeIndex &) = delete;
+  EdgeTypeIndex &operator=(EdgeTypeIndex &&) = delete;
+
+  virtual ~EdgeTypeIndex() = default;
+
+  virtual bool DropIndex(EdgeTypeId edge_type) = 0;
+
+  virtual bool IndexExists(EdgeTypeId edge_type) const = 0;
+
+  virtual std::vector<EdgeTypeId> ListIndices() const = 0;
+
+  virtual uint64_t ApproximateEdgeCount(EdgeTypeId edge_type) const = 0;
+
+  virtual void UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                                    const Transaction &tx) = 0;
+
+  virtual void UpdateOnEdgeModification(Vertex *old_from, Vertex *old_to, Vertex *new_from, Vertex *new_to,
+                                        EdgeRef edge_ref, EdgeTypeId edge_type, const Transaction &tx) = 0;
+};
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/indices/indices.cpp b/src/storage/v2/indices/indices.cpp
index c86ec8442..6068f888f 100644
--- a/src/storage/v2/indices/indices.cpp
+++ b/src/storage/v2/indices/indices.cpp
@@ -10,8 +10,10 @@
 // licenses/APL.txt.
 
 #include "storage/v2/indices/indices.hpp"
+#include "storage/v2/disk/edge_type_index.hpp"
 #include "storage/v2/disk/label_index.hpp"
 #include "storage/v2/disk/label_property_index.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
 #include "storage/v2/inmemory/label_index.hpp"
 #include "storage/v2/inmemory/label_property_index.hpp"
 
@@ -35,6 +37,8 @@ void Indices::AbortEntries(LabelId label, std::span<std::pair<PropertyValue, Ver
 void Indices::RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp, std::stop_token token) const {
   static_cast<InMemoryLabelIndex *>(label_index_.get())->RemoveObsoleteEntries(oldest_active_start_timestamp, token);
   static_cast<InMemoryLabelPropertyIndex *>(label_property_index_.get())
+      ->RemoveObsoleteEntries(oldest_active_start_timestamp, token);
+  static_cast<InMemoryEdgeTypeIndex *>(edge_type_index_.get())
       ->RemoveObsoleteEntries(oldest_active_start_timestamp, std::move(token));
 }
 
@@ -53,14 +57,21 @@ void Indices::UpdateOnSetProperty(PropertyId property, const PropertyValue &valu
   label_property_index_->UpdateOnSetProperty(property, value, vertex, tx);
 }
 
+void Indices::UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                                   const Transaction &tx) const {
+  edge_type_index_->UpdateOnEdgeCreation(from, to, edge_ref, edge_type, tx);
+}
+
 Indices::Indices(const Config &config, StorageMode storage_mode) {
   std::invoke([this, config, storage_mode]() {
     if (storage_mode == StorageMode::IN_MEMORY_TRANSACTIONAL || storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
       label_index_ = std::make_unique<InMemoryLabelIndex>();
       label_property_index_ = std::make_unique<InMemoryLabelPropertyIndex>();
+      edge_type_index_ = std::make_unique<InMemoryEdgeTypeIndex>();
     } else {
       label_index_ = std::make_unique<DiskLabelIndex>(config);
       label_property_index_ = std::make_unique<DiskLabelPropertyIndex>(config);
+      edge_type_index_ = std::make_unique<DiskEdgeTypeIndex>();
     }
   });
 }
diff --git a/src/storage/v2/indices/indices.hpp b/src/storage/v2/indices/indices.hpp
index d95187bbb..40cff577f 100644
--- a/src/storage/v2/indices/indices.hpp
+++ b/src/storage/v2/indices/indices.hpp
@@ -15,6 +15,7 @@
 #include <span>
 
 #include "storage/v2/id_types.hpp"
+#include "storage/v2/indices/edge_type_index.hpp"
 #include "storage/v2/indices/label_index.hpp"
 #include "storage/v2/indices/label_property_index.hpp"
 #include "storage/v2/storage_mode.hpp"
@@ -64,8 +65,12 @@ struct Indices {
   void UpdateOnSetProperty(PropertyId property, const PropertyValue &value, Vertex *vertex,
                            const Transaction &tx) const;
 
+  void UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                            const Transaction &tx) const;
+
   std::unique_ptr<LabelIndex> label_index_;
   std::unique_ptr<LabelPropertyIndex> label_property_index_;
+  std::unique_ptr<EdgeTypeIndex> edge_type_index_;
 };
 
 }  // namespace memgraph::storage
diff --git a/src/storage/v2/inmemory/edge_type_index.cpp b/src/storage/v2/inmemory/edge_type_index.cpp
new file mode 100644
index 000000000..e439628b4
--- /dev/null
+++ b/src/storage/v2/inmemory/edge_type_index.cpp
@@ -0,0 +1,318 @@
+// Copyright 2024 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/v2/inmemory/edge_type_index.hpp"
+
+#include "storage/v2/constraints/constraints.hpp"
+#include "storage/v2/indices/indices_utils.hpp"
+#include "utils/counter.hpp"
+
+namespace {
+
+using Delta = memgraph::storage::Delta;
+using Vertex = memgraph::storage::Vertex;
+using Edge = memgraph::storage::Edge;
+using EdgeRef = memgraph::storage::EdgeRef;
+using EdgeTypeId = memgraph::storage::EdgeTypeId;
+using Transaction = memgraph::storage::Transaction;
+using View = memgraph::storage::View;
+
+bool IsIndexEntryVisible(Edge *edge, const Transaction *transaction, View view) {
+  bool exists = true;
+  bool deleted = true;
+  Delta *delta = nullptr;
+  {
+    auto guard = std::shared_lock{edge->lock};
+    deleted = edge->deleted;
+    delta = edge->delta;
+  }
+  ApplyDeltasForRead(transaction, delta, view, [&](const Delta &delta) {
+    switch (delta.action) {
+      case Delta::Action::ADD_LABEL:
+      case Delta::Action::REMOVE_LABEL:
+      case Delta::Action::SET_PROPERTY:
+      case Delta::Action::ADD_IN_EDGE:
+      case Delta::Action::ADD_OUT_EDGE:
+      case Delta::Action::REMOVE_IN_EDGE:
+      case Delta::Action::REMOVE_OUT_EDGE:
+        break;
+      case Delta::Action::RECREATE_OBJECT: {
+        deleted = false;
+        break;
+      }
+      case Delta::Action::DELETE_DESERIALIZED_OBJECT:
+      case Delta::Action::DELETE_OBJECT: {
+        exists = false;
+        break;
+      }
+    }
+  });
+  return exists && !deleted;
+}
+
+using ReturnType = std::optional<std::tuple<EdgeTypeId, Vertex *, EdgeRef>>;
+ReturnType VertexDeletedConnectedEdges(Vertex *vertex, Edge *edge, const Transaction *transaction, View view) {
+  ReturnType link;
+  Delta *delta = nullptr;
+  {
+    auto guard = std::shared_lock{vertex->lock};
+    delta = vertex->delta;
+  }
+  ApplyDeltasForRead(transaction, delta, view, [&](const Delta &delta) {
+    switch (delta.action) {
+      case Delta::Action::ADD_LABEL:
+      case Delta::Action::REMOVE_LABEL:
+      case Delta::Action::SET_PROPERTY:
+        break;
+      case Delta::Action::ADD_IN_EDGE: {
+        if (edge == delta.vertex_edge.edge.ptr) {
+          link = {delta.vertex_edge.edge_type, delta.vertex_edge.vertex, delta.vertex_edge.edge};
+          auto it = std::find(vertex->in_edges.begin(), vertex->in_edges.end(), link);
+          MG_ASSERT(it == vertex->in_edges.end(), "Invalid database state!");
+          break;
+        }
+      }
+      case Delta::Action::ADD_OUT_EDGE: {
+        if (edge == delta.vertex_edge.edge.ptr) {
+          link = {delta.vertex_edge.edge_type, delta.vertex_edge.vertex, delta.vertex_edge.edge};
+          auto it = std::find(vertex->out_edges.begin(), vertex->out_edges.end(), link);
+          MG_ASSERT(it == vertex->out_edges.end(), "Invalid database state!");
+          break;
+        }
+      }
+      case Delta::Action::REMOVE_IN_EDGE:
+      case Delta::Action::REMOVE_OUT_EDGE:
+      case Delta::Action::RECREATE_OBJECT:
+      case Delta::Action::DELETE_DESERIALIZED_OBJECT:
+      case Delta::Action::DELETE_OBJECT:
+        break;
+    }
+  });
+  return link;
+}
+
+}  // namespace
+
+namespace memgraph::storage {
+
+bool InMemoryEdgeTypeIndex::CreateIndex(EdgeTypeId edge_type, utils::SkipList<Vertex>::Accessor vertices) {
+  auto [it, emplaced] = index_.try_emplace(edge_type);
+  if (!emplaced) {
+    return false;
+  }
+
+  utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
+  try {
+    auto edge_acc = it->second.access();
+    for (auto &from_vertex : vertices) {
+      if (from_vertex.deleted) {
+        continue;
+      }
+
+      for (auto &edge : from_vertex.out_edges) {
+        const auto type = std::get<kEdgeTypeIdPos>(edge);
+        if (type == edge_type) {
+          auto *to_vertex = std::get<kVertexPos>(edge);
+          if (to_vertex->deleted) {
+            continue;
+          }
+          edge_acc.insert({&from_vertex, to_vertex, std::get<kEdgeRefPos>(edge).ptr, 0});
+        }
+      }
+    }
+  } catch (const utils::OutOfMemoryException &) {
+    utils::MemoryTracker::OutOfMemoryExceptionBlocker oom_exception_blocker;
+    index_.erase(it);
+    throw;
+  }
+
+  return true;
+}
+
+bool InMemoryEdgeTypeIndex::DropIndex(EdgeTypeId edge_type) { return index_.erase(edge_type) > 0; }
+
+bool InMemoryEdgeTypeIndex::IndexExists(EdgeTypeId edge_type) const { return index_.find(edge_type) != index_.end(); }
+
+std::vector<EdgeTypeId> InMemoryEdgeTypeIndex::ListIndices() const {
+  std::vector<EdgeTypeId> ret;
+  ret.reserve(index_.size());
+  for (const auto &item : index_) {
+    ret.push_back(item.first);
+  }
+  return ret;
+}
+
+void InMemoryEdgeTypeIndex::RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp, std::stop_token token) {
+  auto maybe_stop = utils::ResettableCounter<2048>();
+
+  for (auto &label_storage : index_) {
+    if (token.stop_requested()) return;
+
+    auto edges_acc = label_storage.second.access();
+    for (auto it = edges_acc.begin(); it != edges_acc.end();) {
+      if (maybe_stop() && token.stop_requested()) return;
+
+      auto next_it = it;
+      ++next_it;
+
+      if (it->timestamp >= oldest_active_start_timestamp) {
+        it = next_it;
+        continue;
+      }
+
+      if (next_it != edges_acc.end() || it->from_vertex->deleted || it->to_vertex->deleted ||
+          !std::ranges::all_of(it->from_vertex->out_edges, [&](const auto &edge) {
+            auto *to_vertex = std::get<InMemoryEdgeTypeIndex::kVertexPos>(edge);
+            return to_vertex != it->to_vertex;
+          })) {
+        edges_acc.remove(*it);
+      }
+
+      it = next_it;
+    }
+  }
+}
+
+uint64_t InMemoryEdgeTypeIndex::ApproximateEdgeCount(EdgeTypeId edge_type) const {
+  if (auto it = index_.find(edge_type); it != index_.end()) {
+    return it->second.size();
+  }
+  return 0;
+}
+
+void InMemoryEdgeTypeIndex::UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                                                 const Transaction &tx) {
+  auto it = index_.find(edge_type);
+  if (it == index_.end()) {
+    return;
+  }
+  auto acc = it->second.access();
+  acc.insert(Entry{from, to, edge_ref.ptr, tx.start_timestamp});
+}
+
+void InMemoryEdgeTypeIndex::UpdateOnEdgeModification(Vertex *old_from, Vertex *old_to, Vertex *new_from, Vertex *new_to,
+                                                     EdgeRef edge_ref, EdgeTypeId edge_type, const Transaction &tx) {
+  auto it = index_.find(edge_type);
+  if (it == index_.end()) {
+    return;
+  }
+  auto acc = it->second.access();
+
+  auto entry_to_update = std::ranges::find_if(acc, [&](const auto &entry) {
+    return entry.from_vertex == old_from && entry.to_vertex == old_to && entry.edge == edge_ref.ptr;
+  });
+
+  acc.remove(Entry{entry_to_update->from_vertex, entry_to_update->to_vertex, entry_to_update->edge,
+                   entry_to_update->timestamp});
+  acc.insert(Entry{new_from, new_to, edge_ref.ptr, tx.start_timestamp});
+}
+
+InMemoryEdgeTypeIndex::Iterable::Iterable(utils::SkipList<Entry>::Accessor index_accessor, EdgeTypeId edge_type,
+                                          View view, Storage *storage, Transaction *transaction)
+    : index_accessor_(std::move(index_accessor)),
+      edge_type_(edge_type),
+      view_(view),
+      storage_(storage),
+      transaction_(transaction) {}
+
+InMemoryEdgeTypeIndex::Iterable::Iterator::Iterator(Iterable *self, utils::SkipList<Entry>::Iterator index_iterator)
+    : self_(self),
+      index_iterator_(index_iterator),
+      current_edge_accessor_(EdgeRef{nullptr}, EdgeTypeId::FromInt(0), nullptr, nullptr, self_->storage_, nullptr),
+      current_edge_(nullptr) {
+  AdvanceUntilValid();
+}
+
+InMemoryEdgeTypeIndex::Iterable::Iterator &InMemoryEdgeTypeIndex::Iterable::Iterator::operator++() {
+  ++index_iterator_;
+  AdvanceUntilValid();
+  return *this;
+}
+
+void InMemoryEdgeTypeIndex::Iterable::Iterator::AdvanceUntilValid() {
+  for (; index_iterator_ != self_->index_accessor_.end(); ++index_iterator_) {
+    auto *from_vertex = index_iterator_->from_vertex;
+    auto *to_vertex = index_iterator_->to_vertex;
+
+    if (!IsIndexEntryVisible(index_iterator_->edge, self_->transaction_, self_->view_) || from_vertex->deleted ||
+        to_vertex->deleted) {
+      continue;
+    }
+
+    const bool edge_was_deleted = index_iterator_->edge->deleted;
+    auto [edge_ref, edge_type, deleted_from_vertex, deleted_to_vertex] = GetEdgeInfo();
+    MG_ASSERT(edge_ref != EdgeRef(nullptr), "Invalid database state!");
+
+    if (edge_was_deleted) {
+      from_vertex = deleted_from_vertex;
+      to_vertex = deleted_to_vertex;
+    }
+
+    auto accessor = EdgeAccessor{edge_ref, edge_type, from_vertex, to_vertex, self_->storage_, self_->transaction_};
+    if (!accessor.IsVisible(self_->view_)) {
+      continue;
+    }
+
+    current_edge_accessor_ = accessor;
+    current_edge_ = edge_ref;
+    break;
+  }
+}
+
+std::tuple<EdgeRef, EdgeTypeId, Vertex *, Vertex *> InMemoryEdgeTypeIndex::Iterable::Iterator::GetEdgeInfo() {
+  auto *from_vertex = index_iterator_->from_vertex;
+  auto *to_vertex = index_iterator_->to_vertex;
+
+  if (index_iterator_->edge->deleted) {
+    const auto missing_in_edge =
+        VertexDeletedConnectedEdges(from_vertex, index_iterator_->edge, self_->transaction_, self_->view_);
+    const auto missing_out_edge =
+        VertexDeletedConnectedEdges(to_vertex, index_iterator_->edge, self_->transaction_, self_->view_);
+    if (missing_in_edge && missing_out_edge &&
+        std::get<kEdgeRefPos>(*missing_in_edge) == std::get<kEdgeRefPos>(*missing_out_edge)) {
+      return std::make_tuple(std::get<kEdgeRefPos>(*missing_in_edge), std::get<kEdgeTypeIdPos>(*missing_in_edge),
+                             to_vertex, from_vertex);
+    }
+  }
+
+  const auto &from_edges = from_vertex->out_edges;
+  const auto &to_edges = to_vertex->in_edges;
+
+  auto it = std::find_if(from_edges.begin(), from_edges.end(), [&](const auto &from_entry) {
+    const auto &from_edge = std::get<kEdgeRefPos>(from_entry);
+    return std::any_of(to_edges.begin(), to_edges.end(), [&](const auto &to_entry) {
+      const auto &to_edge = std::get<kEdgeRefPos>(to_entry);
+      return index_iterator_->edge->gid == from_edge.ptr->gid && from_edge.ptr->gid == to_edge.ptr->gid;
+    });
+  });
+
+  if (it != from_edges.end()) {
+    const auto &from_edge = std::get<kEdgeRefPos>(*it);
+    return std::make_tuple(from_edge, std::get<kEdgeTypeIdPos>(*it), from_vertex, to_vertex);
+  }
+
+  return {EdgeRef(nullptr), EdgeTypeId::FromUint(0U), nullptr, nullptr};
+}
+
+void InMemoryEdgeTypeIndex::RunGC() {
+  for (auto &index_entry : index_) {
+    index_entry.second.run_gc();
+  }
+}
+
+InMemoryEdgeTypeIndex::Iterable InMemoryEdgeTypeIndex::Edges(EdgeTypeId edge_type, View view, Storage *storage,
+                                                             Transaction *transaction) {
+  const auto it = index_.find(edge_type);
+  MG_ASSERT(it != index_.end(), "Index for edge-type {} doesn't exist", edge_type.AsUint());
+  return {it->second.access(), edge_type, view, storage, transaction};
+}
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/inmemory/edge_type_index.hpp b/src/storage/v2/inmemory/edge_type_index.hpp
new file mode 100644
index 000000000..db8f7843f
--- /dev/null
+++ b/src/storage/v2/inmemory/edge_type_index.hpp
@@ -0,0 +1,113 @@
+// Copyright 2024 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 <map>
+#include <utility>
+
+#include "storage/v2/constraints/constraints.hpp"
+#include "storage/v2/edge_accessor.hpp"
+#include "storage/v2/id_types.hpp"
+#include "storage/v2/indices/edge_type_index.hpp"
+#include "storage/v2/indices/label_index_stats.hpp"
+#include "utils/rw_lock.hpp"
+#include "utils/synchronized.hpp"
+
+namespace memgraph::storage {
+
+class InMemoryEdgeTypeIndex : public storage::EdgeTypeIndex {
+ private:
+  struct Entry {
+    Vertex *from_vertex;
+    Vertex *to_vertex;
+
+    Edge *edge;
+
+    uint64_t timestamp;
+
+    bool operator<(const Entry &rhs) const { return edge->gid < rhs.edge->gid; }
+    bool operator==(const Entry &rhs) const { return edge->gid == rhs.edge->gid; }
+  };
+
+ public:
+  InMemoryEdgeTypeIndex() = default;
+
+  /// @throw std::bad_alloc
+  bool CreateIndex(EdgeTypeId edge_type, utils::SkipList<Vertex>::Accessor vertices);
+
+  /// Returns false if there was no index to drop
+  bool DropIndex(EdgeTypeId edge_type) override;
+
+  bool IndexExists(EdgeTypeId edge_type) const override;
+
+  std::vector<EdgeTypeId> ListIndices() const override;
+
+  void RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp, std::stop_token token);
+
+  uint64_t ApproximateEdgeCount(EdgeTypeId edge_type) const override;
+
+  void UpdateOnEdgeCreation(Vertex *from, Vertex *to, EdgeRef edge_ref, EdgeTypeId edge_type,
+                            const Transaction &tx) override;
+
+  void UpdateOnEdgeModification(Vertex *old_from, Vertex *old_to, Vertex *new_from, Vertex *new_to, EdgeRef edge_ref,
+                                EdgeTypeId edge_type, const Transaction &tx) override;
+
+  static constexpr std::size_t kEdgeTypeIdPos = 0U;
+  static constexpr std::size_t kVertexPos = 1U;
+  static constexpr std::size_t kEdgeRefPos = 2U;
+
+  class Iterable {
+   public:
+    Iterable(utils::SkipList<Entry>::Accessor index_accessor, EdgeTypeId edge_type, View view, Storage *storage,
+             Transaction *transaction);
+
+    class Iterator {
+     public:
+      Iterator(Iterable *self, utils::SkipList<Entry>::Iterator index_iterator);
+
+      EdgeAccessor const &operator*() const { return current_edge_accessor_; }
+
+      bool operator==(const Iterator &other) const { return index_iterator_ == other.index_iterator_; }
+      bool operator!=(const Iterator &other) const { return index_iterator_ != other.index_iterator_; }
+
+      Iterator &operator++();
+
+     private:
+      void AdvanceUntilValid();
+      std::tuple<EdgeRef, EdgeTypeId, Vertex *, Vertex *> GetEdgeInfo();
+
+      Iterable *self_;
+      utils::SkipList<Entry>::Iterator index_iterator_;
+      EdgeAccessor current_edge_accessor_;
+      EdgeRef current_edge_{nullptr};
+    };
+
+    Iterator begin() { return {this, index_accessor_.begin()}; }
+    Iterator end() { return {this, index_accessor_.end()}; }
+
+   private:
+    utils::SkipList<Entry>::Accessor index_accessor_;
+    EdgeTypeId edge_type_;
+    View view_;
+    Storage *storage_;
+    Transaction *transaction_;
+  };
+
+  void RunGC();
+
+  Iterable Edges(EdgeTypeId edge_type, View view, Storage *storage, Transaction *transaction);
+
+ private:
+  std::map<EdgeTypeId, utils::SkipList<Entry>> index_;
+};
+
+}  // namespace memgraph::storage
diff --git a/src/storage/v2/inmemory/storage.cpp b/src/storage/v2/inmemory/storage.cpp
index 3a4fa9b91..1ea909450 100644
--- a/src/storage/v2/inmemory/storage.cpp
+++ b/src/storage/v2/inmemory/storage.cpp
@@ -20,6 +20,7 @@
 #include "storage/v2/durability/snapshot.hpp"
 #include "storage/v2/edge_direction.hpp"
 #include "storage/v2/id_types.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
 #include "storage/v2/metadata_delta.hpp"
 
 /// REPLICATION ///
@@ -350,6 +351,9 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::CreateEdge(VertexAccesso
         transaction_.manyDeltasCache.Invalidate(from_vertex, edge_type, EdgeDirection::OUT);
         transaction_.manyDeltasCache.Invalidate(to_vertex, edge_type, EdgeDirection::IN);
 
+        // Update indices if they exist.
+        storage_->indices_.UpdateOnEdgeCreation(from_vertex, to_vertex, edge, edge_type, transaction_);
+
         // Increment edge count.
         storage_->edge_count_.fetch_add(1, std::memory_order_acq_rel);
       }};
@@ -553,6 +557,11 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::EdgeSetFrom(EdgeAccessor
         CreateAndLinkDelta(&transaction_, to_vertex, Delta::RemoveInEdgeTag(), edge_type, new_from_vertex, edge_ref);
         to_vertex->in_edges.emplace_back(edge_type, new_from_vertex, edge_ref);
 
+        auto *in_memory = static_cast<InMemoryStorage *>(storage_);
+        auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+        mem_edge_type_index->UpdateOnEdgeModification(old_from_vertex, to_vertex, new_from_vertex, to_vertex, edge_ref,
+                                                      edge_type, transaction_);
+
         transaction_.manyDeltasCache.Invalidate(new_from_vertex, edge_type, EdgeDirection::OUT);
         transaction_.manyDeltasCache.Invalidate(old_from_vertex, edge_type, EdgeDirection::OUT);
         transaction_.manyDeltasCache.Invalidate(to_vertex, edge_type, EdgeDirection::IN);
@@ -659,6 +668,11 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::EdgeSetTo(EdgeAccessor *
         CreateAndLinkDelta(&transaction_, new_to_vertex, Delta::RemoveInEdgeTag(), edge_type, from_vertex, edge_ref);
         new_to_vertex->in_edges.emplace_back(edge_type, from_vertex, edge_ref);
 
+        auto *in_memory = static_cast<InMemoryStorage *>(storage_);
+        auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+        mem_edge_type_index->UpdateOnEdgeModification(from_vertex, old_to_vertex, from_vertex, new_to_vertex, edge_ref,
+                                                      edge_type, transaction_);
+
         transaction_.manyDeltasCache.Invalidate(from_vertex, edge_type, EdgeDirection::OUT);
         transaction_.manyDeltasCache.Invalidate(old_to_vertex, edge_type, EdgeDirection::IN);
         transaction_.manyDeltasCache.Invalidate(new_to_vertex, edge_type, EdgeDirection::IN);
@@ -1264,6 +1278,18 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryA
   return {};
 }
 
+utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::CreateIndex(
+    EdgeTypeId edge_type) {
+  MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!");
+  auto *in_memory = static_cast<InMemoryStorage *>(storage_);
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+  if (!mem_edge_type_index->CreateIndex(edge_type, in_memory->vertices_.access())) {
+    return StorageIndexDefinitionError{IndexDefinitionError{}};
+  }
+  transaction_.md_deltas.emplace_back(MetadataDelta::edge_index_create, edge_type);
+  return {};
+}
+
 utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::DropIndex(LabelId label) {
   MG_ASSERT(unique_guard_.owns_lock(), "Dropping label index requires a unique access to the storage!");
   auto *in_memory = static_cast<InMemoryStorage *>(storage_);
@@ -1292,6 +1318,18 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryA
   return {};
 }
 
+utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::DropIndex(
+    EdgeTypeId edge_type) {
+  MG_ASSERT(unique_guard_.owns_lock(), "Drop index requires a unique access to the storage!");
+  auto *in_memory = static_cast<InMemoryStorage *>(storage_);
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+  if (!mem_edge_type_index->DropIndex(edge_type)) {
+    return StorageIndexDefinitionError{IndexDefinitionError{}};
+  }
+  transaction_.md_deltas.emplace_back(MetadataDelta::edge_index_drop, edge_type);
+  return {};
+}
+
 utils::BasicResult<StorageExistenceConstraintDefinitionError, void>
 InMemoryStorage::InMemoryAccessor::CreateExistenceConstraint(LabelId label, PropertyId property) {
   MG_ASSERT(unique_guard_.owns_lock(), "Creating existence requires a unique access to the storage!");
@@ -1383,6 +1421,11 @@ VerticesIterable InMemoryStorage::InMemoryAccessor::Vertices(
       mem_label_property_index->Vertices(label, property, lower_bound, upper_bound, view, storage_, &transaction_));
 }
 
+EdgesIterable InMemoryStorage::InMemoryAccessor::Edges(EdgeTypeId edge_type, View view) {
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(storage_->indices_.edge_type_index_.get());
+  return EdgesIterable(mem_edge_type_index->Edges(edge_type, view, storage_, &transaction_));
+}
+
 Transaction InMemoryStorage::CreateTransaction(
     IsolationLevel isolation_level, StorageMode storage_mode,
     memgraph::replication_coordination_glue::ReplicationRole replication_role) {
@@ -2017,6 +2060,10 @@ bool InMemoryStorage::AppendToWal(const Transaction &transaction, uint64_t final
         AppendToWalDataDefinition(durability::StorageMetadataOperation::LABEL_INDEX_CREATE, md_delta.label,
                                   final_commit_timestamp);
       } break;
+      case MetadataDelta::Action::EDGE_INDEX_CREATE: {
+        AppendToWalDataDefinition(durability::StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE, md_delta.edge_type,
+                                  final_commit_timestamp);
+      } break;
       case MetadataDelta::Action::LABEL_PROPERTY_INDEX_CREATE: {
         const auto &info = md_delta.label_property;
         AppendToWalDataDefinition(durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_CREATE, info.label,
@@ -2026,6 +2073,10 @@ bool InMemoryStorage::AppendToWal(const Transaction &transaction, uint64_t final
         AppendToWalDataDefinition(durability::StorageMetadataOperation::LABEL_INDEX_DROP, md_delta.label,
                                   final_commit_timestamp);
       } break;
+      case MetadataDelta::Action::EDGE_INDEX_DROP: {
+        AppendToWalDataDefinition(durability::StorageMetadataOperation::EDGE_TYPE_INDEX_DROP, md_delta.edge_type,
+                                  final_commit_timestamp);
+      } break;
       case MetadataDelta::Action::LABEL_PROPERTY_INDEX_DROP: {
         const auto &info = md_delta.label_property;
         AppendToWalDataDefinition(durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_DROP, info.label,
@@ -2091,6 +2142,12 @@ void InMemoryStorage::AppendToWalDataDefinition(durability::StorageMetadataOpera
   repl_storage_state_.AppendOperation(operation, label, properties, stats, property_stats, final_commit_timestamp);
 }
 
+void InMemoryStorage::AppendToWalDataDefinition(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                                                uint64_t final_commit_timestamp) {
+  wal_file_->AppendOperation(operation, edge_type, final_commit_timestamp);
+  repl_storage_state_.AppendOperation(operation, edge_type, final_commit_timestamp);
+}
+
 void InMemoryStorage::AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                                 const std::set<PropertyId> &properties,
                                                 LabelPropertyIndexStats property_stats,
@@ -2240,7 +2297,8 @@ IndicesInfo InMemoryStorage::InMemoryAccessor::ListAllIndices() const {
   auto *mem_label_index = static_cast<InMemoryLabelIndex *>(in_memory->indices_.label_index_.get());
   auto *mem_label_property_index =
       static_cast<InMemoryLabelPropertyIndex *>(in_memory->indices_.label_property_index_.get());
-  return {mem_label_index->ListIndices(), mem_label_property_index->ListIndices()};
+  auto *mem_edge_type_index = static_cast<InMemoryEdgeTypeIndex *>(in_memory->indices_.edge_type_index_.get());
+  return {mem_label_index->ListIndices(), mem_label_property_index->ListIndices(), mem_edge_type_index->ListIndices()};
 }
 ConstraintsInfo InMemoryStorage::InMemoryAccessor::ListAllConstraints() const {
   const auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
diff --git a/src/storage/v2/inmemory/storage.hpp b/src/storage/v2/inmemory/storage.hpp
index c0e46d0c9..6d10e0fbd 100644
--- a/src/storage/v2/inmemory/storage.hpp
+++ b/src/storage/v2/inmemory/storage.hpp
@@ -16,6 +16,7 @@
 #include <memory>
 #include <utility>
 #include "storage/v2/indices/label_index_stats.hpp"
+#include "storage/v2/inmemory/edge_type_index.hpp"
 #include "storage/v2/inmemory/label_index.hpp"
 #include "storage/v2/inmemory/label_property_index.hpp"
 #include "storage/v2/inmemory/replication/recovery.hpp"
@@ -53,6 +54,7 @@ class InMemoryStorage final : public Storage {
                                                     const InMemoryStorage *storage);
   friend class InMemoryLabelIndex;
   friend class InMemoryLabelPropertyIndex;
+  friend class InMemoryEdgeTypeIndex;
 
  public:
   enum class CreateSnapshotError : uint8_t { DisabledForReplica, ReachedMaxNumTries };
@@ -107,6 +109,8 @@ class InMemoryStorage final : public Storage {
                               const std::optional<utils::Bound<PropertyValue>> &lower_bound,
                               const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) override;
 
+    EdgesIterable Edges(EdgeTypeId edge_type, View view) override;
+
     /// Return approximate number of all vertices in the database.
     /// Note that this is always an over-estimate and never an under-estimate.
     uint64_t ApproximateVertexCount() const override {
@@ -145,6 +149,10 @@ class InMemoryStorage final : public Storage {
           label, property, lower, upper);
     }
 
+    uint64_t ApproximateEdgeCount(EdgeTypeId id) const override {
+      return static_cast<InMemoryStorage *>(storage_)->indices_.edge_type_index_->ApproximateEdgeCount(id);
+    }
+
     template <typename TResult, typename TIndex, typename TIndexKey>
     std::optional<TResult> GetIndexStatsForIndex(TIndex *index, TIndexKey &&key) const {
       return index->GetIndexStats(key);
@@ -204,6 +212,10 @@ class InMemoryStorage final : public Storage {
       return static_cast<InMemoryStorage *>(storage_)->indices_.label_property_index_->IndexExists(label, property);
     }
 
+    bool EdgeTypeIndexExists(EdgeTypeId edge_type) const override {
+      return static_cast<InMemoryStorage *>(storage_)->indices_.edge_type_index_->IndexExists(edge_type);
+    }
+
     IndicesInfo ListAllIndices() const override;
 
     ConstraintsInfo ListAllConstraints() const override;
@@ -239,6 +251,14 @@ class InMemoryStorage final : public Storage {
     /// @throw std::bad_alloc
     utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(LabelId label, PropertyId property) override;
 
+    /// Create an index.
+    /// Returns void if the index has been created.
+    /// Returns `StorageIndexDefinitionError` if an error occures. Error can be:
+    /// * `ReplicationError`:  there is at least one SYNC replica that has not confirmed receiving the transaction.
+    /// * `IndexDefinitionError`: the index already exists.
+    /// @throw std::bad_alloc
+    utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(EdgeTypeId edge_type) override;
+
     /// Drop an existing index.
     /// Returns void if the index has been dropped.
     /// Returns `StorageIndexDefinitionError` if an error occures. Error can be:
@@ -253,6 +273,13 @@ class InMemoryStorage final : public Storage {
     /// * `IndexDefinitionError`: the index does not exist.
     utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label, PropertyId property) override;
 
+    /// Drop an existing index.
+    /// Returns void if the index has been dropped.
+    /// Returns `StorageIndexDefinitionError` if an error occures. Error can be:
+    /// * `ReplicationError`:  there is at least one SYNC replica that has not confirmed receiving the transaction.
+    /// * `IndexDefinitionError`: the index does not exist.
+    utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(EdgeTypeId edge_type) override;
+
     /// Returns void if the existence constraint has been created.
     /// Returns `StorageExistenceConstraintDefinitionError` if an error occures. Error can be:
     /// * `ReplicationError`: there is at least one SYNC replica that has not confirmed receiving the transaction.
@@ -374,20 +401,17 @@ class InMemoryStorage final : public Storage {
   /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   [[nodiscard]] bool AppendToWal(const Transaction &transaction, uint64_t final_commit_timestamp,
                                  DatabaseAccessProtector db_acc);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                  uint64_t final_commit_timestamp);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
+  void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                                 uint64_t final_commit_timestamp);
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                  const std::set<PropertyId> &properties, uint64_t final_commit_timestamp);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label, LabelIndexStats stats,
                                  uint64_t final_commit_timestamp);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                  const std::set<PropertyId> &properties, LabelPropertyIndexStats property_stats,
                                  uint64_t final_commit_timestamp);
-  /// Return true in all cases excepted if any sync replicas have not sent confirmation.
   void AppendToWalDataDefinition(durability::StorageMetadataOperation operation, LabelId label,
                                  const std::set<PropertyId> &properties, LabelIndexStats stats,
                                  LabelPropertyIndexStats property_stats, uint64_t final_commit_timestamp);
diff --git a/src/storage/v2/metadata_delta.hpp b/src/storage/v2/metadata_delta.hpp
index 94d806c19..b34966a62 100644
--- a/src/storage/v2/metadata_delta.hpp
+++ b/src/storage/v2/metadata_delta.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -35,6 +35,8 @@ struct MetadataDelta {
     LABEL_PROPERTY_INDEX_DROP,
     LABEL_PROPERTY_INDEX_STATS_SET,
     LABEL_PROPERTY_INDEX_STATS_CLEAR,
+    EDGE_INDEX_CREATE,
+    EDGE_INDEX_DROP,
     EXISTENCE_CONSTRAINT_CREATE,
     EXISTENCE_CONSTRAINT_DROP,
     UNIQUE_CONSTRAINT_CREATE,
@@ -57,6 +59,10 @@ struct MetadataDelta {
   } label_property_index_stats_set;
   static constexpr struct LabelPropertyIndexStatsClear {
   } label_property_index_stats_clear;
+  static constexpr struct EdgeIndexCreate {
+  } edge_index_create;
+  static constexpr struct EdgeIndexDrop {
+  } edge_index_drop;
   static constexpr struct ExistenceConstraintCreate {
   } existence_constraint_create;
   static constexpr struct ExistenceConstraintDrop {
@@ -87,6 +93,11 @@ struct MetadataDelta {
   MetadataDelta(LabelPropertyIndexStatsClear /*tag*/, LabelId label)
       : action(Action::LABEL_PROPERTY_INDEX_STATS_CLEAR), label{label} {}
 
+  MetadataDelta(EdgeIndexCreate /*tag*/, EdgeTypeId edge_type)
+      : action(Action::EDGE_INDEX_CREATE), edge_type(edge_type) {}
+
+  MetadataDelta(EdgeIndexDrop /*tag*/, EdgeTypeId edge_type) : action(Action::EDGE_INDEX_DROP), edge_type(edge_type) {}
+
   MetadataDelta(ExistenceConstraintCreate /*tag*/, LabelId label, PropertyId property)
       : action(Action::EXISTENCE_CONSTRAINT_CREATE), label_property{label, property} {}
 
@@ -114,6 +125,8 @@ struct MetadataDelta {
       case Action::LABEL_PROPERTY_INDEX_DROP:
       case Action::LABEL_PROPERTY_INDEX_STATS_SET:
       case Action::LABEL_PROPERTY_INDEX_STATS_CLEAR:
+      case Action::EDGE_INDEX_CREATE:
+      case Action::EDGE_INDEX_DROP:
       case Action::EXISTENCE_CONSTRAINT_CREATE:
       case Action::EXISTENCE_CONSTRAINT_DROP:
         break;
@@ -129,6 +142,8 @@ struct MetadataDelta {
   union {
     LabelId label;
 
+    EdgeTypeId edge_type;
+
     struct {
       LabelId label;
       PropertyId property;
diff --git a/src/storage/v2/replication/replication_client.cpp b/src/storage/v2/replication/replication_client.cpp
index fb332672a..3c1081206 100644
--- a/src/storage/v2/replication/replication_client.cpp
+++ b/src/storage/v2/replication/replication_client.cpp
@@ -407,6 +407,12 @@ void ReplicaStream::AppendOperation(durability::StorageMetadataOperation operati
                   timestamp);
 }
 
+void ReplicaStream::AppendOperation(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                                    uint64_t timestamp) {
+  replication::Encoder encoder(stream_.GetBuilder());
+  EncodeOperation(&encoder, storage_->name_id_mapper_.get(), operation, edge_type, timestamp);
+}
+
 replication::AppendDeltasRes ReplicaStream::Finalize() { return stream_.AwaitResponse(); }
 
 }  // namespace memgraph::storage
diff --git a/src/storage/v2/replication/replication_client.hpp b/src/storage/v2/replication/replication_client.hpp
index 063501111..77a9ba40b 100644
--- a/src/storage/v2/replication/replication_client.hpp
+++ b/src/storage/v2/replication/replication_client.hpp
@@ -65,6 +65,9 @@ class ReplicaStream {
                        const std::set<PropertyId> &properties, const LabelIndexStats &stats,
                        const LabelPropertyIndexStats &property_stats, uint64_t timestamp);
 
+  /// @throw rpc::RpcFailedException
+  void AppendOperation(durability::StorageMetadataOperation operation, EdgeTypeId edge_type, uint64_t timestamp);
+
   /// @throw rpc::RpcFailedException
   replication::AppendDeltasRes Finalize();
 
diff --git a/src/storage/v2/replication/replication_storage_state.cpp b/src/storage/v2/replication/replication_storage_state.cpp
index 25cf484c9..b8f3fef62 100644
--- a/src/storage/v2/replication/replication_storage_state.cpp
+++ b/src/storage/v2/replication/replication_storage_state.cpp
@@ -53,6 +53,16 @@ void ReplicationStorageState::AppendOperation(durability::StorageMetadataOperati
   });
 }
 
+void ReplicationStorageState::AppendOperation(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                                              uint64_t final_commit_timestamp) {
+  replication_clients_.WithLock([&](auto &clients) {
+    for (auto &client : clients) {
+      client->IfStreamingTransaction(
+          [&](auto &stream) { stream.AppendOperation(operation, edge_type, final_commit_timestamp); });
+    }
+  });
+}
+
 bool ReplicationStorageState::FinalizeTransaction(uint64_t timestamp, Storage *storage,
                                                   DatabaseAccessProtector db_acc) {
   return replication_clients_.WithLock([=, db_acc = std::move(db_acc)](auto &clients) mutable {
diff --git a/src/storage/v2/replication/replication_storage_state.hpp b/src/storage/v2/replication/replication_storage_state.hpp
index 91cec563c..f99807c13 100644
--- a/src/storage/v2/replication/replication_storage_state.hpp
+++ b/src/storage/v2/replication/replication_storage_state.hpp
@@ -46,6 +46,8 @@ struct ReplicationStorageState {
   void AppendOperation(durability::StorageMetadataOperation operation, LabelId label,
                        const std::set<PropertyId> &properties, const LabelIndexStats &stats,
                        const LabelPropertyIndexStats &property_stats, uint64_t final_commit_timestamp);
+  void AppendOperation(durability::StorageMetadataOperation operation, EdgeTypeId edge_type,
+                       uint64_t final_commit_timestamp);
   bool FinalizeTransaction(uint64_t timestamp, Storage *storage, DatabaseAccessProtector db_acc);
 
   // Getters
diff --git a/src/storage/v2/storage.hpp b/src/storage/v2/storage.hpp
index 5868d70a3..58936bd56 100644
--- a/src/storage/v2/storage.hpp
+++ b/src/storage/v2/storage.hpp
@@ -30,6 +30,7 @@
 #include "storage/v2/durability/paths.hpp"
 #include "storage/v2/durability/wal.hpp"
 #include "storage/v2/edge_accessor.hpp"
+#include "storage/v2/edges_iterable.hpp"
 #include "storage/v2/indices/indices.hpp"
 #include "storage/v2/mvcc.hpp"
 #include "storage/v2/replication/enums.hpp"
@@ -61,6 +62,7 @@ class EdgeAccessor;
 struct IndicesInfo {
   std::vector<LabelId> label;
   std::vector<std::pair<LabelId, PropertyId>> label_property;
+  std::vector<EdgeTypeId> edge_type;
 };
 
 struct ConstraintsInfo {
@@ -172,6 +174,8 @@ class Storage {
                                       const std::optional<utils::Bound<PropertyValue>> &lower_bound,
                                       const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) = 0;
 
+    virtual EdgesIterable Edges(EdgeTypeId edge_type, View view) = 0;
+
     virtual Result<std::optional<VertexAccessor>> DeleteVertex(VertexAccessor *vertex);
 
     virtual Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> DetachDeleteVertex(
@@ -192,6 +196,8 @@ class Storage {
                                             const std::optional<utils::Bound<PropertyValue>> &lower,
                                             const std::optional<utils::Bound<PropertyValue>> &upper) const = 0;
 
+    virtual uint64_t ApproximateEdgeCount(EdgeTypeId id) const = 0;
+
     virtual std::optional<storage::LabelIndexStats> GetIndexStats(const storage::LabelId &label) const = 0;
 
     virtual std::optional<storage::LabelPropertyIndexStats> GetIndexStats(
@@ -224,6 +230,8 @@ class Storage {
 
     virtual bool LabelPropertyIndexExists(LabelId label, PropertyId property) const = 0;
 
+    virtual bool EdgeTypeIndexExists(EdgeTypeId edge_type) const = 0;
+
     virtual IndicesInfo ListAllIndices() const = 0;
 
     virtual ConstraintsInfo ListAllConstraints() const = 0;
@@ -268,10 +276,14 @@ class Storage {
 
     virtual utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(LabelId label, PropertyId property) = 0;
 
+    virtual utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(EdgeTypeId edge_type) = 0;
+
     virtual utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label) = 0;
 
     virtual utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label, PropertyId property) = 0;
 
+    virtual utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(EdgeTypeId edge_type) = 0;
+
     virtual utils::BasicResult<StorageExistenceConstraintDefinitionError, void> CreateExistenceConstraint(
         LabelId label, PropertyId property) = 0;
 
diff --git a/src/storage/v2/vertices_iterable.cpp b/src/storage/v2/vertices_iterable.cpp
index f6ff46da6..9753052ae 100644
--- a/src/storage/v2/vertices_iterable.cpp
+++ b/src/storage/v2/vertices_iterable.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -10,7 +10,6 @@
 // licenses/APL.txt.
 
 #include "storage/v2/vertices_iterable.hpp"
-
 namespace memgraph::storage {
 
 VerticesIterable::VerticesIterable(AllVerticesIterable vertices) : type_(Type::ALL) {
diff --git a/src/storage/v2/vertices_iterable.hpp b/src/storage/v2/vertices_iterable.hpp
index e057e8a38..6075a68a2 100644
--- a/src/storage/v2/vertices_iterable.hpp
+++ b/src/storage/v2/vertices_iterable.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
diff --git a/src/utils/atomic_memory_block.hpp b/src/utils/atomic_memory_block.hpp
index c15424549..31a3cf3a9 100644
--- a/src/utils/atomic_memory_block.hpp
+++ b/src/utils/atomic_memory_block.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
diff --git a/src/utils/event_counter.cpp b/src/utils/event_counter.cpp
index a7f4d30fb..54ff4ed5c 100644
--- a/src/utils/event_counter.cpp
+++ b/src/utils/event_counter.cpp
@@ -26,6 +26,7 @@
   M(ScanAllByLabelPropertyValueOperator, Operator, "Number of times ScanAllByLabelPropertyValue operator was used.") \
   M(ScanAllByLabelPropertyOperator, Operator, "Number of times ScanAllByLabelProperty operator was used.")           \
   M(ScanAllByIdOperator, Operator, "Number of times ScanAllById operator was used.")                                 \
+  M(ScanAllByEdgeTypeOperator, Operator, "Number of times ScanAllByEdgeTypeOperator operator was used.")             \
   M(ExpandOperator, Operator, "Number of times Expand operator was used.")                                           \
   M(ExpandVariableOperator, Operator, "Number of times ExpandVariable operator was used.")                           \
   M(ConstructNamedPathOperator, Operator, "Number of times ConstructNamedPath operator was used.")                   \
diff --git a/src/utils/settings.cpp b/src/utils/settings.cpp
index 4768edc42..5e0954b4b 100644
--- a/src/utils/settings.cpp
+++ b/src/utils/settings.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
diff --git a/src/utils/typeinfo.hpp b/src/utils/typeinfo.hpp
index 3ed6128fc..77910f731 100644
--- a/src/utils/typeinfo.hpp
+++ b/src/utils/typeinfo.hpp
@@ -32,6 +32,7 @@ enum class TypeId : uint64_t {
   SCAN_ALL_BY_LABEL_PROPERTY_VALUE,
   SCAN_ALL_BY_LABEL_PROPERTY,
   SCAN_ALL_BY_ID,
+  SCAN_ALL_BY_EDGE_TYPE,
   EXPAND_COMMON,
   EXPAND,
   EXPANSION_LAMBDA,
@@ -185,6 +186,7 @@ enum class TypeId : uint64_t {
   AST_EXPLAIN_QUERY,
   AST_PROFILE_QUERY,
   AST_INDEX_QUERY,
+  AST_EDGE_INDEX_QUERY,
   AST_CREATE,
   AST_CALL_PROCEDURE,
   AST_MATCH,
diff --git a/tests/integration/durability/tests/v17/test_all/create_dataset.cypher b/tests/integration/durability/tests/v17/test_all/create_dataset.cypher
new file mode 100644
index 000000000..9ee350d9a
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_all/create_dataset.cypher
@@ -0,0 +1,22 @@
+// --storage-items-per-batch is set to 10
+CREATE INDEX ON :`label2`(`prop2`);
+CREATE INDEX ON :`label2`(`prop`);
+CREATE INDEX ON :`label`;
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE EDGE INDEX ON :`edge_type`;
+CREATE (:`edge_index_from`), (:`edge_index_to`);
+MATCH (n:`edge_index_from`), (m:`edge_index_to`) CREATE (n)-[r:`edge_type`]->(m);
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 0, `prop2`: ["kaj", 2, Null, {`prop4`: -1.341}], `ext`: 2, `prop`: "joj"});
+CREATE (:__mg_vertex__:`label2`:`label` {__mg_id__: 1, `ext`: 2, `prop`: "joj"});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 2, `prop2`: 2, `prop`: 1});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 3, `prop2`: 2, `prop`: 2});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 0 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 1 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 2 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 3 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`prop2`, u.`prop` IS UNIQUE;
+ANALYZE GRAPH;
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+DROP EDGE INDEX ON :`edge_type`;
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_all/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_all/expected_snapshot.cypher
new file mode 100644
index 000000000..fb2d74667
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_all/expected_snapshot.cypher
@@ -0,0 +1,19 @@
+    CREATE (:__mg_vertex__:`edge_index_from` {__mg_id__: 0});
+    CREATE (:__mg_vertex__:`edge_index_to` {__mg_id__: 1});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 2, `prop2`: ["kaj", 2, Null, {`prop4`: -1.341}], `ext`: 2, `prop`: "joj"});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop2`: 2, `prop`: 1});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 5, `prop2`: 2, `prop`: 2});
+    CREATE (:__mg_vertex__:`label`:`label2` {__mg_id__: 3, `ext`: 2, `prop`: "joj"});
+    CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`prop2`, u.`prop` IS UNIQUE;
+    CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+    CREATE INDEX ON :__mg_vertex__(__mg_id__);
+    CREATE INDEX ON :`label2`(`prop2`);
+    CREATE INDEX ON :`label2`(`prop`);
+    CREATE INDEX ON :`label`;
+    DROP INDEX ON :__mg_vertex__(__mg_id__);
+    MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge_type`]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 2 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 3 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 4 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 5 CREATE (u)-[:`link` {`ext`: [false, {`k`: "l"}], `prop`: -1}]->(v);
diff --git a/tests/integration/durability/tests/v17/test_all/expected_wal.cypher b/tests/integration/durability/tests/v17/test_all/expected_wal.cypher
new file mode 100644
index 000000000..33efec9e2
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_all/expected_wal.cypher
@@ -0,0 +1,19 @@
+    CREATE (:__mg_vertex__:`edge_index_from` {__mg_id__: 0});
+    CREATE (:__mg_vertex__:`edge_index_to` {__mg_id__: 1});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 2, `prop2`: ["kaj", 2, Null, {`prop4`: -1.341}], `prop`: "joj", `ext`: 2});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop2`: 2, `prop`: 1});
+    CREATE (:__mg_vertex__:`label2` {__mg_id__: 5, `prop2`: 2, `prop`: 2});
+    CREATE (:__mg_vertex__:`label`:`label2` {__mg_id__: 3, `prop`: "joj", `ext`: 2});
+    CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`prop2`, u.`prop` IS UNIQUE;
+    CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+    CREATE INDEX ON :__mg_vertex__(__mg_id__);
+    CREATE INDEX ON :`label2`(`prop2`);
+    CREATE INDEX ON :`label2`(`prop`);
+    CREATE INDEX ON :`label`;
+    DROP INDEX ON :__mg_vertex__(__mg_id__);
+    MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge_type`]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 2 CREATE (u)-[:`link` {`prop`: -1, `ext`: [false, {`k`: "l"}]}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 3 CREATE (u)-[:`link` {`prop`: -1, `ext`: [false, {`k`: "l"}]}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 4 CREATE (u)-[:`link` {`prop`: -1, `ext`: [false, {`k`: "l"}]}]->(v);
+    MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 3 AND v.__mg_id__ = 5 CREATE (u)-[:`link` {`prop`: -1, `ext`: [false, {`k`: "l"}]}]->(v);
diff --git a/tests/integration/durability/tests/v17/test_all/snapshot.bin b/tests/integration/durability/tests/v17/test_all/snapshot.bin
new file mode 100644
index 0000000000000000000000000000000000000000..9cc54b4809a1a843cc80955e0b5634a4f70154e1
GIT binary patch
literal 2067
zcmcIlJ#Q015M91O2tn2xwnJwKA`&`@i*E_89MMowQljX5w`avRwi8^0Cci8azk&)O
zQBl)G3KbB0-kXRsqKMe)Y3I$GH?w<tuV1~NteNF{X$OMrw@XW8JJ$p}xGrE5U?1S^
z4G9eZz5tvGXxk;l7=pHy8pd9hAc40(W!YO2^Xp*wHRJCi*8z+?;kZqqCb+Qll|Rk0
z#)YM?{x9i-Q`e!($>@Qs;D{fiS#stk(Jh>Sy$SoxH0}^Uh*cBLQKiY|&{52<V_4oH
zI4XvoBL3dFOCp!p%nqir+_ygeI2s<$Pd+M{8pG*OuN>p*2=y(B+S_9CTqBI($|{>3
zF$AD}{L2F}RFxhu6g5mTIozR5hPXo=hPZ=Svhl<+#_2TML`YAF)Ai~WENzXJ=mgq{
zr4^<jMr}&3tjkeQfUW$!?glm0CG(kvoz~Kl+fhq22@bE0a_Fo{W(jawZx-uQPimb4
zeDAGLZgePFUwfi+f^@U9$^6GFn#R+tA)19X4PoaYKQB(t;T*?BKaP2?9Gs5#CL;=w
zeD*DG(VxS6ve4M#TjcJDr&tZ*?%t$|hr9Es1>2#AMR@*U7A@XIcej~YA57oTKi`aa
zIOoC9^qIXUwD-tuh+2_vhdP93X%@3Q&9k~ri>xZrGVAA|)2+&o<s|1X%I&P|mT6su
o-{d;hX+OkH+U;lYdC?6;43&K<%KAB=I}S~M>=?C%_^U_OFSaa&o&W#<

literal 0
HcmV?d00001

diff --git a/tests/integration/durability/tests/v17/test_all/wal.bin b/tests/integration/durability/tests/v17/test_all/wal.bin
new file mode 100644
index 0000000000000000000000000000000000000000..61a33372da4fa550b0bbaf5b614b319121e6eabc
GIT binary patch
literal 3582
zcmbVO%Wl&^6b(-S;?WwnMu4CSRfQ0)nrD+P*|0*Y(o`t|WV!Yv%A-k?xTSz#!*3z+
zA$$i5RxnQH+&gwAiAolcJ#)^v=ia$9o;R;Q_Es~~-z)2>da}N&hiCStdMfUet5s1c
zyFnpebc<!b>Q=*&aCa+yAm4=iD-pOH6mnsqnR9o=an&tKid$)hq1&wZmEC+*ggb?t
zrGztE)@Akh&c3dmdj7HK<?U<gbvPOf^Nc&%vaZgfU?Qe4Ofn$-mQ*J}ctT=skm?=v
z<asA8?@Wv$aqfBcO?|W-dLC0G`<9v&gcD&)c+sb!;7N(Qt#JEV2{2ZpG5a3mVv%+~
zln$Ds!HLelq#!(2voK~Og^7tNKN>Jn8o8_Jha40cl5?6OVdhDR70LyR$YV8HSn`r6
zrxA%|nsxv~>MWM%3TF)~?$}q9nXcb)2=1&n>r@k+T6FIJ`tz;xBl`W*7zVm=C$V;C
z&@rM>xJia_YdBOnBOYGGdBk2BDN76fz6C6g)o5&9X~EWz>Cze?sE^tjK-4+hf~pwM
zhN@sKI2lbHnWm}&Wmy%Q8V*i)tVYMeysCh$A=6a>q|Rcf%H$Z*SIdBft4Y^rd&U4Y
z6o)qv@G|9fN*P9YtY%?sUdmu=$aKnp)L9IwGbtOe$&@(-1JsZ*mYj2vDL+stA5sNv
zMCrBrU1~?#<xFYJQl@M1SN)7oxQH8X4JkOQ8SU&q<8~bzk3H6srXP}N2+T2!U`%OH
zB1B1>hPnoY&V@}wOPYR6rlCd-l9fjNnbM$TAWGUa)HNu=T-Y?Ur0FMQ8m`De(lpwb
zDGlD%;HFJOU4#C&X=q8)8_M*D<OPJjTCyMvlK8B`d#PIyw+gb06Ub8q;x}eXp-m;^
zrvg7x<6pAD;ItoQ_i`^cvi@1i3q@-rM0T(AmB~TM1!+x#chg!0sTH=zQM(^R-kIM!
z6=P3@$;+v6<VW#rklowaE~I|SMr$-U9eU&TXK5w3T}pb(E%K(XtUY6`INw_AI$^D3
z^pf&hNLtG$tl=7#xcblyk**8&QM!T)Y{pZ%O_Ry_o@7gGsbKHTUyWINOp)y8irpj}
zT^D&$;$A3R9pCQ^5h!nGMaSV_U5q>6Enuv!u1H7m?59VIe85N9Qbn8v%RHh9%_Y`7
R5^{-0?Dyr0gj}e2^bhc)#{2*P

literal 0
HcmV?d00001

diff --git a/tests/integration/durability/tests/v17/test_constraints/create_dataset.cypher b/tests/integration/durability/tests/v17/test_constraints/create_dataset.cypher
new file mode 100644
index 000000000..96bb4bac4
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_constraints/create_dataset.cypher
@@ -0,0 +1,6 @@
+CREATE CONSTRAINT ON (u:`label2`) ASSERT EXISTS (u.`ext2`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`a` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`b` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`c` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`a`, u.`b` IS UNIQUE;
diff --git a/tests/integration/durability/tests/v17/test_constraints/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_constraints/expected_snapshot.cypher
new file mode 100644
index 000000000..fbe2c28ab
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_constraints/expected_snapshot.cypher
@@ -0,0 +1,6 @@
+CREATE CONSTRAINT ON (u:`label2`) ASSERT EXISTS (u.`ext2`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`c` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`b` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`a` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`b`, u.`a` IS UNIQUE;
diff --git a/tests/integration/durability/tests/v17/test_constraints/expected_wal.cypher b/tests/integration/durability/tests/v17/test_constraints/expected_wal.cypher
new file mode 100644
index 000000000..9260455ed
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_constraints/expected_wal.cypher
@@ -0,0 +1,6 @@
+CREATE CONSTRAINT ON (u:`label2`) ASSERT EXISTS (u.`ext2`);
+CREATE CONSTRAINT ON (u:`label`) ASSERT EXISTS (u.`ext`);
+CREATE CONSTRAINT ON (u:`label2`) ASSERT u.`a`, u.`b` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`a` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`b` IS UNIQUE;
+CREATE CONSTRAINT ON (u:`label`) ASSERT u.`c` IS UNIQUE;
diff --git a/tests/integration/durability/tests/v17/test_constraints/snapshot.bin b/tests/integration/durability/tests/v17/test_constraints/snapshot.bin
new file mode 100644
index 0000000000000000000000000000000000000000..76986ab9af9feb2f1a955fdddbacb9fa3a1ad874
GIT binary patch
literal 625
zcmaKpJx&8L5QR4+kSaw+J^=|LjivRh_pePu#Tk(8b)bmSBn5|{=6clJ0z~FbA_c{b
zKh1mdJhq>o_Pcp;Tx)t2`UgHBeFD~_BheS&8}M=?(wo4Nq^r0BE7cxI`p)c<U7Vyb
zCYb>A5N%iK79N!}P(7E@m99Gpyr(ImQ8I{5ga9o?!f{#(gqs*Zen03v4KlVN<}D%H
zAm`W1`f>&{{F(Vtg@swln1gGECRD90x$3O<iEZpRZ2Q)^&TslUH>~E6a}Kdfc1WGK
cp3Bn4wi>MCvQFOB+~+_uJb>(Hzjjgi0aYC*O8@`>

literal 0
HcmV?d00001

diff --git a/tests/integration/durability/tests/v17/test_constraints/wal.bin b/tests/integration/durability/tests/v17/test_constraints/wal.bin
new file mode 100644
index 0000000000000000000000000000000000000000..f2d54e5fd2ccb96b8792d650d271c29e2c1a47ae
GIT binary patch
literal 460
zcma)&JqyAx5Qgg~A`TL=J1E@(%~zYQE>7-Uisag4>L}>$A2-lTqmyQO!#&U4ao5XZ
z*k|KAlLIF5!f>P~OoesURHb5Np-ZNu)XYLJnJ`9M=xW|qVuGC(-UGORc+ONW4Kqpr
z^Sl;DI__%d2sX}gGRN4`HKxIW8w6eA*FEndVNFhAp{1)&gD#Sgpuoqzw{M=zkf$-&
k#0G7H<)0cPHfY;_wJxzi+f7<rcRa&%pS0za!r-ViZvzA{R{#J2

literal 0
HcmV?d00001

diff --git a/tests/integration/durability/tests/v17/test_edges/create_dataset.cypher b/tests/integration/durability/tests/v17/test_edges/create_dataset.cypher
new file mode 100644
index 000000000..ab3b3af6d
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_edges/create_dataset.cypher
@@ -0,0 +1,60 @@
+// --storage-items-per-batch is set to 7
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__ {__mg_id__: 1});
+CREATE (:__mg_vertex__ {__mg_id__: 2});
+CREATE (:__mg_vertex__ {__mg_id__: 3});
+CREATE (:__mg_vertex__ {__mg_id__: 4});
+CREATE (:__mg_vertex__ {__mg_id__: 5});
+CREATE (:__mg_vertex__ {__mg_id__: 6});
+CREATE (:__mg_vertex__ {__mg_id__: 7});
+CREATE (:__mg_vertex__ {__mg_id__: 8});
+CREATE (:__mg_vertex__ {__mg_id__: 9});
+CREATE (:__mg_vertex__ {__mg_id__: 10});
+CREATE (:__mg_vertex__ {__mg_id__: 11});
+CREATE (:__mg_vertex__ {__mg_id__: 12});
+CREATE (:__mg_vertex__ {__mg_id__: 13});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 14});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 15});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 2 AND v.__mg_id__ = 3 CREATE (u)-[:`edge` {`prop`: 11}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:`edge` {`prop`: true}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:`edge2`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 9 CREATE (u)-[:`edge2` {`prop`: -3.141}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 11 CREATE (u)-[:`edgelink` {`prop`: {`prop`: 1, `prop2`: {`prop4`: 9}}}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 13 CREATE (u)-[:`edgelink` {`prop`: [1, Null, false, "\n\n\n\n\\\"\"\n\t"]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+ANALYZE GRAPH;
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_edges/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_edges/expected_snapshot.cypher
new file mode 100644
index 000000000..596753ba5
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_edges/expected_snapshot.cypher
@@ -0,0 +1,58 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__ {__mg_id__: 1});
+CREATE (:__mg_vertex__ {__mg_id__: 2});
+CREATE (:__mg_vertex__ {__mg_id__: 3});
+CREATE (:__mg_vertex__ {__mg_id__: 4});
+CREATE (:__mg_vertex__ {__mg_id__: 5});
+CREATE (:__mg_vertex__ {__mg_id__: 6});
+CREATE (:__mg_vertex__ {__mg_id__: 7});
+CREATE (:__mg_vertex__ {__mg_id__: 8});
+CREATE (:__mg_vertex__ {__mg_id__: 9});
+CREATE (:__mg_vertex__ {__mg_id__: 10});
+CREATE (:__mg_vertex__ {__mg_id__: 11});
+CREATE (:__mg_vertex__ {__mg_id__: 12});
+CREATE (:__mg_vertex__ {__mg_id__: 13});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 14});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 15});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 2 AND v.__mg_id__ = 3 CREATE (u)-[:`edge` {`prop`: 11}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:`edge` {`prop`: true}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:`edge2`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 9 CREATE (u)-[:`edge2` {`prop`: -3.141}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 11 CREATE (u)-[:`edgelink` {`prop`: {`prop`: 1, `prop2`: {`prop4`: 9}}}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 13 CREATE (u)-[:`edgelink` {`prop`: [1, Null, false, "\n\n\n\n\\\"\"\n\t"]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_edges/expected_wal.cypher b/tests/integration/durability/tests/v17/test_edges/expected_wal.cypher
new file mode 100644
index 000000000..596753ba5
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_edges/expected_wal.cypher
@@ -0,0 +1,58 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__ {__mg_id__: 1});
+CREATE (:__mg_vertex__ {__mg_id__: 2});
+CREATE (:__mg_vertex__ {__mg_id__: 3});
+CREATE (:__mg_vertex__ {__mg_id__: 4});
+CREATE (:__mg_vertex__ {__mg_id__: 5});
+CREATE (:__mg_vertex__ {__mg_id__: 6});
+CREATE (:__mg_vertex__ {__mg_id__: 7});
+CREATE (:__mg_vertex__ {__mg_id__: 8});
+CREATE (:__mg_vertex__ {__mg_id__: 9});
+CREATE (:__mg_vertex__ {__mg_id__: 10});
+CREATE (:__mg_vertex__ {__mg_id__: 11});
+CREATE (:__mg_vertex__ {__mg_id__: 12});
+CREATE (:__mg_vertex__ {__mg_id__: 13});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 14});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 15});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`edge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 2 AND v.__mg_id__ = 3 CREATE (u)-[:`edge` {`prop`: 11}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:`edge` {`prop`: true}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:`edge2`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 9 CREATE (u)-[:`edge2` {`prop`: -3.141}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 11 CREATE (u)-[:`edgelink` {`prop`: {`prop`: 1, `prop2`: {`prop4`: 9}}}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 13 CREATE (u)-[:`edgelink` {`prop`: [1, Null, false, "\n\n\n\n\\\"\"\n\t"]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 0 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 1 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 2 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 3 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 4 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 5 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 6 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 7 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 8 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 9 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 10 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 11 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 12 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 13 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 14 CREATE (u)-[:`testedge`]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 15 CREATE (u)-[:`testedge`]->(v);
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_edges/snapshot.bin b/tests/integration/durability/tests/v17/test_edges/snapshot.bin
new file mode 100644
index 0000000000000000000000000000000000000000..070bbe530945bfc947eaa95233be64f6d54aa10b
GIT binary patch
literal 4297
zcmb7|yKYlK5Qfd21Omy%$>Gi(=SE`*Ut*uMG*ony2+6UJ0wM|#iA2F;P*T(K3>3Tx
z4+1ec|F?$UjzSub{@K}?+4*O7PhLDfI#{Zm{yhtOl@t~(pGslpb|t))@J7NX3CoMs
znuiiTOZZj^d8l@-A>`pgg(ac#TFb(XN|%R=vsW$s%EKk8tKV84w9AxzmtX$;`sGKQ
z`<{m@G9(PyWvlX87j*dH{b9y_b=i*LcDAY={=&Oin5$gm;p%K>EAo-{&la<#T2`0&
z{K?;|Mq_?157)F$^?Q=-b%$g)an|#`N=H1g>ZY$+aOjpp6%{6{TUCA%6453+O4RR3
zLd(8=-JunSRvpS5y5rDYhweFa-=PN%J#^@iLysMb4&@Ft99nZ|-JuPKHvfepRhS~E
zClUwY0es`Q;R7o5NZJ`hEgWgEw5STP=6#;l$Ue1#M@H2>GP#Ip)CfvG$ex7U2Q+dY
z<cdJh65_d1#GZg;A82up3^sdYN;RgjDQHUe%nbKId$<n|S_71vW17eAo)k%1GZXv3
z=<>+uQjbi&Vw%u`rex17bRV?6`yhMLV2bM)VrnXPj~N2DiR=RbA;43nk4Gk7F?Eih
zDcNK1J{V`L!9CeiGE|B_a#1F)#6i;7bK%K8aMgHZvDzqjWO5Nx=Lkx1z&P$M_mR)`
zLH5K-2dRjWw@&g(9Au9@T`v0|vqS<;xG_62xrnKA1Zl%s8i?G<p8eWvA7oFgbdZo3
zfZRRVYjuhhgr|W=#w~bO?J*-*N=`r^JUX!As5fRG1N5Zt*%@<4O&F*U2m=H%a8jt?
z8GVC_Q3<I~Q4G!(6)BSiD$bWIP?0+b0v$ybsKmh38mM3*S)hUnWP$38!trV(3t7c9
zG^h$SpoAc7#7SwRuz3!0@OENn%z3;J1}fehgn^1n-^syU?d0IHbaHUVIXSpwoE+Q@
zPL8N!A!dYJDkn!nDkzQ>v&hN8FmQ6vy8lzSb#XL(;5hOGXafnMk=`6C9v_hVgWYl;
zeO*Uaj`qs^w+HWdEMYL#w0RBj-8zQ3UD@WiJUafPQTbUv{-E6=!nL|ity}C2yQ9&l
zS?;z*&F)~k-7H#Vzgax(_B)+cuiYNBSX`8YvTXN?X4&g^n%&)Q(cBpn!)DPNb&CFQ
bd)OL{!beqcO9G<;mhVZ$8dUs)LRP;408MO2

literal 0
HcmV?d00001

diff --git a/tests/integration/durability/tests/v17/test_edges/wal.bin b/tests/integration/durability/tests/v17/test_edges/wal.bin
new file mode 100644
index 0000000000000000000000000000000000000000..914f49154070a94eb847744868f9f0f10759e93d
GIT binary patch
literal 6616
zcma)=TW{l36ot#3mRsw|)Z8hVQXw=MC2rED6+&o)03kq?$B+stOfF6cgtltZf<Qd+
zGkE5I@W4OehrmwiwN^U59Qx$$?DegkbB>qYe)iE%ll7JBpZne$>6Q0;dU2<+on8fv
z`hKh3+^q%uexufG_xiPVf4^NDcAM>1BT0sPjRQ8f8ui8?=nZPSgWX}R88jNT?y!DP
zs|P`IzZ*1r2d#mZf>(CDN7Ku%E6=4@as1;bzDh1;$<J|ISzWG<2XQRP@s9U+`svXU
z5w(!De6-Y&Ix}`YKU2RMKS|?9(ks<M549p=s3Ude7=t3R89z;9tek2gSF^|%>PVeA
z##yo%uhIB1eq+=^549p=bVlmTF$P6$X8bITv9qd$JOM?<P)F*_G0u|B_<0&X!EcOO
z=%H3*jLt}%ImV#K&5UtV9`Tr|7P3}k40WW=9Ag|ok<A!q{m6TY-x#%!wIXA5M(WHl
z&XUcz!llLuRW0P1FEWNYQfH2FmTbnGG=@U8&_k`r80tu!kStPco>x9U6WNSkq%m#{
z)k5BEMaED^>dZ0DlFj&K8b6a>sTO*u6&XVvsWZnI6p_vNRT|@-R4wFPUSte)q|O}U
zEZK}-r!gKes)ZhEMaED^>dY|)MPxJHqA|iiwUF^rWDNCEC(>fl?IjbjBC&Et%X-^r
zRgU&TdZk+Ep;j~+)JvTYjfRd$tenx{BX1jRJ00i?<jG0}@th1s39Ec|N@D6As`2=|
zsm8O7&ml=VBvNvIadz(WsH@eC-lh?dR4vBD%#43YI(qt|YhkqUuX@bk9XjB-SivgK
z9RlV%^Hv&@-=!QT56KaTO92H<M^8(L7Zcu!{`vEdzqeNZ65gEd(FyZaz@85}8K3^Z
zoLX8;$U0miuHeFKGqbSQ+rf>6+q@k3o4NJoa{j{6X0CH)JPETiBTabcOV;7y-<<h2
zmdod1^4I;16`wD%YxZ+et*)&KX5$1L2hT^2GfSqk#fid=%sW6bBQ#0C+LsWpRFWBC
zj|AMRB?Rn^WJYL{fJaXW0f$;LBOH=|XGjSFYb}`(-X#IsS3<xElFSJ2k$_Y$A>i~#
zW`y@iz@C*5u;G#!;XVn-$PxlJTrwkkKmuL}O9<F-$&BzJ2^;BE2>}}}nGrrF0cW;^
zfDM<-2%nH}%RK`&TrwkkN&;RJOJ=}^OJ;=6Nx1Ev0UIuv5spZ}8)L}~*l@{=5R%{*
z&Hy3S+WxwR%m@()czMjuK!WlCG9!FR!b|QMln;;@;g|%xN0!W>e1OadUy<;Ndj{nL
zWJWk40k4!LGbkS*Gr~6{yyl)k`2d*_z9j)~mnAbOA0RVAj|BYRri7q;fXoO361LK-
z5`yvpG9wI0c+*W#K0s!K??|Y+3Cah^jPN}PciaT!17t>+kg)A0C?6m*!YK)Fxe3Y#
z$c%7K!j7AOn^ZC*T%@{w{wq7*KKN&81;_E%YX9u=bXGlV?A_g}4#v~j__RNZuey`V
zWEwAjHl1~6^N;)0!@9M0uSW6c;_UJ~o{oP>On+S(B%_POer?`s@3(%_WH#h19rEP=
z4k?MLuTu2`=372Sou&WRTUXx$zN@*3NXG1n`ZtKkKR1z`xXHLjH;KqYH<6tVBJ$Bq
zWT%6OymS-U=^!FM-9&aeh{#hnk(~}A^3_dbr-O)(-9&ccqTuu(f89iOI*7<)H<6tV
iBJ$ZyWT%6Oymk}W=^!G%-9&aeh{$s{k)5T)<NpAH_`ARW

literal 0
HcmV?d00001

diff --git a/tests/integration/durability/tests/v17/test_indices/create_dataset.cypher b/tests/integration/durability/tests/v17/test_indices/create_dataset.cypher
new file mode 100644
index 000000000..739062f19
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_indices/create_dataset.cypher
@@ -0,0 +1,6 @@
+CREATE INDEX ON :`label2`;
+CREATE INDEX ON :`label2`(`prop2`);
+CREATE INDEX ON :`label`(`prop2`);
+CREATE INDEX ON :`label`(`prop`);
+CREATE EDGE INDEX ON :`edgetype`;
+ANALYZE GRAPH;
diff --git a/tests/integration/durability/tests/v17/test_indices/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_indices/expected_snapshot.cypher
new file mode 100644
index 000000000..1e930697a
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_indices/expected_snapshot.cypher
@@ -0,0 +1,5 @@
+CREATE INDEX ON :`label2`;
+CREATE INDEX ON :`label`(`prop`);
+CREATE INDEX ON :`label`(`prop2`);
+CREATE INDEX ON :`label2`(`prop2`);
+CREATE EDGE INDEX ON :`edgetype`;
diff --git a/tests/integration/durability/tests/v17/test_indices/expected_wal.cypher b/tests/integration/durability/tests/v17/test_indices/expected_wal.cypher
new file mode 100644
index 000000000..bfae88b0b
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_indices/expected_wal.cypher
@@ -0,0 +1,5 @@
+CREATE INDEX ON :`label2`;
+CREATE INDEX ON :`label2`(`prop2`);
+CREATE INDEX ON :`label`(`prop2`);
+CREATE INDEX ON :`label`(`prop`);
+CREATE EDGE INDEX ON :`edgetype`;
diff --git a/tests/integration/durability/tests/v17/test_indices/snapshot.bin b/tests/integration/durability/tests/v17/test_indices/snapshot.bin
new file mode 100644
index 0000000000000000000000000000000000000000..99ad6e0ea29bd82dbabbc8cd429ce40afa116005
GIT binary patch
literal 731
zcmbu7J#NB45QUupQ3@J{JwZYQ%}O47h1oVuM3E{d@Or&&EU+L=&XYTE2ilw_9r<Bq
zjAR#);>Mrny?L`c^7GSvGl|a6BUD5^@P2jH;J|QXSYMO$!f<5xAeaG}BcKscPlcr^
z0}P0Ix+wV~M=H`KAc5Uuup~nXK2K5sN&aVh_dI`?zGyc!B>O|V4-UA45h1Wk-BP9W
zRr&hXl(J66mS(qY<aUY^>fDvJOqKGFIg!cghSGoC$y|hcH6sOcvvk&=Mi-+tu5kMM
wE7!62Hck-HXK8CT!C|s;IxKC_Cip_z%w&4yQ*=e1nk-lt^Bw7Z@jG=YU(Ba5i2wiq

literal 0
HcmV?d00001

diff --git a/tests/integration/durability/tests/v17/test_indices/wal.bin b/tests/integration/durability/tests/v17/test_indices/wal.bin
new file mode 100644
index 0000000000000000000000000000000000000000..661cba6c1b1f55ffefae349b245c01a11b6533b1
GIT binary patch
literal 847
zcmc(cy-UPE5XBEs5wS6(vrwX3<w9<gc)ON1R`w?;!hQ@P5R#B=PE@S?$KAV|O^$F_
z>Q=M7_j_-WeR+N>FK4^+<Xz+BedBQJ-{C}A905xNp%JGfma!&6E;$LsLIY)sP%#+H
ziYth;=7b5RNeoLxWSB@2!h;lyCsgv-!{FK6JHqkRKgFpO5=uHMtGRcKQaUmDCnz*+
z-B`r=EaKAA!L!XBZb&$rCC-Ktm%1Zd=!O_w7C1)$e20S0!MryxYSwmvHZfiP3Wd)=
z)qQ1x$MF6p5bHeCkhcK#{x#BSO1mYuAL6HnvUJgzqEaSXi?W9<+pFn}FdJ2kt$ADb
dP1Y43wwY9-`fV3ie&C<}?<)I`^|za>egKm>sSp4F

literal 0
HcmV?d00001

diff --git a/tests/integration/durability/tests/v17/test_vertices/create_dataset.cypher b/tests/integration/durability/tests/v17/test_vertices/create_dataset.cypher
new file mode 100644
index 000000000..061df375b
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_vertices/create_dataset.cypher
@@ -0,0 +1,18 @@
+// --storage-items-per-batch is set to 5
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 2, `prop`: false});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 3, `prop`: true});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop`: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 5, `prop2`: 3.141});
+CREATE (:__mg_vertex__:`label6` {__mg_id__: 6, `prop3`: true, `prop2`: -314000000});
+CREATE (:__mg_vertex__:`label3`:`label1`:`label2` {__mg_id__: 7});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 8, `prop3`: "str", `prop2`: 2, `prop`: 1});
+CREATE (:__mg_vertex__:`label2`:`label1` {__mg_id__: 9, `prop`: {`prop_nes`: "kaj je"}});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 10, `prop_array`: [1, false, Null, "str", {`prop2`: 2}]});
+CREATE (:__mg_vertex__:`label3`:`label` {__mg_id__: 11, `prop`: {`prop`: [1, false], `prop2`: {}, `prop3`: "test2", `prop4`: "test"}});
+CREATE (:__mg_vertex__ {__mg_id__: 12, `prop`: " \n\"\'\t\\%"});
+ANALYZE GRAPH;
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_vertices/expected_snapshot.cypher b/tests/integration/durability/tests/v17/test_vertices/expected_snapshot.cypher
new file mode 100644
index 000000000..ecdc1229e
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_vertices/expected_snapshot.cypher
@@ -0,0 +1,16 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 2, `prop`: false});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 3, `prop`: true});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop`: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 5, `prop2`: 3.141});
+CREATE (:__mg_vertex__:`label6` {__mg_id__: 6, `prop3`: true, `prop2`: -314000000});
+CREATE (:__mg_vertex__:`label2`:`label3`:`label1` {__mg_id__: 7});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 8, `prop3`: "str", `prop2`: 2, `prop`: 1});
+CREATE (:__mg_vertex__:`label1`:`label2` {__mg_id__: 9, `prop`: {`prop_nes`: "kaj je"}});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 10, `prop_array`: [1, false, Null, "str", {`prop2`: 2}]});
+CREATE (:__mg_vertex__:`label`:`label3` {__mg_id__: 11, `prop`: {`prop`: [1, false], `prop2`: {}, `prop3`: "test2", `prop4`: "test"}});
+CREATE (:__mg_vertex__ {__mg_id__: 12, `prop`: " \n\"\'\t\\%"});
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_vertices/expected_wal.cypher b/tests/integration/durability/tests/v17/test_vertices/expected_wal.cypher
new file mode 100644
index 000000000..d8f758737
--- /dev/null
+++ b/tests/integration/durability/tests/v17/test_vertices/expected_wal.cypher
@@ -0,0 +1,16 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 2, `prop`: false});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 3, `prop`: true});
+CREATE (:__mg_vertex__:`label2` {__mg_id__: 4, `prop`: 1});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 5, `prop2`: 3.141});
+CREATE (:__mg_vertex__:`label6` {__mg_id__: 6, `prop2`: -314000000, `prop3`: true});
+CREATE (:__mg_vertex__:`label2`:`label3`:`label1` {__mg_id__: 7});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 8, `prop`: 1, `prop2`: 2, `prop3`: "str"});
+CREATE (:__mg_vertex__:`label1`:`label2` {__mg_id__: 9, `prop`: {`prop_nes`: "kaj je"}});
+CREATE (:__mg_vertex__:`label` {__mg_id__: 10, `prop_array`: [1, false, Null, "str", {`prop2`: 2}]});
+CREATE (:__mg_vertex__:`label`:`label3` {__mg_id__: 11, `prop`: {`prop`: [1, false], `prop2`: {}, `prop3`: "test2", `prop4`: "test"}});
+CREATE (:__mg_vertex__ {__mg_id__: 12, `prop`: " \n\"\'\t\\%"});
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v17/test_vertices/snapshot.bin b/tests/integration/durability/tests/v17/test_vertices/snapshot.bin
new file mode 100644
index 0000000000000000000000000000000000000000..8a67d9a7d31453b35945fe69115087c3040990d0
GIT binary patch
literal 1739
zcma)7y>8S%7`%l1a3M(Chm&nrNzg%I>Fo9Wb%-LQNtY4{d3`IS5QXUOnsmGeHKNT6
z(D5eJJVQ7qJF~IYM7Wy!c6N5Y+0VYy_lwJ3=l*#opF9d+WAnZwZvYIyF+hLIb25M*
z05=|J@yeb50b15zgkT+&e+y|LDc;7)6EC28Enc)>Iar8A!BM^2xE(DUu*x&z3MIlh
zQDa(s!ks{h&48m`p8fv*?U(MpZS&;s;eiq>l%$e;{dw}|cJ;_M!)omm%ON#(`V=8_
z0iyosQw!OQjINLr&rl(a;Fxq+QVBLLmUEs&poPA;l@&HYH_%f1C?&9bHUDz;@zO0+
zf<!-;=lb07C;}}z$O0-Y6Z_0_)L!Tg^wrF&7mdyy7y`XEl&hc|<*o&11QJ6)TOs7o
zJ90??>Z8;i_J)gD`AnL2_Ti#YH;O-(Zn4bys}?R4&#4y~BY~D(<O%f_*9j&<?;P~I
zA6{`g!4c6T;5FA;6b`~PA=(6YeK#NFd|qCY#7Vg*t-HuL{mr`J+t-N22<EIwAMtC^
zpy&J?HR(CuC#GCG<wQ{A&SRh5A0|gW?7S#*I~`PMR#aJ9IO|etXOq;;?5HqiHk{@a
t$um2)<*+oVvxQBIVqB$@!K6x!&5SGUU_7d(au{9b^QeMXA^b&w^A~t%WBC98

literal 0
HcmV?d00001

diff --git a/tests/integration/durability/tests/v17/test_vertices/wal.bin b/tests/integration/durability/tests/v17/test_vertices/wal.bin
new file mode 100644
index 0000000000000000000000000000000000000000..304db455f08ea9363a01f9248e3ece3020f731db
GIT binary patch
literal 4355
zcma)8%Wl&^6b)}$9!iZ{BZMFdP&Qb~!)cSQ5F6MaRjwBmRI;385~YN;O5E}i5)z-m
zn$KXzpRnc|$|RX{@3<a!$ZB%u+;h)8zMk>xmmkLE(&G2Rd8k&-ceQxrK31ziXLF-n
z>u&l<FRA-MqZ|0$L2KYQx}89@I_+jX2?*S7)q<ek5`JIwyMEA0+I~kg>wY4Vda%(N
z)LXrVqlioE&PBC+>)ukUIDS8jPsAh@ALH0v(xan(9COO<x^r3WZflH)g%mSWb9ThY
zTse>S7%vN7rNS4~iesUJSW#hQYsf2#W}I}zn7Jb6GDt%m#!G3}NgBN6SV(>sNkg`V
zJR=Q=h`9{X^0hTQ9?{X{@W@+{QvgmWf0^W8B4-f`9mI;{8<1z@0}(NoK|boR9u1$B
zf65ps|0c-?ien+oYmt0pYe>w>d@veJ7BQJYf4;be$L8FA8Zf4oZQQ0dU?0aq2eG0y
zkgXw+qUIZjh`9`1Ac<>hcx=wPXn1#`KR<u`t}H!gtZW0v+cv$h91AH{)CRIOWaepX
zFj>T8hCYyG#Ao|(zWsXj_f-C@*rvClfn>8dskWVK^yVNIQmm+10}`_5&#cJ>G_nk&
zZimPLMxlr$87#}UAUkwvDsf<NETluEs6%9HNO-TN5{QVo4ASzoHGI*#WuQj!#Q@W3
z$}MTMI=1P#IG(2IgyGVlH8KdgITkvI6&ZwV4Vihq^5NMaas&X0xE>16ulCs`?$MqR
zpD}R_x10;(XNJWGV(Ph&_b%D5><i|SR@@~kaN=_;q+6!Q3S?_YJOVW<Kt#-CXft11
z!((&aPcoS#pS)F6@#tKZ7o=CtJaUgM0X#QDkSCqEM^4;OD~^Q@Vnt3MTSLNioC^jB
zbSbldh`5}=EJ$H_8a_J{_~&9VGr(KLS4YM@7h)DBVLnTlxO}MqOT{#8(7#zy;8C5~
zChL>#-Y0i)d2=kJ&w(O$ksUG8U`(D_!)F|Dac)+Yt528qp0E}v^?{PwGposeYrAh$
z#+PdE@c1CDZq=L5D%JjInvM>7X?&85kHs|B_oiu*&bE8it(t@;C&PF+IXpg!r=!n8
z<}X5hF`NkbxAv7)Nr4qrZts5ua*E8Zs_Y%yFeNh$BhUTtFEeh3UPe13omTnhm|?`I
z+8GIHVZ^0iXC$PB5u;^iB&3BA@0gvDkQPQ9Ms`L*S{U&P*%=9GVZ_B{XC$PB5!-e~
jLSn&}aAqEH8QB>LX<@{1Zf7K<g%RhVosp2|GVcBZa+C9o

literal 0
HcmV?d00001

diff --git a/tests/manual/interactive_planning.cpp b/tests/manual/interactive_planning.cpp
index 3f64c4f37..f0f60ba91 100644
--- a/tests/manual/interactive_planning.cpp
+++ b/tests/manual/interactive_planning.cpp
@@ -214,6 +214,8 @@ class InteractiveDbAccessor {
     return label_property_index_.at(key);
   }
 
+  bool EdgeTypeIndexExists(memgraph::storage::EdgeTypeId edge_type) { return true; }
+
   std::optional<memgraph::storage::LabelIndexStats> GetIndexStats(const memgraph::storage::LabelId label) const {
     return dba_->GetIndexStats(label);
   }
diff --git a/tests/unit/dbms_database.cpp b/tests/unit/dbms_database.cpp
index 535c0c055..0fded2324 100644
--- a/tests/unit/dbms_database.cpp
+++ b/tests/unit/dbms_database.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
diff --git a/tests/unit/query_plan.cpp b/tests/unit/query_plan.cpp
index bc4b2660c..5b574c1ff 100644
--- a/tests/unit/query_plan.cpp
+++ b/tests/unit/query_plan.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -808,13 +808,68 @@ TYPED_TEST(TestPlanner, MatchWhereBeforeExpand) {
   CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectFilter(), ExpectExpand(), ExpectProduce());
 }
 
+TYPED_TEST(TestPlanner, MatchEdgeTypeIndex) {
+  FakeDbAccessor dba;
+  auto indexed_edge_type = dba.EdgeType("indexed_edgetype");
+  dba.SetIndexCount(indexed_edge_type, 1);
+  {
+    // Test MATCH ()-[r:indexed_edgetype]->() RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(PATTERN(NODE("anon1"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"indexed_edgetype"}),
+                      NODE("anon2"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAllByEdgeType(), ExpectProduce());
+  }
+  {
+    // Test MATCH (a)-[r:indexed_edgetype]->() RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(PATTERN(NODE("a"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"indexed_edgetype"}),
+                      NODE("anon2"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectExpand(), ExpectProduce());
+  }
+  {
+    // Test MATCH ()-[r:indexed_edgetype]->(b) RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(PATTERN(NODE("anon1"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"indexed_edgetype"}),
+                      NODE("b"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectExpand(), ExpectProduce());
+  }
+  {
+    // Test MATCH (a)-[r:indexed_edgetype]->(b) RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(
+            PATTERN(NODE("a"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"indexed_edgetype"}), NODE("b"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectExpand(), ExpectProduce());
+  }
+  {
+    // Test MATCH ()-[r:not_indexed_edgetype]->() RETURN r;
+    auto *query = QUERY(SINGLE_QUERY(
+        MATCH(PATTERN(NODE("anon1"), EDGE("r", memgraph::query::EdgeAtom::Direction::OUT, {"not_indexed_edgetype"}),
+                      NODE("anon2"))),
+        RETURN("r")));
+    auto symbol_table = memgraph::query::MakeSymbolTable(query);
+    auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
+    CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectExpand(), ExpectProduce());
+  }
+}
+
 TYPED_TEST(TestPlanner, MatchFilterPropIsNotNull) {
   FakeDbAccessor dba;
   auto label = dba.Label("label");
   auto prop = PROPERTY_PAIR(dba, "prop");
   dba.SetIndexCount(label, 1);
   dba.SetIndexCount(label, prop.second, 1);
-
   {
     // Test MATCH (n :label) -[r]- (m) WHERE n.prop IS NOT NULL RETURN n
     auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n", "label"), EDGE("r"), NODE("m"))),
diff --git a/tests/unit/query_plan_checker.hpp b/tests/unit/query_plan_checker.hpp
index 92089eb82..6eef3841a 100644
--- a/tests/unit/query_plan_checker.hpp
+++ b/tests/unit/query_plan_checker.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -65,6 +65,7 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor {
   PRE_VISIT(ScanAllByLabelPropertyValue);
   PRE_VISIT(ScanAllByLabelPropertyRange);
   PRE_VISIT(ScanAllByLabelProperty);
+  PRE_VISIT(ScanAllByEdgeType);
   PRE_VISIT(ScanAllById);
   PRE_VISIT(Expand);
   PRE_VISIT(ExpandVariable);
@@ -170,6 +171,7 @@ using ExpectCreateExpand = OpChecker<CreateExpand>;
 using ExpectDelete = OpChecker<Delete>;
 using ExpectScanAll = OpChecker<ScanAll>;
 using ExpectScanAllByLabel = OpChecker<ScanAllByLabel>;
+using ExpectScanAllByEdgeType = OpChecker<ScanAllByEdgeType>;
 using ExpectScanAllById = OpChecker<ScanAllById>;
 using ExpectExpand = OpChecker<Expand>;
 using ExpectConstructNamedPath = OpChecker<ConstructNamedPath>;
@@ -560,6 +562,12 @@ class FakeDbAccessor {
     return 0;
   }
 
+  int64_t EdgesCount(memgraph::storage::EdgeTypeId edge_type) const {
+    auto found = edge_type_index_.find(edge_type);
+    if (found != edge_type_index_.end()) return found->second;
+    return 0;
+  }
+
   bool LabelIndexExists(memgraph::storage::LabelId label) const {
     return label_index_.find(label) != label_index_.end();
   }
@@ -573,6 +581,10 @@ class FakeDbAccessor {
     return false;
   }
 
+  bool EdgeTypeIndexExists(memgraph::storage::EdgeTypeId edge_type) const {
+    return edge_type_index_.find(edge_type) != edge_type_index_.end();
+  }
+
   std::optional<memgraph::storage::LabelPropertyIndexStats> GetIndexStats(
       const memgraph::storage::LabelId label, const memgraph::storage::PropertyId property) const {
     return memgraph::storage::LabelPropertyIndexStats{.statistic = 0, .avg_group_size = 1};  // unique id
@@ -594,6 +606,8 @@ class FakeDbAccessor {
     label_property_index_.emplace_back(label, property, count);
   }
 
+  void SetIndexCount(memgraph::storage::EdgeTypeId edge_type, int64_t count) { edge_type_index_[edge_type] = count; }
+
   memgraph::storage::LabelId NameToLabel(const std::string &name) {
     auto found = labels_.find(name);
     if (found != labels_.end()) return found->second;
@@ -608,6 +622,8 @@ class FakeDbAccessor {
     return edge_types_.emplace(name, memgraph::storage::EdgeTypeId::FromUint(edge_types_.size())).first->second;
   }
 
+  memgraph::storage::EdgeTypeId EdgeType(const std::string &name) { return NameToEdgeType(name); }
+
   memgraph::storage::PropertyId NameToProperty(const std::string &name) {
     auto found = properties_.find(name);
     if (found != properties_.end()) return found->second;
@@ -632,6 +648,7 @@ class FakeDbAccessor {
 
   std::unordered_map<memgraph::storage::LabelId, int64_t> label_index_;
   std::vector<std::tuple<memgraph::storage::LabelId, memgraph::storage::PropertyId, int64_t>> label_property_index_;
+  std::unordered_map<memgraph::storage::EdgeTypeId, int64_t> edge_type_index_;
 };
 
 }  // namespace memgraph::query::plan
diff --git a/tests/unit/storage_v2_decoder_encoder.cpp b/tests/unit/storage_v2_decoder_encoder.cpp
index 9b627cb77..15db49b1c 100644
--- a/tests/unit/storage_v2_decoder_encoder.cpp
+++ b/tests/unit/storage_v2_decoder_encoder.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 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
@@ -337,6 +337,7 @@ TEST_F(DecoderEncoderTest, PropertyValueInvalidMarker) {
         case memgraph::storage::durability::Marker::SECTION_CONSTRAINTS:
         case memgraph::storage::durability::Marker::SECTION_DELTA:
         case memgraph::storage::durability::Marker::SECTION_EPOCH_HISTORY:
+        case memgraph::storage::durability::Marker::SECTION_EDGE_INDICES:
         case memgraph::storage::durability::Marker::SECTION_OFFSETS:
         case memgraph::storage::durability::Marker::DELTA_VERTEX_CREATE:
         case memgraph::storage::durability::Marker::DELTA_VERTEX_DELETE:
@@ -355,6 +356,8 @@ TEST_F(DecoderEncoderTest, PropertyValueInvalidMarker) {
         case memgraph::storage::durability::Marker::DELTA_LABEL_PROPERTY_INDEX_DROP:
         case memgraph::storage::durability::Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_SET:
         case memgraph::storage::durability::Marker::DELTA_LABEL_PROPERTY_INDEX_STATS_CLEAR:
+        case memgraph::storage::durability::Marker::DELTA_EDGE_TYPE_INDEX_CREATE:
+        case memgraph::storage::durability::Marker::DELTA_EDGE_TYPE_INDEX_DROP:
         case memgraph::storage::durability::Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
         case memgraph::storage::durability::Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
         case memgraph::storage::durability::Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
diff --git a/tests/unit/storage_v2_durability_inmemory.cpp b/tests/unit/storage_v2_durability_inmemory.cpp
index 54671077f..7794f2ab9 100644
--- a/tests/unit/storage_v2_durability_inmemory.cpp
+++ b/tests/unit/storage_v2_durability_inmemory.cpp
@@ -69,6 +69,7 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
     ONLY_EXTENDED,
     ONLY_EXTENDED_WITH_BASE_INDICES_AND_CONSTRAINTS,
     BASE_WITH_EXTENDED,
+    BASE_WITH_EDGE_TYPE_INDEXED,
   };
 
  public:
@@ -270,6 +271,15 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
     if (single_transaction) ASSERT_FALSE(acc->Commit().HasError());
   }
 
+  void CreateEdgeIndex(memgraph::storage::Storage *store, memgraph::storage::EdgeTypeId edge_type) {
+    {
+      // Create edge-type index.
+      auto unique_acc = store->UniqueAccess(ReplicationRole::MAIN);
+      ASSERT_FALSE(unique_acc->CreateIndex(edge_type).HasError());
+      ASSERT_FALSE(unique_acc->Commit().HasError());
+    }
+  }
+
   void VerifyDataset(memgraph::storage::Storage *store, DatasetType type, bool properties_on_edges,
                      bool verify_info = true) {
     auto base_label_indexed = store->NameToLabel("base_indexed");
@@ -310,13 +320,19 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
                       UnorderedElementsAre(std::make_pair(base_label_indexed, property_id),
                                            std::make_pair(extended_label_indexed, property_count)));
           break;
+        case DatasetType::BASE_WITH_EDGE_TYPE_INDEXED:
+          ASSERT_THAT(info.label, UnorderedElementsAre(base_label_unindexed));
+          ASSERT_THAT(info.label_property, UnorderedElementsAre(std::make_pair(base_label_indexed, property_id)));
+          ASSERT_THAT(info.edge_type, UnorderedElementsAre(et1));
+          break;
       }
     }
 
     // Verify index statistics
     {
       switch (type) {
-        case DatasetType::ONLY_BASE: {
+        case DatasetType::ONLY_BASE:
+        case DatasetType::BASE_WITH_EDGE_TYPE_INDEXED: {
           const auto l_stats = acc->GetIndexStats(base_label_unindexed);
           ASSERT_TRUE(l_stats);
           ASSERT_EQ(l_stats->count, 1);
@@ -379,6 +395,7 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
       auto info = acc->ListAllConstraints();
       switch (type) {
         case DatasetType::ONLY_BASE:
+        case DatasetType::BASE_WITH_EDGE_TYPE_INDEXED:
           ASSERT_THAT(info.existence, UnorderedElementsAre(std::make_pair(base_label_unindexed, property_id)));
           ASSERT_THAT(info.unique, UnorderedElementsAre(
                                        std::make_pair(base_label_unindexed, std::set{property_id, property_extra})));
@@ -402,6 +419,7 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
 
     bool have_base_dataset = false;
     bool have_extended_dataset = false;
+    bool have_edge_type_indexed_dataset = false;
     switch (type) {
       case DatasetType::ONLY_BASE:
       case DatasetType::ONLY_BASE_WITH_EXTENDED_INDICES_AND_CONSTRAINTS:
@@ -415,6 +433,9 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
         have_base_dataset = true;
         have_extended_dataset = true;
         break;
+      case DatasetType::BASE_WITH_EDGE_TYPE_INDEXED:
+        have_base_dataset = true;
+        have_edge_type_indexed_dataset = true;
     }
 
     // Verify base dataset.
@@ -675,6 +696,19 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
       }
     }
 
+    if (have_edge_type_indexed_dataset) {
+      MG_ASSERT(properties_on_edges, "Edge-type indexing needs --properties-on-edges!");
+      // Verify edge-type indices.
+      {
+        std::vector<memgraph::storage::EdgeAccessor> edges;
+        edges.reserve(kNumBaseEdges / 2);
+        for (auto edge : acc->Edges(et1, memgraph::storage::View::OLD)) {
+          edges.push_back(edge);
+        }
+        ASSERT_EQ(edges.size(), kNumBaseEdges / 2);
+      }
+    }
+
     if (verify_info) {
       auto info = store->GetBaseInfo();
       if (have_base_dataset) {
@@ -2972,3 +3006,42 @@ TEST_P(DurabilityTest, ConstraintsRecoveryFunctionSetting) {
           &variant_existence_constraint_creation_func);
   MG_ASSERT(pval_existence, "Chose wrong type of function for recovery of existence constraint data");
 }
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_P(DurabilityTest, EdgeTypeIndexRecovered) {
+  if (GetParam() == false) {
+    return;
+  }
+  // Create snapshot.
+  {
+    memgraph::storage::Config config{.salient.items = {.properties_on_edges = GetParam()},
+                                     .durability = {.storage_directory = storage_directory, .snapshot_on_exit = true}};
+    memgraph::replication::ReplicationState repl_state{memgraph::storage::ReplicationStateRootPath(config)};
+    memgraph::dbms::Database db{config, repl_state};
+    CreateBaseDataset(db.storage(), GetParam());
+    VerifyDataset(db.storage(), DatasetType::ONLY_BASE, GetParam());
+    CreateEdgeIndex(db.storage(), db.storage()->NameToEdgeType("base_et1"));
+    VerifyDataset(db.storage(), DatasetType::BASE_WITH_EDGE_TYPE_INDEXED, GetParam());
+  }
+
+  ASSERT_EQ(GetSnapshotsList().size(), 1);
+  ASSERT_EQ(GetBackupSnapshotsList().size(), 0);
+  ASSERT_EQ(GetWalsList().size(), 0);
+  ASSERT_EQ(GetBackupWalsList().size(), 0);
+
+  // Recover snapshot.
+  memgraph::storage::Config config{.salient.items = {.properties_on_edges = GetParam()},
+                                   .durability = {.storage_directory = storage_directory, .recover_on_startup = true}};
+  memgraph::replication::ReplicationState repl_state{memgraph::storage::ReplicationStateRootPath(config)};
+  memgraph::dbms::Database db{config, repl_state};
+  VerifyDataset(db.storage(), DatasetType::BASE_WITH_EDGE_TYPE_INDEXED, GetParam());
+
+  // Try to use the storage.
+  {
+    auto acc = db.Access();
+    auto vertex = acc->CreateVertex();
+    auto edge = acc->CreateEdge(&vertex, &vertex, db.storage()->NameToEdgeType("et"));
+    ASSERT_TRUE(edge.HasValue());
+    ASSERT_FALSE(acc->Commit().HasError());
+  }
+}
diff --git a/tests/unit/storage_v2_indices.cpp b/tests/unit/storage_v2_indices.cpp
index 8ee053087..23c82313d 100644
--- a/tests/unit/storage_v2_indices.cpp
+++ b/tests/unit/storage_v2_indices.cpp
@@ -44,6 +44,8 @@ class IndexTest : public testing::Test {
     this->prop_val = acc->NameToProperty("val");
     this->label1 = acc->NameToLabel("label1");
     this->label2 = acc->NameToLabel("label2");
+    this->edge_type_id1 = acc->NameToEdgeType("edge_type_1");
+    this->edge_type_id2 = acc->NameToEdgeType("edge_type_2");
     vertex_id = 0;
   }
 
@@ -61,6 +63,8 @@ class IndexTest : public testing::Test {
   PropertyId prop_val;
   LabelId label1;
   LabelId label2;
+  EdgeTypeId edge_type_id1;
+  EdgeTypeId edge_type_id2;
 
   VertexAccessor CreateVertex(Storage::Accessor *accessor) {
     VertexAccessor vertex = accessor->CreateVertex();
@@ -68,11 +72,23 @@ class IndexTest : public testing::Test {
     return vertex;
   }
 
+  VertexAccessor CreateVertexWithoutProperties(Storage::Accessor *accessor) {
+    VertexAccessor vertex = accessor->CreateVertex();
+    return vertex;
+  }
+
+  EdgeAccessor CreateEdge(VertexAccessor *from, VertexAccessor *to, EdgeTypeId edge_type, Storage::Accessor *accessor) {
+    auto edge = accessor->CreateEdge(from, to, edge_type);
+    MG_ASSERT(!edge.HasError());
+    MG_ASSERT(!edge->SetProperty(this->prop_id, PropertyValue(vertex_id++)).HasError());
+    return edge.GetValue();
+  }
+
   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(this->prop_id, view)->ValueInt());
+    for (auto item : iterable) {
+      ret.push_back(item.GetProperty(this->prop_id, view)->ValueInt());
     }
     return ret;
   }
@@ -1292,3 +1308,368 @@ TYPED_TEST(IndexTest, LabelPropertyIndexClearOldDataFromDisk) {
     ASSERT_EQ(disk_test_utils::GetRealNumberOfEntriesInRocksDB(tx_db), 1);
   }
 }
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexCreate) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_FALSE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_EQ(acc->ListAllIndices().edge_type.size(), 0);
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 0; i < 10; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+      ASSERT_NO_ERROR(acc->Commit());
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 10; i < 20; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+
+      acc->AdvanceCommand();
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+
+      acc->Abort();
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 10; i < 20; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+      acc->AdvanceCommand();
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+      ASSERT_NO_ERROR(acc->Commit());
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+
+      acc->AdvanceCommand();
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 21, 23, 25, 27, 29));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, 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)
+TYPED_TEST(IndexTest, EdgeTypeIndexDrop) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_FALSE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_EQ(acc->ListAllIndices().edge_type.size(), 0);
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 0; i < 10; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+      ASSERT_NO_ERROR(acc->Commit());
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9));
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->DropIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_FALSE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_EQ(acc->ListAllIndices().label.size(), 0);
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_TRUE(unique_acc->DropIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_FALSE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_EQ(acc->ListAllIndices().label.size(), 0);
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      for (int i = 10; i < 20; ++i) {
+        auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+        auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+        this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+      }
+      ASSERT_NO_ERROR(acc->Commit());
+    }
+
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+      EXPECT_TRUE(acc->EdgeTypeIndexExists(this->edge_type_id1));
+      EXPECT_THAT(acc->ListAllIndices().edge_type, UnorderedElementsAre(this->edge_type_id1));
+    }
+
+    {
+      auto acc = this->storage->Access(ReplicationRole::MAIN);
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+
+      acc->AdvanceCommand();
+
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+      EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                  UnorderedElementsAre(1, 3, 5, 7, 9, 11, 13, 15, 17, 19));
+    }
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexBasic) {
+  // The following steps are performed and index correctness is validated after
+  // each step:
+  // 1. Create 10 edges numbered from 0 to 9.
+  // 2. Add EdgeType1 to odd numbered, and EdgeType2 to even numbered edges.
+  // 3. Delete even numbered edges.
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id2).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    auto acc = this->storage->Access(ReplicationRole::MAIN);
+    EXPECT_THAT(acc->ListAllIndices().edge_type, UnorderedElementsAre(this->edge_type_id1, this->edge_type_id2));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW), IsEmpty());
+
+    for (int i = 0; i < 10; ++i) {
+      auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+      auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+      this->CreateEdge(&vertex_from, &vertex_to, i % 2 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+    }
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 2, 4, 6, 8));
+
+    acc->AdvanceCommand();
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD),
+                UnorderedElementsAre(0, 2, 4, 6, 8));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 2, 4, 6, 8));
+
+    for (auto vertex : acc->Vertices(View::OLD)) {
+      auto edges = vertex.OutEdges(View::OLD)->edges;
+      for (auto &edge : edges) {
+        int64_t id = edge.GetProperty(this->prop_id, View::OLD)->ValueInt();
+        if (id % 2 == 0) {
+          ASSERT_NO_ERROR(acc->DetachDelete({}, {&edge}, false));
+        }
+      }
+    }
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD),
+                UnorderedElementsAre(0, 2, 4, 6, 8));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW), IsEmpty());
+
+    acc->AdvanceCommand();
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::OLD), View::OLD),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::OLD), View::OLD), IsEmpty());
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(1, 3, 5, 7, 9));
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id2, View::NEW), View::NEW), IsEmpty());
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexTransactionalIsolation) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    // Check that transactions only see entries they are supposed to see.
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id2).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    auto acc_before = this->storage->Access(ReplicationRole::MAIN);
+    auto acc = this->storage->Access(ReplicationRole::MAIN);
+    auto acc_after = this->storage->Access(ReplicationRole::MAIN);
+
+    for (int i = 0; i < 5; ++i) {
+      auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+      auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+      this->CreateEdge(&vertex_from, &vertex_to, this->edge_type_id1, acc.get());
+    }
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 1, 2, 3, 4));
+
+    EXPECT_THAT(this->GetIds(acc_before->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+
+    EXPECT_THAT(this->GetIds(acc_after->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+
+    ASSERT_NO_ERROR(acc->Commit());
+
+    auto acc_after_commit = this->storage->Access(ReplicationRole::MAIN);
+
+    EXPECT_THAT(this->GetIds(acc_before->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+
+    EXPECT_THAT(this->GetIds(acc_after->Edges(this->edge_type_id1, View::NEW), View::NEW), IsEmpty());
+
+    EXPECT_THAT(this->GetIds(acc_after_commit->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 1, 2, 3, 4));
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexCountEstimate) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id2).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    auto acc = this->storage->Access(ReplicationRole::MAIN);
+    for (int i = 0; i < 20; ++i) {
+      auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+      auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+      this->CreateEdge(&vertex_from, &vertex_to, i % 3 ? this->edge_type_id1 : this->edge_type_id2, acc.get());
+    }
+
+    EXPECT_EQ(acc->ApproximateEdgeCount(this->edge_type_id1), 13);
+    EXPECT_EQ(acc->ApproximateEdgeCount(this->edge_type_id2), 7);
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TYPED_TEST(IndexTest, EdgeTypeIndexRepeatingEdgeTypesBetweenSameVertices) {
+  if constexpr ((std::is_same_v<TypeParam, memgraph::storage::InMemoryStorage>)) {
+    {
+      auto unique_acc = this->storage->UniqueAccess(ReplicationRole::MAIN);
+      EXPECT_FALSE(unique_acc->CreateIndex(this->edge_type_id1).HasError());
+      ASSERT_NO_ERROR(unique_acc->Commit());
+    }
+
+    auto acc = this->storage->Access(ReplicationRole::MAIN);
+    auto vertex_from = this->CreateVertexWithoutProperties(acc.get());
+    auto vertex_to = this->CreateVertexWithoutProperties(acc.get());
+
+    for (int i = 0; i < 5; ++i) {
+      this->CreateEdge(&vertex_from, &vertex_to, this->edge_type_id1, acc.get());
+    }
+
+    EXPECT_EQ(acc->ApproximateEdgeCount(this->edge_type_id1), 5);
+
+    EXPECT_THAT(this->GetIds(acc->Edges(this->edge_type_id1, View::NEW), View::NEW),
+                UnorderedElementsAre(0, 1, 2, 3, 4));
+  }
+}
diff --git a/tests/unit/storage_v2_wal_file.cpp b/tests/unit/storage_v2_wal_file.cpp
index 07a35d754..dcb7d3326 100644
--- a/tests/unit/storage_v2_wal_file.cpp
+++ b/tests/unit/storage_v2_wal_file.cpp
@@ -37,6 +37,10 @@ memgraph::storage::durability::WalDeltaData::Type StorageMetadataOperationToWalD
       return memgraph::storage::durability::WalDeltaData::Type::LABEL_INDEX_CREATE;
     case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_DROP:
       return memgraph::storage::durability::WalDeltaData::Type::LABEL_INDEX_DROP;
+    case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+      return memgraph::storage::durability::WalDeltaData::Type::EDGE_INDEX_CREATE;
+    case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_DROP:
+      return memgraph::storage::durability::WalDeltaData::Type::EDGE_INDEX_DROP;
     case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_STATS_SET:
       return memgraph::storage::durability::WalDeltaData::Type::LABEL_INDEX_STATS_SET;
     case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_STATS_CLEAR:
@@ -280,6 +284,41 @@ class DeltaGenerator final {
         case memgraph::storage::durability::StorageMetadataOperation::UNIQUE_CONSTRAINT_DROP:
           data.operation_label_properties.label = label;
           data.operation_label_properties.properties = properties;
+          break;
+        case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_DROP:
+          MG_ASSERT(false, "Invalid function call!");
+      }
+      data_.emplace_back(timestamp_, data);
+    }
+  }
+
+  void AppendEdgeTypeOperation(memgraph::storage::durability::StorageMetadataOperation operation,
+                               const std::string &edge_type) {
+    auto edge_type_id = memgraph::storage::EdgeTypeId::FromUint(mapper_.NameToId(edge_type));
+    wal_file_.AppendOperation(operation, edge_type_id, timestamp_);
+    if (valid_) {
+      UpdateStats(timestamp_, 1);
+      memgraph::storage::durability::WalDeltaData data;
+      data.type = StorageMetadataOperationToWalDeltaDataType(operation);
+      switch (operation) {
+        case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::EDGE_TYPE_INDEX_DROP:
+          data.operation_edge_type.edge_type = edge_type;
+          break;
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_DROP:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_STATS_CLEAR:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_CLEAR:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_INDEX_STATS_SET:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_DROP:
+        case memgraph::storage::durability::StorageMetadataOperation::EXISTENCE_CONSTRAINT_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::EXISTENCE_CONSTRAINT_DROP:;
+        case memgraph::storage::durability::StorageMetadataOperation::LABEL_PROPERTY_INDEX_STATS_SET:
+        case memgraph::storage::durability::StorageMetadataOperation::UNIQUE_CONSTRAINT_CREATE:
+        case memgraph::storage::durability::StorageMetadataOperation::UNIQUE_CONSTRAINT_DROP:
+          MG_ASSERT(false, "Invalid function call!");
       }
       data_.emplace_back(timestamp_, data);
     }