From 0876a8848d993071c7661b13f0a2861750338cb6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Boris=20Ta=C5=A1evski?=
 <36607228+BorisTasevski@users.noreply.github.com>
Date: Wed, 14 Sep 2022 18:36:21 +0200
Subject: [PATCH] Merge master to epic and fix differences (#552)

---
 src/glue/communication.cpp                    |  28 ++
 src/glue/communication.hpp                    |   9 +
 src/query/CMakeLists.txt                      |   4 +-
 src/query/common.cpp                          |   1 +
 src/query/db_accessor.cpp                     | 148 ++++++++
 src/query/db_accessor.hpp                     | 188 +++++++++--
 src/query/frontend/ast/ast.lcp                |   5 +-
 .../frontend/ast/cypher_main_visitor.cpp      |   4 +
 src/query/graph.cpp                           |  73 ++++
 src/query/graph.hpp                           | 114 +++++++
 .../interpret/awesome_memgraph_functions.cpp  |   2 +
 src/query/interpret/eval.hpp                  |  27 ++
 src/query/path.hpp                            |   6 +
 src/query/plan/operator.cpp                   |  38 ++-
 src/query/procedure/mg_procedure_impl.cpp     | 307 +++++++++++++----
 src/query/procedure/mg_procedure_impl.hpp     |  91 +++--
 src/query/typed_value.cpp                     |  21 ++
 src/query/typed_value.hpp                     |  22 +-
 tests/e2e/write_procedures/CMakeLists.txt     |   1 +
 tests/e2e/write_procedures/procedures/read.py |  59 ++++
 .../e2e/write_procedures/procedures/write.py  |  53 ++-
 tests/e2e/write_procedures/read_subgraph.py   | 319 ++++++++++++++++++
 tests/e2e/write_procedures/workloads.yaml     |   5 +
 .../memgraph_V1/features/aggregations.feature |  58 ++++
 tests/unit/formatters.hpp                     |   2 +
 tests/unit/query_procedures_mgp_graph.cpp     |  19 +-
 26 files changed, 1452 insertions(+), 152 deletions(-)
 create mode 100644 src/query/db_accessor.cpp
 create mode 100644 src/query/graph.cpp
 create mode 100644 src/query/graph.hpp
 create mode 100644 tests/e2e/write_procedures/read_subgraph.py

diff --git a/src/glue/communication.cpp b/src/glue/communication.cpp
index 90cf87c56..c8f7c1879 100644
--- a/src/glue/communication.cpp
+++ b/src/glue/communication.cpp
@@ -127,6 +127,10 @@ storage::Result<Value> ToBoltValue(const query::TypedValue &value, const storage
       return Value(value.ValueLocalDateTime());
     case query::TypedValue::Type::Duration:
       return Value(value.ValueDuration());
+    case query::TypedValue::Type::Graph:
+      auto maybe_graph = ToBoltGraph(value.ValueGraph(), db, view);
+      if (maybe_graph.HasError()) return maybe_graph.GetError();
+      return Value(std::move(*maybe_graph));
   }
 }
 
@@ -183,6 +187,30 @@ storage::Result<communication::bolt::Path> ToBoltPath(const query::Path &path, c
   return communication::bolt::Path(vertices, edges);
 }
 
+storage::Result<std::map<std::string, Value>> ToBoltGraph(const query::Graph &graph, const storage::Storage &db,
+                                                          storage::View view) {
+  std::map<std::string, Value> map;
+  std::vector<Value> vertices;
+  vertices.reserve(graph.vertices().size());
+  for (const auto &v : graph.vertices()) {
+    auto maybe_vertex = ToBoltVertex(v, db, view);
+    if (maybe_vertex.HasError()) return maybe_vertex.GetError();
+    vertices.emplace_back(Value(std::move(*maybe_vertex)));
+  }
+  map.emplace("nodes", Value(vertices));
+
+  std::vector<Value> edges;
+  edges.reserve(graph.edges().size());
+  for (const auto &e : graph.edges()) {
+    auto maybe_edge = ToBoltEdge(e, db, view);
+    if (maybe_edge.HasError()) return maybe_edge.GetError();
+    edges.emplace_back(Value(std::move(*maybe_edge)));
+  }
+  map.emplace("edges", Value(edges));
+
+  return std::move(map);
+}
+
 storage::PropertyValue ToPropertyValue(const Value &value) {
   switch (value.type()) {
     case Value::Type::Null:
diff --git a/src/glue/communication.hpp b/src/glue/communication.hpp
index 6e5fd32c5..0e3b39f4d 100644
--- a/src/glue/communication.hpp
+++ b/src/glue/communication.hpp
@@ -51,6 +51,15 @@ storage::Result<communication::bolt::Edge> ToBoltEdge(const storage::EdgeAccesso
 storage::Result<communication::bolt::Path> ToBoltPath(const query::Path &path, const storage::Storage &db,
                                                       storage::View view);
 
+/// @param query::Graph for converting to communication::bolt::Map.
+/// @param storage::Storage for ToBoltVertex and ToBoltEdge.
+/// @param storage::View for ToBoltVertex and ToBoltEdge.
+///
+/// @throw std::bad_alloc
+storage::Result<std::map<std::string, communication::bolt::Value>> ToBoltGraph(const query::Graph &graph,
+                                                                               const storage::Storage &db,
+                                                                               storage::View view);
+
 /// @param query::TypedValue for converting to communication::bolt::Value.
 /// @param storage::Storage for ToBoltVertex and ToBoltEdge.
 /// @param storage::View for ToBoltVertex and ToBoltEdge.
diff --git a/src/query/CMakeLists.txt b/src/query/CMakeLists.txt
index 0303f2fa5..77a12a430 100644
--- a/src/query/CMakeLists.txt
+++ b/src/query/CMakeLists.txt
@@ -39,7 +39,9 @@ set(mg_query_sources
     stream/common.cpp
     trigger.cpp
     trigger_context.cpp
-    typed_value.cpp)
+    typed_value.cpp
+    graph.cpp
+    db_accessor.cpp)
 
 find_package(Boost REQUIRED)
 
diff --git a/src/query/common.cpp b/src/query/common.cpp
index 0bf56baa0..793ae8044 100644
--- a/src/query/common.cpp
+++ b/src/query/common.cpp
@@ -61,6 +61,7 @@ bool TypedValueCompare(const TypedValue &a, const TypedValue &b) {
     case TypedValue::Type::Vertex:
     case TypedValue::Type::Edge:
     case TypedValue::Type::Path:
+    case TypedValue::Type::Graph:
       throw QueryRuntimeException("Comparison is not defined for values of type {}.", a.type());
     case TypedValue::Type::Null:
       LOG_FATAL("Invalid type");
diff --git a/src/query/db_accessor.cpp b/src/query/db_accessor.cpp
new file mode 100644
index 000000000..bbb9ac09a
--- /dev/null
+++ b/src/query/db_accessor.cpp
@@ -0,0 +1,148 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include "query/db_accessor.hpp"
+
+#include "query/graph.hpp"
+
+#include <cppitertools/filter.hpp>
+#include <cppitertools/imap.hpp>
+#include "utils/pmr/unordered_set.hpp"
+
+namespace memgraph::query {
+SubgraphDbAccessor::SubgraphDbAccessor(query::DbAccessor db_accessor, Graph *graph)
+    : db_accessor_(db_accessor), graph_(graph) {}
+
+storage::PropertyId SubgraphDbAccessor::NameToProperty(const std::string_view name) {
+  return db_accessor_.NameToProperty(name);
+}
+
+storage::LabelId SubgraphDbAccessor::NameToLabel(const std::string_view name) { return db_accessor_.NameToLabel(name); }
+
+storage::EdgeTypeId SubgraphDbAccessor::NameToEdgeType(const std::string_view name) {
+  return db_accessor_.NameToEdgeType(name);
+}
+
+const std::string &SubgraphDbAccessor::PropertyToName(storage::PropertyId prop) const {
+  return db_accessor_.PropertyToName(prop);
+}
+
+const std::string &SubgraphDbAccessor::LabelToName(storage::LabelId label) const {
+  return db_accessor_.LabelToName(label);
+}
+
+const std::string &SubgraphDbAccessor::EdgeTypeToName(storage::EdgeTypeId type) const {
+  return db_accessor_.EdgeTypeToName(type);
+}
+
+storage::Result<std::optional<EdgeAccessor>> SubgraphDbAccessor::RemoveEdge(EdgeAccessor *edge) {
+  if (!this->graph_->ContainsEdge(*edge)) {
+    throw std::logic_error{"Projected graph must contain edge!"};
+  }
+  auto result = db_accessor_.RemoveEdge(edge);
+  if (result.HasError() || !*result) {
+    return result;
+  }
+  return this->graph_->RemoveEdge(*edge);
+}
+
+storage::Result<EdgeAccessor> SubgraphDbAccessor::InsertEdge(SubgraphVertexAccessor *from, SubgraphVertexAccessor *to,
+                                                             const storage::EdgeTypeId &edge_type) {
+  VertexAccessor *from_impl = &from->impl_;
+  VertexAccessor *to_impl = &to->impl_;
+  if (!this->graph_->ContainsVertex(*from_impl) || !this->graph_->ContainsVertex(*to_impl)) {
+    throw std::logic_error{"Projected graph must contain both vertices to insert edge!"};
+  }
+  auto result = db_accessor_.InsertEdge(from_impl, to_impl, edge_type);
+  if (result.HasError()) {
+    return result;
+  }
+  this->graph_->InsertEdge(*result);
+  return result;
+}
+
+storage::Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>>
+SubgraphDbAccessor::DetachRemoveVertex(  // NOLINT(readability-convert-member-functions-to-static)
+    SubgraphVertexAccessor *) {          // NOLINT(hicpp-named-parameter)
+  throw std::logic_error{
+      "Vertex holds only partial information about edges. Cannot detach delete safely while using projected graph."};
+}
+
+storage::Result<std::optional<VertexAccessor>> SubgraphDbAccessor::RemoveVertex(
+    SubgraphVertexAccessor *subgraphvertex_accessor) {
+  VertexAccessor *vertex_accessor = &subgraphvertex_accessor->impl_;
+  if (!this->graph_->ContainsVertex(*vertex_accessor)) {
+    throw std::logic_error{"Projected graph must contain vertex!"};
+  }
+  auto result = db_accessor_.RemoveVertex(vertex_accessor);
+  if (result.HasError() || !*result) {
+    return result;
+  }
+  return this->graph_->RemoveVertex(*vertex_accessor);
+}
+
+SubgraphVertexAccessor SubgraphDbAccessor::InsertVertex() {
+  VertexAccessor vertex = db_accessor_.InsertVertex();
+  this->graph_->InsertVertex(vertex);
+  return SubgraphVertexAccessor(vertex, this->getGraph());
+}
+
+VerticesIterable SubgraphDbAccessor::Vertices(storage::View) {  // NOLINT(hicpp-named-parameter)
+  return VerticesIterable(&graph_->vertices());
+}
+
+std::optional<VertexAccessor> SubgraphDbAccessor::FindVertex(storage::Gid gid, storage::View view) {
+  std::optional<VertexAccessor> maybe_vertex = db_accessor_.FindVertex(gid, view);
+  if (maybe_vertex && this->graph_->ContainsVertex(*maybe_vertex)) {
+    return *maybe_vertex;
+  }
+  return std::nullopt;
+}
+
+query::Graph *SubgraphDbAccessor::getGraph() { return graph_; }
+
+VertexAccessor SubgraphVertexAccessor::GetVertexAccessor() const { return impl_; }
+
+auto SubgraphVertexAccessor::OutEdges(storage::View view) const -> decltype(impl_.OutEdges(view)) {
+  auto maybe_edges = impl_.impl_.OutEdges(view, {});
+  if (maybe_edges.HasError()) return maybe_edges.GetError();
+  auto edges = std::move(*maybe_edges);
+  auto graph_edges = graph_->edges();
+
+  std::vector<storage::EdgeAccessor> filteredOutEdges;
+  for (auto &edge : edges) {
+    auto edge_q = EdgeAccessor(edge);
+    if (graph_edges.contains(edge_q)) {
+      filteredOutEdges.push_back(edge);
+    }
+  }
+
+  return iter::imap(VertexAccessor::MakeEdgeAccessor, std::move(filteredOutEdges));
+}
+
+auto SubgraphVertexAccessor::InEdges(storage::View view) const -> decltype(impl_.InEdges(view)) {
+  auto maybe_edges = impl_.impl_.InEdges(view, {});
+  if (maybe_edges.HasError()) return maybe_edges.GetError();
+  auto edges = std::move(*maybe_edges);
+  auto graph_edges = graph_->edges();
+
+  std::vector<storage::EdgeAccessor> filteredOutEdges;
+  for (auto &edge : edges) {
+    auto edge_q = EdgeAccessor(edge);
+    if (graph_edges.contains(edge_q)) {
+      filteredOutEdges.push_back(edge);
+    }
+  }
+
+  return iter::imap(VertexAccessor::MakeEdgeAccessor, std::move(filteredOutEdges));
+}
+
+}  // namespace memgraph::query
diff --git a/src/query/db_accessor.hpp b/src/query/db_accessor.hpp
index 25415e6a1..ecd719e78 100644
--- a/src/query/db_accessor.hpp
+++ b/src/query/db_accessor.hpp
@@ -20,6 +20,8 @@
 #include "storage/v2/id_types.hpp"
 #include "storage/v2/property_value.hpp"
 #include "storage/v2/result.hpp"
+#include "utils/pmr/unordered_set.hpp"
+#include "utils/variant_helpers.hpp"
 
 ///////////////////////////////////////////////////////////
 // Our communication layer and query engine don't mix
@@ -45,6 +47,7 @@
 
 namespace memgraph::query {
 
+class Graph;
 class VertexAccessor;
 
 class EdgeAccessor final {
@@ -185,38 +188,123 @@ inline VertexAccessor EdgeAccessor::From() const { return VertexAccessor(impl_.F
 
 inline bool EdgeAccessor::IsCycle() const { return To() == From(); }
 
-class DbAccessor final {
-  storage::Storage::Accessor *accessor_;
+class SubgraphVertexAccessor final {
+ public:
+  query::VertexAccessor impl_;
+  query::Graph *graph_;
 
-  class VerticesIterable final {
-    storage::VerticesIterable iterable_;
+  explicit SubgraphVertexAccessor(query::VertexAccessor impl, query::Graph *graph_) : impl_(impl), graph_(graph_) {}
+
+  bool operator==(const SubgraphVertexAccessor &v) const noexcept {
+    static_assert(noexcept(impl_ == v.impl_));
+    return impl_ == v.impl_;
+  }
+
+  auto InEdges(storage::View view) const -> decltype(impl_.OutEdges(view));
+
+  auto OutEdges(storage::View view) const -> decltype(impl_.OutEdges(view));
+
+  auto Labels(storage::View view) const { return impl_.Labels(view); }
+
+  storage::Result<bool> AddLabel(storage::LabelId label) { return impl_.AddLabel(label); }
+
+  storage::Result<bool> RemoveLabel(storage::LabelId label) { return impl_.RemoveLabel(label); }
+
+  storage::Result<bool> HasLabel(storage::View view, storage::LabelId label) const {
+    return impl_.HasLabel(view, label);
+  }
+
+  auto Properties(storage::View view) const { return impl_.Properties(view); }
+
+  storage::Result<storage::PropertyValue> GetProperty(storage::View view, storage::PropertyId key) const {
+    return impl_.GetProperty(view, key);
+  }
+
+  storage::Gid Gid() const noexcept { return impl_.Gid(); }
+
+  storage::Result<storage::PropertyValue> SetProperty(storage::PropertyId key, const storage::PropertyValue &value) {
+    return impl_.SetProperty(key, value);
+  }
+  VertexAccessor GetVertexAccessor() const;
+};
+}  // namespace memgraph::query
+
+namespace std {
+
+template <>
+struct hash<memgraph::query::VertexAccessor> {
+  size_t operator()(const memgraph::query::VertexAccessor &v) const { return std::hash<decltype(v.impl_)>{}(v.impl_); }
+};
+
+template <>
+struct hash<memgraph::query::EdgeAccessor> {
+  size_t operator()(const memgraph::query::EdgeAccessor &e) const { return std::hash<decltype(e.impl_)>{}(e.impl_); }
+};
+
+}  // namespace std
+
+namespace memgraph::query {
+
+class VerticesIterable final {
+  std::variant<storage::VerticesIterable, std::unordered_set<VertexAccessor, std::hash<VertexAccessor>,
+                                                             std::equal_to<void>, utils::Allocator<VertexAccessor>> *>
+      iterable_;
+
+ public:
+  class Iterator final {
+    std::variant<storage::VerticesIterable::Iterator,
+                 std::unordered_set<VertexAccessor, std::hash<VertexAccessor>, std::equal_to<void>,
+                                    utils::Allocator<VertexAccessor>>::iterator>
+        it_;
 
    public:
-    class Iterator final {
-      storage::VerticesIterable::Iterator it_;
+    explicit Iterator(storage::VerticesIterable::Iterator it) : it_(it) {}
+    explicit Iterator(std::unordered_set<VertexAccessor, std::hash<VertexAccessor>, std::equal_to<void>,
+                                         utils::Allocator<VertexAccessor>>::iterator it)
+        : it_(it) {}
 
-     public:
-      explicit Iterator(storage::VerticesIterable::Iterator it) : it_(it) {}
+    VertexAccessor operator*() const {
+      return std::visit([](auto it_) { return VertexAccessor(*it_); }, it_);
+    }
 
-      VertexAccessor operator*() const { return VertexAccessor(*it_); }
+    Iterator &operator++() {
+      std::visit([this](auto it_) { this->it_ = ++it_; }, it_);
+      return *this;
+    }
 
-      Iterator &operator++() {
-        ++it_;
-        return *this;
-      }
+    bool operator==(const Iterator &other) const { return it_ == other.it_; }
 
-      bool operator==(const Iterator &other) const { return it_ == other.it_; }
-
-      bool operator!=(const Iterator &other) const { return !(other == *this); }
-    };
-
-    explicit VerticesIterable(storage::VerticesIterable iterable) : iterable_(std::move(iterable)) {}
-
-    Iterator begin() { return Iterator(iterable_.begin()); }
-
-    Iterator end() { return Iterator(iterable_.end()); }
+    bool operator!=(const Iterator &other) const { return !(other == *this); }
   };
 
+  explicit VerticesIterable(storage::VerticesIterable iterable) : iterable_(std::move(iterable)) {}
+  explicit VerticesIterable(std::unordered_set<VertexAccessor, std::hash<VertexAccessor>, std::equal_to<void>,
+                                               utils::Allocator<VertexAccessor>> *vertices)
+      : iterable_(vertices) {}
+
+  Iterator begin() {
+    return std::visit(memgraph::utils::Overloaded{
+                          [](storage::VerticesIterable &iterable_) { return Iterator(iterable_.begin()); },
+                          [](std::unordered_set<VertexAccessor, std::hash<VertexAccessor>, std::equal_to<void>,
+                                                utils::Allocator<VertexAccessor>> *iterable_) {
+                            return Iterator(iterable_->begin());
+                          }},
+                      iterable_);
+  }
+
+  Iterator end() {
+    return std::visit(
+        memgraph::utils::Overloaded{
+            [](storage::VerticesIterable &iterable_) { return Iterator(iterable_.end()); },
+            [](std::unordered_set<VertexAccessor, std::hash<VertexAccessor>, std::equal_to<void>,
+                                  utils::Allocator<VertexAccessor>> *iterable_) { return Iterator(iterable_->end()); }},
+        iterable_);
+  }
+};
+
+class DbAccessor final {
+  storage::Storage::Accessor *accessor_;
+
  public:
   explicit DbAccessor(storage::Storage::Accessor *accessor) : accessor_(accessor) {}
 
@@ -358,18 +446,44 @@ class DbAccessor final {
   storage::ConstraintsInfo ListAllConstraints() const { return accessor_->ListAllConstraints(); }
 };
 
+class SubgraphDbAccessor final {
+  DbAccessor db_accessor_;
+  Graph *graph_;
+
+ public:
+  explicit SubgraphDbAccessor(DbAccessor db_accessor, Graph *graph);
+
+  static SubgraphDbAccessor *MakeSubgraphDbAccessor(DbAccessor *db_accessor, Graph *graph);
+
+  storage::PropertyId NameToProperty(std::string_view name);
+
+  storage::LabelId NameToLabel(std::string_view name);
+
+  storage::EdgeTypeId NameToEdgeType(std::string_view name);
+
+  const std::string &PropertyToName(storage::PropertyId prop) const;
+
+  const std::string &LabelToName(storage::LabelId label) const;
+
+  const std::string &EdgeTypeToName(storage::EdgeTypeId type) const;
+
+  storage::Result<std::optional<EdgeAccessor>> RemoveEdge(EdgeAccessor *edge);
+
+  storage::Result<EdgeAccessor> InsertEdge(SubgraphVertexAccessor *from, SubgraphVertexAccessor *to,
+                                           const storage::EdgeTypeId &edge_type);
+
+  storage::Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> DetachRemoveVertex(
+      SubgraphVertexAccessor *vertex_accessor);
+
+  storage::Result<std::optional<VertexAccessor>> RemoveVertex(SubgraphVertexAccessor *vertex_accessor);
+
+  SubgraphVertexAccessor InsertVertex();
+
+  VerticesIterable Vertices(storage::View view);
+
+  std::optional<VertexAccessor> FindVertex(storage::Gid gid, storage::View view);
+
+  Graph *getGraph();
+};
+
 }  // namespace memgraph::query
-
-namespace std {
-
-template <>
-struct hash<memgraph::query::VertexAccessor> {
-  size_t operator()(const memgraph::query::VertexAccessor &v) const { return std::hash<decltype(v.impl_)>{}(v.impl_); }
-};
-
-template <>
-struct hash<memgraph::query::EdgeAccessor> {
-  size_t operator()(const memgraph::query::EdgeAccessor &e) const { return std::hash<decltype(e.impl_)>{}(e.impl_); }
-};
-
-}  // namespace std
diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp
index 5ced6ee2f..f78f5f5c3 100644
--- a/src/query/frontend/ast/ast.lcp
+++ b/src/query/frontend/ast/ast.lcp
@@ -464,7 +464,7 @@ cpp<#
                :documentation "Symbol table position of the symbol this Aggregation is mapped to."))
   (:public
     (lcp:define-enum op
-      (count min max sum avg collect-list collect-map)
+      (count min max sum avg collect-list collect-map project)
       (:serialize))
     #>cpp
     Aggregation() = default;
@@ -475,10 +475,11 @@ cpp<#
     static const constexpr char *const kSum = "SUM";
     static const constexpr char *const kAvg = "AVG";
     static const constexpr char *const kCollect = "COLLECT";
+    static const constexpr char *const kProject = "PROJECT";
 
     static std::string OpToString(Op op) {
       const char *op_strings[] = {kCount, kMin,     kMax,    kSum,
-                                  kAvg,   kCollect, kCollect};
+                                  kAvg,   kCollect, kCollect, kProject};
       return op_strings[static_cast<int>(op)];
     }
 
diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp
index 01486cae2..d4c66a7a9 100644
--- a/src/query/frontend/ast/cypher_main_visitor.cpp
+++ b/src/query/frontend/ast/cypher_main_visitor.cpp
@@ -2250,6 +2250,10 @@ antlrcpp::Any CypherMainVisitor::visitFunctionInvocation(MemgraphCypher::Functio
       return static_cast<Expression *>(
           storage_->Create<Aggregation>(expressions[0], nullptr, Aggregation::Op::COLLECT_LIST));
     }
+    if (function_name == Aggregation::kProject) {
+      return static_cast<Expression *>(
+          storage_->Create<Aggregation>(expressions[0], nullptr, Aggregation::Op::PROJECT));
+    }
   }
 
   if (expressions.size() == 2U && function_name == Aggregation::kCollect) {
diff --git a/src/query/graph.cpp b/src/query/graph.cpp
new file mode 100644
index 000000000..8407c15a7
--- /dev/null
+++ b/src/query/graph.cpp
@@ -0,0 +1,73 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#include "query/graph.hpp"
+#include "query/path.hpp"
+
+namespace memgraph::query {
+
+Graph::Graph(utils::MemoryResource *memory) : vertices_(memory), edges_(memory) {}
+
+Graph::Graph(const Graph &other, utils::MemoryResource *memory)
+    : vertices_(other.vertices_, memory), edges_(other.edges_, memory) {}
+
+Graph::Graph(Graph &&other) noexcept : Graph(std::move(other), other.GetMemoryResource()) {}
+
+Graph::Graph(const Graph &other)
+    : Graph(other,
+            std::allocator_traits<allocator_type>::select_on_container_copy_construction(other.GetMemoryResource())
+                .GetMemoryResource()) {}
+
+Graph::Graph(Graph &&other, utils::MemoryResource *memory)
+    : vertices_(std::move(other.vertices_), memory), edges_(std::move(other.edges_), memory) {}
+
+void Graph::Expand(const Path &path) {
+  const auto &path_vertices_ = path.vertices();
+  const auto &path_edges_ = path.edges();
+  std::for_each(path_vertices_.begin(), path_vertices_.end(), [this](const VertexAccessor v) { vertices_.insert(v); });
+  std::for_each(path_edges_.begin(), path_edges_.end(), [this](const EdgeAccessor e) { edges_.insert(e); });
+}
+
+void Graph::InsertVertex(const VertexAccessor &vertex) { vertices_.insert(vertex); }
+
+void Graph::InsertEdge(const EdgeAccessor &edge) { edges_.insert(edge); }
+
+bool Graph::ContainsVertex(const VertexAccessor &vertex) { return vertices_.contains(vertex); }
+
+bool Graph::ContainsEdge(const EdgeAccessor &edge) { return edges_.contains(edge); }
+
+std::optional<VertexAccessor> Graph::RemoveVertex(const VertexAccessor &vertex) {
+  if (!ContainsVertex(vertex)) {
+    return std::nullopt;
+  }
+  auto value = vertices_.erase(vertex);
+  if (value == 0) {
+    return std::nullopt;
+  }
+  return vertex;
+}
+
+std::optional<EdgeAccessor> Graph::RemoveEdge(const EdgeAccessor &edge) {
+  auto value = edges_.erase(edge);
+  if (value == 0) {
+    return std::nullopt;
+  }
+  return edge;
+}
+
+utils::pmr::unordered_set<VertexAccessor> &Graph::vertices() { return vertices_; }
+utils::pmr::unordered_set<EdgeAccessor> &Graph::edges() { return edges_; }
+const utils::pmr::unordered_set<VertexAccessor> &Graph::vertices() const { return vertices_; }
+const utils::pmr::unordered_set<EdgeAccessor> &Graph::edges() const { return edges_; }
+
+utils::MemoryResource *Graph::GetMemoryResource() const { return vertices_.get_allocator().GetMemoryResource(); }
+
+}  // namespace memgraph::query
diff --git a/src/query/graph.hpp b/src/query/graph.hpp
new file mode 100644
index 000000000..801a9f685
--- /dev/null
+++ b/src/query/graph.hpp
@@ -0,0 +1,114 @@
+// Copyright 2022 Memgraph Ltd.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+// License, and you may not use this file except in compliance with the Business Source License.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+#pragma once
+
+#include <functional>
+#include <utility>
+#include "query/db_accessor.hpp"
+#include "utils/logging.hpp"
+#include "utils/memory.hpp"
+#include "utils/pmr/unordered_set.hpp"
+#include "utils/pmr/vector.hpp"
+
+namespace memgraph::query {
+
+class Path;
+/**
+ *  A data structure that holds a graph. A graph consists of at least one
+ * vertex, and zero or more edges.
+ */
+class Graph final {
+ public:
+  /** Allocator type so that STL containers are aware that we need one */
+  using allocator_type = utils::Allocator<Graph>;
+
+  /**
+   * Create the graph with no elements
+   * Allocations are done using the given MemoryResource.
+   */
+  explicit Graph(utils::MemoryResource *memory);
+
+  /**
+   * Construct a copy of other.
+   * utils::MemoryResource is obtained by calling
+   * std::allocator_traits<>::
+   *     select_on_container_copy_construction(other.GetMemoryResource()).
+   * Since we use utils::Allocator, which does not propagate, this means that we
+   * will default to utils::NewDeleteResource().
+   */
+  Graph(const Graph &other);
+
+  /** Construct a copy using the given utils::MemoryResource */
+  Graph(const Graph &other, utils::MemoryResource *memory);
+
+  /**
+   * Construct with the value of other.
+   * utils::MemoryResource is obtained from other. After the move, other will be
+   * empty.
+   */
+  Graph(Graph &&other) noexcept;
+
+  /**
+   * Construct with the value of other, but use the given utils::MemoryResource.
+   * After the move, other may not be empty if `*memory !=
+   * *other.GetMemoryResource()`, because an element-wise move will be
+   * performed.
+   */
+  Graph(Graph &&other, utils::MemoryResource *memory);
+
+  /** Expands the graph with the given path. */
+  void Expand(const Path &path);
+
+  /** Inserts the vertex in the graph. */
+  void InsertVertex(const VertexAccessor &vertex);
+
+  /** Inserts the edge in the graph. */
+  void InsertEdge(const EdgeAccessor &edge);
+
+  /** Checks whether the graph contains the vertex. */
+  bool ContainsVertex(const VertexAccessor &vertex);
+
+  /** Checks whether the graph contains the edge. */
+  bool ContainsEdge(const EdgeAccessor &edge);
+
+  /** Removes the vertex from the graph if the vertex is in the graph. */
+  std::optional<VertexAccessor> RemoveVertex(const VertexAccessor &vertex);
+
+  /** Removes the vertex from the graph if the vertex is in the graph. */
+  std::optional<EdgeAccessor> RemoveEdge(const EdgeAccessor &edge);
+
+  /** Return the out edges of the given vertex. */
+  std::vector<EdgeAccessor> OutEdges(VertexAccessor vertex_accessor);
+
+  /** Copy assign other, utils::MemoryResource of `this` is used */
+  Graph &operator=(const Graph &) = default;
+
+  /** Move assign other, utils::MemoryResource of `this` is used. */
+  Graph &operator=(Graph &&) noexcept = default;
+
+  ~Graph() = default;
+
+  utils::pmr::unordered_set<VertexAccessor> &vertices();
+  utils::pmr::unordered_set<EdgeAccessor> &edges();
+  const utils::pmr::unordered_set<VertexAccessor> &vertices() const;
+  const utils::pmr::unordered_set<EdgeAccessor> &edges() const;
+
+  utils::MemoryResource *GetMemoryResource() const;
+
+ private:
+  // Contains all the vertices in the Graph.
+  utils::pmr::unordered_set<VertexAccessor> vertices_;
+  // Contains all the edges in the Graph
+  utils::pmr::unordered_set<EdgeAccessor> edges_;
+};
+
+}  // namespace memgraph::query
diff --git a/src/query/interpret/awesome_memgraph_functions.cpp b/src/query/interpret/awesome_memgraph_functions.cpp
index 7fae2c190..21d045601 100644
--- a/src/query/interpret/awesome_memgraph_functions.cpp
+++ b/src/query/interpret/awesome_memgraph_functions.cpp
@@ -587,6 +587,8 @@ TypedValue ValueType(const TypedValue *args, int64_t nargs, const FunctionContex
       return TypedValue("LOCAL_DATE_TIME", ctx.memory);
     case TypedValue::Type::Duration:
       return TypedValue("DURATION", ctx.memory);
+    case TypedValue::Type::Graph:
+      throw QueryRuntimeException("Cannot fetch graph as it is not standardized openCypher type name");
   }
 }
 
diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp
index 398d25b5e..1ea20846d 100644
--- a/src/query/interpret/eval.hpp
+++ b/src/query/interpret/eval.hpp
@@ -313,6 +313,25 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
       }
       return std::nullopt;
     };
+    auto maybe_graph = [this](const auto &graph, const auto &prop_name) -> std::optional<TypedValue> {
+      if (prop_name == "nodes") {
+        utils::pmr::vector<TypedValue> vertices(ctx_->memory);
+        vertices.reserve(graph.vertices().size());
+        for (const auto &v : graph.vertices()) {
+          vertices.emplace_back(TypedValue(v, ctx_->memory));
+        }
+        return TypedValue(vertices, ctx_->memory);
+      }
+      if (prop_name == "edges") {
+        utils::pmr::vector<TypedValue> edges(ctx_->memory);
+        edges.reserve(graph.edges().size());
+        for (const auto &e : graph.edges()) {
+          edges.emplace_back(TypedValue(e, ctx_->memory));
+        }
+        return TypedValue(edges, ctx_->memory);
+      }
+      return std::nullopt;
+    };
     switch (expression_result.type()) {
       case TypedValue::Type::Null:
         return TypedValue(ctx_->memory);
@@ -365,6 +384,14 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
         }
         throw QueryRuntimeException("Invalid property name {} for LocalDateTime", prop_name);
       }
+      case TypedValue::Type::Graph: {
+        const auto &prop_name = property_lookup.property_.name;
+        const auto &graph = expression_result.ValueGraph();
+        if (auto graph_field = maybe_graph(graph, prop_name); graph_field) {
+          return std::move(*graph_field);
+        }
+        throw QueryRuntimeException("Invalid property name {} for Graph", prop_name);
+      }
       default:
         throw QueryRuntimeException("Only nodes, edges, maps and temporal types have properties to be looked-up.");
     }
diff --git a/src/query/path.hpp b/src/query/path.hpp
index 832532fbc..43ecb8953 100644
--- a/src/query/path.hpp
+++ b/src/query/path.hpp
@@ -31,6 +31,12 @@ class Path {
   /** Allocator type so that STL containers are aware that we need one */
   using allocator_type = utils::Allocator<char>;
 
+  /**
+   * Create the path with no elements
+   * Allocations are done using the given MemoryResource.
+   */
+  explicit Path(utils::MemoryResource *memory) : vertices_(memory), edges_(memory) {}
+
   /**
    * Create the path starting with the given vertex.
    * Allocations are done using the given MemoryResource.
diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp
index cbc27ccc4..f374a7c1c 100644
--- a/src/query/plan/operator.cpp
+++ b/src/query/plan/operator.cpp
@@ -33,6 +33,7 @@
 #include "query/exceptions.hpp"
 #include "query/frontend/ast/ast.hpp"
 #include "query/frontend/semantic/symbol_table.hpp"
+#include "query/graph.hpp"
 #include "query/interpret/eval.hpp"
 #include "query/path.hpp"
 #include "query/plan/scoped_profile.hpp"
@@ -3143,6 +3144,8 @@ TypedValue DefaultAggregationOpValue(const Aggregate::Element &element, utils::M
       return TypedValue(TypedValue::TVector(memory));
     case Aggregation::Op::COLLECT_MAP:
       return TypedValue(TypedValue::TMap(memory));
+    case Aggregation::Op::PROJECT:
+      return TypedValue(query::Graph(memory));
   }
 }
 }  // namespace
@@ -3305,7 +3308,6 @@ class AggregateCursor : public Cursor {
                "Expected as much AggregationValue.counts_ as there are "
                "aggregations.");
 
-    // we iterate over counts, values and aggregation info at the same time
     auto count_it = agg_value->counts_.begin();
     auto value_it = agg_value->values_.begin();
     auto agg_elem_it = self_.aggregations_.begin();
@@ -3344,6 +3346,11 @@ class AggregateCursor : public Cursor {
           case Aggregation::Op::COLLECT_LIST:
             value_it->ValueList().push_back(input_value);
             break;
+          case Aggregation::Op::PROJECT: {
+            EnsureOkForProject(input_value);
+            value_it->ValueGraph().Expand(input_value.ValuePath());
+            break;
+          }
           case Aggregation::Op::COLLECT_MAP:
             auto key = agg_elem_it->key->Accept(*evaluator);
             if (key.type() != TypedValue::Type::String) throw QueryRuntimeException("Map key must be a string.");
@@ -3392,6 +3399,11 @@ class AggregateCursor : public Cursor {
         case Aggregation::Op::COLLECT_LIST:
           value_it->ValueList().push_back(input_value);
           break;
+        case Aggregation::Op::PROJECT: {
+          EnsureOkForProject(input_value);
+          value_it->ValueGraph().Expand(input_value.ValuePath());
+          break;
+        }
         case Aggregation::Op::COLLECT_MAP:
           auto key = agg_elem_it->key->Accept(*evaluator);
           if (key.type() != TypedValue::Type::String) throw QueryRuntimeException("Map key must be a string.");
@@ -3428,6 +3440,18 @@ class AggregateCursor : public Cursor {
         throw QueryRuntimeException("Only numeric values allowed in SUM and AVG aggregations.");
     }
   }
+
+  /** Checks if the given TypedValue is legal in PROJECT and PROJECT_TRANSITIVE. If not
+   * an appropriate exception is thrown. */
+  // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
+  void EnsureOkForProject(const TypedValue &value) const {
+    switch (value.type()) {
+      case TypedValue::Type::Path:
+        return;
+      default:
+        throw QueryRuntimeException("Only path values allowed in PROJECT aggregation.");
+    }
+  }
 };
 
 UniqueCursorPtr Aggregate::MakeCursor(utils::MemoryResource *mem) const {
@@ -4238,6 +4262,18 @@ void CallCustomProcedure(const std::string_view fully_qualified_procedure_name,
   for (auto *expression : args) {
     args_list.emplace_back(expression->Accept(*evaluator));
   }
+  std::optional<query::Graph> subgraph;
+  std::optional<query::SubgraphDbAccessor> db_acc;
+
+  if (!args_list.empty() && args_list.front().type() == TypedValue::Type::Graph) {
+    auto subgraph_value = args_list.front().ValueGraph();
+    subgraph = query::Graph(std::move(subgraph_value), subgraph_value.GetMemoryResource());
+    args_list.erase(args_list.begin());
+
+    db_acc = query::SubgraphDbAccessor(*std::get<query::DbAccessor *>(graph.impl), &*subgraph);
+    graph.impl = &*db_acc;
+  }
+
   procedure::ConstructArguments(args_list, proc, fully_qualified_procedure_name, proc_args, graph);
   if (memory_limit) {
     SPDLOG_INFO("Running '{}' with memory limit of {}", fully_qualified_procedure_name,
diff --git a/src/query/procedure/mg_procedure_impl.cpp b/src/query/procedure/mg_procedure_impl.cpp
index 03f60bf8b..c01070d52 100644
--- a/src/query/procedure/mg_procedure_impl.cpp
+++ b/src/query/procedure/mg_procedure_impl.cpp
@@ -40,6 +40,9 @@
 #include "utils/temporal.hpp"
 #include "utils/variant_helpers.hpp"
 
+#include <cppitertools/filter.hpp>
+#include <cppitertools/imap.hpp>
+
 // This file contains implementation of top level C API functions, but this is
 // all actually part of memgraph::query::procedure. So use that namespace for simplicity.
 // NOLINTNEXTLINE(google-build-using-namespace)
@@ -299,6 +302,8 @@ mgp_value_type FromTypedValueType(memgraph::query::TypedValue::Type type) {
       return MGP_VALUE_TYPE_LOCAL_DATE_TIME;
     case memgraph::query::TypedValue::Type::Duration:
       return MGP_VALUE_TYPE_DURATION;
+    case memgraph::query::TypedValue::Type::Graph:
+      throw std::logic_error{"mgp_value for TypedValue::Type::Graph doesn't exist."};
   }
 }
 }  // namespace
@@ -333,17 +338,17 @@ memgraph::query::TypedValue ToTypedValue(const mgp_value &val, memgraph::utils::
       return memgraph::query::TypedValue(std::move(tv_map));
     }
     case MGP_VALUE_TYPE_VERTEX:
-      return memgraph::query::TypedValue(val.vertex_v->impl, memory);
+      return memgraph::query::TypedValue(val.vertex_v->getImpl(), memory);
     case MGP_VALUE_TYPE_EDGE:
       return memgraph::query::TypedValue(val.edge_v->impl, memory);
     case MGP_VALUE_TYPE_PATH: {
       const auto *path = val.path_v;
       MG_ASSERT(!path->vertices.empty());
       MG_ASSERT(path->vertices.size() == path->edges.size() + 1);
-      memgraph::query::Path tv_path(path->vertices[0].impl, memory);
+      memgraph::query::Path tv_path(path->vertices[0].getImpl(), memory);
       for (size_t i = 0; i < path->edges.size(); ++i) {
         tv_path.Expand(path->edges[i].impl);
-        tv_path.Expand(path->vertices[i + 1].impl);
+        tv_path.Expand(path->vertices[i + 1].getImpl());
       }
       return memgraph::query::TypedValue(std::move(tv_path));
     }
@@ -463,12 +468,31 @@ mgp_value::mgp_value(const memgraph::query::TypedValue &tv, mgp_graph *graph, me
     }
     case MGP_VALUE_TYPE_VERTEX: {
       memgraph::utils::Allocator<mgp_vertex> allocator(m);
-      vertex_v = allocator.new_object<mgp_vertex>(tv.ValueVertex(), graph);
+      vertex_v = std::visit(
+          memgraph::utils::Overloaded{
+              [&](memgraph::query::DbAccessor *) { return allocator.new_object<mgp_vertex>(tv.ValueVertex(), graph); },
+              [&](memgraph::query::SubgraphDbAccessor *impl) {
+                return allocator.new_object<mgp_vertex>(
+                    memgraph::query::SubgraphVertexAccessor(tv.ValueVertex(), impl->getGraph()), graph);
+              }},
+          graph->impl);
+
       break;
     }
     case MGP_VALUE_TYPE_EDGE: {
       memgraph::utils::Allocator<mgp_edge> allocator(m);
-      edge_v = allocator.new_object<mgp_edge>(tv.ValueEdge(), graph);
+
+      edge_v = std::visit(
+          memgraph::utils::Overloaded{
+              [&tv, graph, &allocator](memgraph::query::DbAccessor *) {
+                return allocator.new_object<mgp_edge>(tv.ValueEdge(), graph);
+              },
+              [&tv, graph, &allocator](memgraph::query::SubgraphDbAccessor *db_impl) {
+                return allocator.new_object<mgp_edge>(
+                    tv.ValueEdge(), memgraph::query::SubgraphVertexAccessor(tv.ValueEdge().From(), db_impl->getGraph()),
+                    memgraph::query::SubgraphVertexAccessor(tv.ValueEdge().To(), db_impl->getGraph()), graph);
+              }},
+          graph->impl);
       break;
     }
     case MGP_VALUE_TYPE_PATH: {
@@ -479,11 +503,24 @@ mgp_value::mgp_value(const memgraph::query::TypedValue &tv, mgp_graph *graph, me
       mgp_path tmp_path(m);
       tmp_path.vertices.reserve(tv.ValuePath().vertices().size());
       for (const auto &v : tv.ValuePath().vertices()) {
-        tmp_path.vertices.emplace_back(v, graph);
+        std::visit(
+            memgraph::utils::Overloaded{
+                [&v, graph, &tmp_path](memgraph::query::DbAccessor *) { tmp_path.vertices.emplace_back(v, graph); },
+                [&v, graph, &tmp_path](memgraph::query::SubgraphDbAccessor *impl) {
+                  tmp_path.vertices.emplace_back(memgraph::query::SubgraphVertexAccessor(v, impl->getGraph()), graph);
+                }},
+            graph->impl);
       }
       tmp_path.edges.reserve(tv.ValuePath().edges().size());
       for (const auto &e : tv.ValuePath().edges()) {
-        tmp_path.edges.emplace_back(e, graph);
+        std::visit(memgraph::utils::Overloaded{
+                       [&e, graph, &tmp_path](memgraph::query::DbAccessor *) { tmp_path.edges.emplace_back(e, graph); },
+                       [&e, graph, &tmp_path](memgraph::query::SubgraphDbAccessor *db_impl) {
+                         tmp_path.edges.emplace_back(
+                             e, memgraph::query::SubgraphVertexAccessor(e.From(), db_impl->getGraph()),
+                             memgraph::query::SubgraphVertexAccessor(e.To(), db_impl->getGraph()), graph);
+                       }},
+                   graph->impl);
       }
       memgraph::utils::Allocator<mgp_path> allocator(m);
       path_v = allocator.new_object<mgp_path>(std::move(tmp_path));
@@ -808,7 +845,15 @@ mgp_value::mgp_value(mgp_value &&other, memgraph::utils::MemoryResource *m) : ty
 mgp_value::~mgp_value() noexcept { DeleteValueMember(this); }
 
 mgp_edge *mgp_edge::Copy(const mgp_edge &edge, mgp_memory &memory) {
-  return NewRawMgpObject<mgp_edge>(&memory, edge.impl, edge.from.graph);
+  return std::visit(
+      memgraph::utils::Overloaded{
+          [&](memgraph::query::DbAccessor *) { return NewRawMgpObject<mgp_edge>(&memory, edge.impl, edge.from.graph); },
+          [&](memgraph::query::SubgraphDbAccessor *db_impl) {
+            return NewRawMgpObject<mgp_edge>(
+                &memory, edge.impl, memgraph::query::SubgraphVertexAccessor(edge.impl.From(), db_impl->getGraph()),
+                memgraph::query::SubgraphVertexAccessor(edge.impl.To(), db_impl->getGraph()), edge.to.graph);
+          }},
+      edge.to.graph->impl);
 }
 
 void mgp_value_destroy(mgp_value *val) { DeleteRawMgpObject(val); }
@@ -1140,7 +1185,7 @@ mgp_error mgp_path_equal(mgp_path *p1, mgp_path *p2, int *result) {
         }
         const auto *start1 = Call<mgp_vertex *>(mgp_path_vertex_at, p1, 0);
         const auto *start2 = Call<mgp_vertex *>(mgp_path_vertex_at, p2, 0);
-        static_assert(noexcept(start1->impl == start2->impl));
+        static_assert(noexcept(start1 == start2));
         if (*start1 != *start2) {
           return 0;
         }
@@ -1507,9 +1552,12 @@ mgp_error mgp_properties_iterator_next(mgp_properties_iterator *it, mgp_property
           return nullptr;
         }
         memgraph::utils::OnScopeExit clean_up([it] { it->current = std::nullopt; });
-        it->current.emplace(memgraph::utils::pmr::string(it->graph->impl->PropertyToName(it->current_it->first),
-                                                         it->GetMemoryResource()),
-                            mgp_value(it->current_it->second, it->GetMemoryResource()));
+        auto propToName = std::visit(
+            [it](auto *impl) {
+              return memgraph::utils::pmr::string(impl->PropertyToName(it->current_it->first), it->GetMemoryResource());
+            },
+            it->graph->impl);
+        it->current.emplace(propToName, mgp_value(it->current_it->second, it->GetMemoryResource()));
         it->property.name = it->current->first.c_str();
         it->property.value = &it->current->second;
         clean_up.Disable();
@@ -1519,7 +1567,9 @@ mgp_error mgp_properties_iterator_next(mgp_properties_iterator *it, mgp_property
 }
 
 mgp_error mgp_vertex_get_id(mgp_vertex *v, mgp_vertex_id *result) {
-  return WrapExceptions([v] { return mgp_vertex_id{.as_int = v->impl.Gid().AsInt()}; }, result);
+  return WrapExceptions(
+      [v] { return mgp_vertex_id{.as_int = std::visit([](auto &impl) { return impl.Gid().AsInt(); }, v->impl)}; },
+      result);
 }
 
 mgp_error mgp_vertex_underlying_graph_is_mutable(mgp_vertex *v, int *result) {
@@ -1592,7 +1642,8 @@ mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_nam
 
 #ifdef MG_ENTERPRISE
     if (memgraph::utils::license::global_license_checker.IsValidLicenseFast() && ctx && ctx->auth_checker &&
-        !ctx->auth_checker->Has(v->impl, v->graph->view, memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE)) {
+        !ctx->auth_checker->Has(v->getImpl(), v->graph->view,
+                                memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE)) {
       throw AuthorizationException{"Insufficient permissions for setting a property on vertex!"};
     }
 #endif
@@ -1600,8 +1651,12 @@ mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_nam
       throw ImmutableObjectException{"Cannot set a property on an immutable vertex!"};
     }
 
-    const auto prop_key = v->graph->impl->NameToProperty(property_name);
-    const auto result = v->impl.SetProperty(prop_key, ToPropertyValue(*property_value));
+    const auto prop_key =
+        std::visit([property_name](auto *impl) { return impl->NameToProperty(property_name); }, v->graph->impl);
+
+    const auto result = std::visit(
+        [prop_key, property_value](auto &impl) { return impl.SetProperty(prop_key, ToPropertyValue(*property_value)); },
+        v->impl);
     if (result.HasError()) {
       switch (result.GetError()) {
         case memgraph::storage::Error::DELETED_OBJECT:
@@ -1625,23 +1680,24 @@ mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_nam
     }
     const auto old_value = memgraph::query::TypedValue(*result);
     if (property_value->type == mgp_value_type::MGP_VALUE_TYPE_NULL) {
-      trigger_ctx_collector->RegisterRemovedObjectProperty(v->impl, prop_key, old_value);
+      trigger_ctx_collector->RegisterRemovedObjectProperty(v->getImpl(), prop_key, old_value);
       return;
     }
     const auto new_value = ToTypedValue(*property_value, property_value->memory);
-    trigger_ctx_collector->RegisterSetObjectProperty(v->impl, prop_key, old_value, new_value);
+    trigger_ctx_collector->RegisterSetObjectProperty(v->getImpl(), prop_key, old_value, new_value);
   });
 }
 
 mgp_error mgp_vertex_add_label(struct mgp_vertex *v, mgp_label label) {
   return WrapExceptions([=] {
     auto *ctx = v->graph->ctx;
+    const auto label_id = std::visit([label](auto *impl) { return impl->NameToLabel(label.name); }, v->graph->impl);
 
 #ifdef MG_ENTERPRISE
     if (memgraph::utils::license::global_license_checker.IsValidLicenseFast() && ctx && ctx->auth_checker &&
-        !(ctx->auth_checker->Has(v->impl, v->graph->view, memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE) &&
-          ctx->auth_checker->Has({v->graph->impl->NameToLabel(label.name)},
-                                 memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE))) {
+        !(ctx->auth_checker->Has(v->getImpl(), v->graph->view,
+                                 memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE) &&
+          ctx->auth_checker->Has({label_id}, memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE))) {
       throw AuthorizationException{"Insufficient permissions for adding a label to vertex!"};
     }
 #endif
@@ -1649,8 +1705,8 @@ mgp_error mgp_vertex_add_label(struct mgp_vertex *v, mgp_label label) {
     if (!MgpVertexIsMutable(*v)) {
       throw ImmutableObjectException{"Cannot add a label to an immutable vertex!"};
     }
-    const auto label_id = v->graph->impl->NameToLabel(label.name);
-    const auto result = v->impl.AddLabel(label_id);
+
+    const auto result = std::visit([label_id](auto &impl) { return impl.AddLabel(label_id); }, v->impl);
 
     if (result.HasError()) {
       switch (result.GetError()) {
@@ -1669,7 +1725,7 @@ mgp_error mgp_vertex_add_label(struct mgp_vertex *v, mgp_label label) {
     ctx->execution_stats[memgraph::query::ExecutionStats::Key::CREATED_LABELS] += 1;
 
     if (ctx->trigger_context_collector) {
-      ctx->trigger_context_collector->RegisterSetVertexLabel(v->impl, label_id);
+      ctx->trigger_context_collector->RegisterSetVertexLabel(v->getImpl(), label_id);
     }
   });
 }
@@ -1677,20 +1733,20 @@ mgp_error mgp_vertex_add_label(struct mgp_vertex *v, mgp_label label) {
 mgp_error mgp_vertex_remove_label(struct mgp_vertex *v, mgp_label label) {
   return WrapExceptions([=] {
     auto *ctx = v->graph->ctx;
+    const auto label_id = std::visit([&label](auto *impl) { return impl->NameToLabel(label.name); }, v->graph->impl);
 
 #ifdef MG_ENTERPRISE
     if (memgraph::utils::license::global_license_checker.IsValidLicenseFast() && ctx && ctx->auth_checker &&
-        !(ctx->auth_checker->Has(v->impl, v->graph->view, memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE) &&
-          ctx->auth_checker->Has({v->graph->impl->NameToLabel(label.name)},
-                                 memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE))) {
+        !(ctx->auth_checker->Has(v->getImpl(), v->graph->view,
+                                 memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE) &&
+          ctx->auth_checker->Has({label_id}, memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE))) {
       throw AuthorizationException{"Insufficient permissions for removing a label from vertex!"};
     }
 #endif
     if (!MgpVertexIsMutable(*v)) {
       throw ImmutableObjectException{"Cannot remove a label from an immutable vertex!"};
     }
-    const auto label_id = v->graph->impl->NameToLabel(label.name);
-    const auto result = v->impl.RemoveLabel(label_id);
+    const auto result = std::visit([label_id](auto &impl) { return impl.RemoveLabel(label_id); }, v->impl);
 
     if (result.HasError()) {
       switch (result.GetError()) {
@@ -1709,7 +1765,7 @@ mgp_error mgp_vertex_remove_label(struct mgp_vertex *v, mgp_label label) {
     ctx->execution_stats[memgraph::query::ExecutionStats::Key::DELETED_LABELS] += 1;
 
     if (ctx->trigger_context_collector) {
-      ctx->trigger_context_collector->RegisterRemovedVertexLabel(v->impl, label_id);
+      ctx->trigger_context_collector->RegisterRemovedVertexLabel(v->getImpl(), label_id);
     }
   });
 }
@@ -1730,7 +1786,7 @@ mgp_error mgp_vertex_equal(mgp_vertex *v1, mgp_vertex *v2, int *result) {
 mgp_error mgp_vertex_labels_count(mgp_vertex *v, size_t *result) {
   return WrapExceptions(
       [v]() -> size_t {
-        auto maybe_labels = v->impl.Labels(v->graph->view);
+        auto maybe_labels = std::visit([v](const auto &impl) { return impl.Labels(v->graph->view); }, v->impl);
         if (maybe_labels.HasError()) {
           switch (maybe_labels.GetError()) {
             case memgraph::storage::Error::DELETED_OBJECT:
@@ -1752,7 +1808,7 @@ mgp_error mgp_vertex_label_at(mgp_vertex *v, size_t i, mgp_label *result) {
   return WrapExceptions(
       [v, i]() -> const char * {
         // TODO: Maybe it's worth caching this in mgp_vertex.
-        auto maybe_labels = v->impl.Labels(v->graph->view);
+        auto maybe_labels = std::visit([v](const auto &impl) { return impl.Labels(v->graph->view); }, v->impl);
         if (maybe_labels.HasError()) {
           switch (maybe_labels.GetError()) {
             case memgraph::storage::Error::DELETED_OBJECT:
@@ -1769,10 +1825,12 @@ mgp_error mgp_vertex_label_at(mgp_vertex *v, size_t i, mgp_label *result) {
           throw std::out_of_range("Label cannot be retrieved, because index exceeds the number of labels!");
         }
         const auto &label = (*maybe_labels)[i];
-        static_assert(std::is_lvalue_reference_v<decltype(v->graph->impl->LabelToName(label))>,
+        static_assert(std::is_lvalue_reference_v<
+                          decltype(std::get<memgraph::query::DbAccessor *>(v->graph->impl)->LabelToName(label))>,
                       "Expected LabelToName to return a pointer or reference, so we "
                       "don't have to take a copy and manage memory.");
-        const auto &name = v->graph->impl->LabelToName(label);
+
+        const auto &name = std::visit([label](const auto *impl) { return impl->LabelToName(label); }, v->graph->impl);
         return name.c_str();
       },
       &result->name);
@@ -1782,9 +1840,10 @@ mgp_error mgp_vertex_has_label_named(mgp_vertex *v, const char *name, int *resul
   return WrapExceptions(
       [v, name] {
         memgraph::storage::LabelId label;
-        label = v->graph->impl->NameToLabel(name);
+        label = std::visit([name](auto *impl) { return impl->NameToLabel(name); }, v->graph->impl);
 
-        auto maybe_has_label = v->impl.HasLabel(v->graph->view, label);
+        auto maybe_has_label =
+            std::visit([v, label](auto &impl) { return impl.HasLabel(v->graph->view, label); }, v->impl);
         if (maybe_has_label.HasError()) {
           switch (maybe_has_label.GetError()) {
             case memgraph::storage::Error::DELETED_OBJECT:
@@ -1812,8 +1871,9 @@ mgp_error mgp_vertex_has_label(mgp_vertex *v, mgp_label label, int *result) {
 mgp_error mgp_vertex_get_property(mgp_vertex *v, const char *name, mgp_memory *memory, mgp_value **result) {
   return WrapExceptions(
       [v, name, memory]() -> mgp_value * {
-        const auto &key = v->graph->impl->NameToProperty(name);
-        auto maybe_prop = v->impl.GetProperty(v->graph->view, key);
+        const auto &key = std::visit([name](auto *impl) { return impl->NameToProperty(name); }, v->graph->impl);
+
+        auto maybe_prop = std::visit([v, key](auto &impl) { return impl.GetProperty(v->graph->view, key); }, v->impl);
         if (maybe_prop.HasError()) {
           switch (maybe_prop.GetError()) {
             case memgraph::storage::Error::DELETED_OBJECT:
@@ -1839,7 +1899,7 @@ mgp_error mgp_vertex_iter_properties(mgp_vertex *v, mgp_memory *memory, mgp_prop
   // will probably require a different API in storage.
   return WrapExceptions(
       [v, memory] {
-        auto maybe_props = v->impl.Properties(v->graph->view);
+        auto maybe_props = std::visit([v](auto &impl) { return impl.Properties(v->graph->view); }, v->impl);
         if (maybe_props.HasError()) {
           switch (maybe_props.GetError()) {
             case memgraph::storage::Error::DELETED_OBJECT:
@@ -1874,7 +1934,8 @@ void NextPermittedEdge(mgp_edges_iterator &it, const bool for_in) {
     const auto view = it.source_vertex.graph->view;
     while (*impl_it != end) {
       if (auth_checker->Has(**impl_it, memgraph::query::AuthQuery::FineGrainedPrivilege::READ)) {
-        const auto &check_vertex = it.source_vertex.impl == (*impl_it)->From() ? (*impl_it)->To() : (*impl_it)->From();
+        const auto &check_vertex =
+            it.source_vertex.getImpl() == (*impl_it)->From() ? (*impl_it)->To() : (*impl_it)->From();
         if (auth_checker->Has(check_vertex, view, memgraph::query::AuthQuery::FineGrainedPrivilege::READ)) {
           break;
         }
@@ -1893,7 +1954,7 @@ mgp_error mgp_vertex_iter_in_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges_
         auto it = NewMgpObject<mgp_edges_iterator>(memory, *v);
         MG_ASSERT(it != nullptr);
 
-        auto maybe_edges = v->impl.InEdges(v->graph->view);
+        auto maybe_edges = std::visit([v](auto &impl) { return impl.InEdges(v->graph->view); }, v->impl);
         if (maybe_edges.HasError()) {
           switch (maybe_edges.GetError()) {
             case memgraph::storage::Error::DELETED_OBJECT:
@@ -1917,7 +1978,19 @@ mgp_error mgp_vertex_iter_in_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges_
 #endif
 
         if (*it->in_it != it->in->end()) {
-          it->current_e.emplace(**it->in_it, v->graph, it->GetMemoryResource());
+          std::visit(memgraph::utils::Overloaded{
+                         [&](memgraph::query::DbAccessor *) {
+                           it->current_e.emplace(**it->in_it, (**it->in_it).From(), (**it->in_it).To(), v->graph,
+                                                 it->GetMemoryResource());
+                         },
+                         [&](memgraph::query::SubgraphDbAccessor *impl) {
+                           it->current_e.emplace(
+                               **it->in_it,
+                               memgraph::query::SubgraphVertexAccessor((**it->in_it).From(), impl->getGraph()),
+                               memgraph::query::SubgraphVertexAccessor((**it->in_it).To(), impl->getGraph()), v->graph,
+                               it->GetMemoryResource());
+                         }},
+                     v->graph->impl);
         }
 
         return it.release();
@@ -1930,8 +2003,8 @@ mgp_error mgp_vertex_iter_out_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges
       [v, memory] {
         auto it = NewMgpObject<mgp_edges_iterator>(memory, *v);
         MG_ASSERT(it != nullptr);
+        auto maybe_edges = std::visit([v](auto &impl) { return impl.OutEdges(v->graph->view); }, v->impl);
 
-        auto maybe_edges = v->impl.OutEdges(v->graph->view);
         if (maybe_edges.HasError()) {
           switch (maybe_edges.GetError()) {
             case memgraph::storage::Error::DELETED_OBJECT:
@@ -1946,6 +2019,7 @@ mgp_error mgp_vertex_iter_out_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges
               LOG_FATAL("Unexpected error when getting the outbound edges of a vertex.");
           }
         }
+
         it->out.emplace(std::move(*maybe_edges));
         it->out_it.emplace(it->out->begin());
 
@@ -1956,7 +2030,19 @@ mgp_error mgp_vertex_iter_out_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges
 #endif
 
         if (*it->out_it != it->out->end()) {
-          it->current_e.emplace(**it->out_it, v->graph, it->GetMemoryResource());
+          std::visit(memgraph::utils::Overloaded{
+                         [&](memgraph::query::DbAccessor *) {
+                           it->current_e.emplace(**it->out_it, (**it->out_it).From(), (**it->out_it).To(), v->graph,
+                                                 it->GetMemoryResource());
+                         },
+                         [&](memgraph::query::SubgraphDbAccessor *impl) {
+                           it->current_e.emplace(
+                               **it->out_it,
+                               memgraph::query::SubgraphVertexAccessor((**it->out_it).From(), impl->getGraph()),
+                               memgraph::query::SubgraphVertexAccessor((**it->out_it).To(), impl->getGraph()), v->graph,
+                               it->GetMemoryResource());
+                         }},
+                     v->graph->impl);
         }
 
         return it.release();
@@ -2005,8 +2091,19 @@ mgp_error mgp_edges_iterator_next(mgp_edges_iterator *it, mgp_edge **result) {
             it->current_e = std::nullopt;
             return nullptr;
           }
+          std::visit(memgraph::utils::Overloaded{
+                         [&](memgraph::query::DbAccessor *) {
+                           it->current_e.emplace(**impl_it, (**impl_it).From(), (**impl_it).To(),
+                                                 it->source_vertex.graph, it->GetMemoryResource());
+                         },
+                         [&](memgraph::query::SubgraphDbAccessor *impl) {
+                           it->current_e.emplace(
+                               **impl_it, memgraph::query::SubgraphVertexAccessor((**impl_it).From(), impl->getGraph()),
+                               memgraph::query::SubgraphVertexAccessor((**impl_it).To(), impl->getGraph()),
+                               it->source_vertex.graph, it->GetMemoryResource());
+                         }},
+                     it->source_vertex.graph->impl);
 
-          it->current_e.emplace(**impl_it, it->source_vertex.graph, it->GetMemoryResource());
           return &*it->current_e;
         };
         if (it->in_it) {
@@ -2044,10 +2141,8 @@ mgp_error mgp_edge_equal(mgp_edge *e1, mgp_edge *e2, int *result) {
 mgp_error mgp_edge_get_type(mgp_edge *e, mgp_edge_type *result) {
   return WrapExceptions(
       [e] {
-        const auto &name = e->from.graph->impl->EdgeTypeToName(e->impl.EdgeType());
-        static_assert(std::is_lvalue_reference_v<decltype(e->from.graph->impl->EdgeTypeToName(e->impl.EdgeType()))>,
-                      "Expected EdgeTypeToName to return a pointer or reference, so we "
-                      "don't have to take a copy and manage memory.");
+        const auto &name =
+            std::visit([e](const auto *impl) { return impl->EdgeTypeToName(e->impl.EdgeType()); }, e->from.graph->impl);
         return name.c_str();
       },
       &result->name);
@@ -2066,7 +2161,7 @@ mgp_error mgp_edge_get_to(mgp_edge *e, mgp_vertex **result) {
 mgp_error mgp_edge_get_property(mgp_edge *e, const char *name, mgp_memory *memory, mgp_value **result) {
   return WrapExceptions(
       [e, name, memory] {
-        const auto &key = e->from.graph->impl->NameToProperty(name);
+        const auto &key = std::visit([name](auto *impl) { return impl->NameToProperty(name); }, e->from.graph->impl);
         auto view = e->from.graph->view;
         auto maybe_prop = e->impl.GetProperty(view, key);
         if (maybe_prop.HasError()) {
@@ -2101,7 +2196,8 @@ mgp_error mgp_edge_set_property(struct mgp_edge *e, const char *property_name, m
     if (!MgpEdgeIsMutable(*e)) {
       throw ImmutableObjectException{"Cannot set a property on an immutable edge!"};
     }
-    const auto prop_key = e->from.graph->impl->NameToProperty(property_name);
+    const auto prop_key =
+        std::visit([property_name](auto *impl) { return impl->NameToProperty(property_name); }, e->from.graph->impl);
     const auto result = e->impl.SetProperty(prop_key, ToPropertyValue(*property_value));
 
     if (result.HasError()) {
@@ -2167,9 +2263,22 @@ mgp_error mgp_edge_iter_properties(mgp_edge *e, mgp_memory *memory, mgp_properti
 mgp_error mgp_graph_get_vertex_by_id(mgp_graph *graph, mgp_vertex_id id, mgp_memory *memory, mgp_vertex **result) {
   return WrapExceptions(
       [graph, id, memory]() -> mgp_vertex * {
-        auto maybe_vertex = graph->impl->FindVertex(memgraph::storage::Gid::FromInt(id.as_int), graph->view);
+        std::optional<memgraph::query::VertexAccessor> maybe_vertex = std::visit(
+            [graph, id](auto *impl) {
+              return impl->FindVertex(memgraph::storage::Gid::FromInt(id.as_int), graph->view);
+            },
+            graph->impl);
         if (maybe_vertex) {
-          return NewRawMgpObject<mgp_vertex>(memory, *maybe_vertex, graph);
+          return std::visit(memgraph::utils::Overloaded{
+                                [memory, graph, maybe_vertex](memgraph::query::DbAccessor *) {
+                                  return NewRawMgpObject<mgp_vertex>(memory, *maybe_vertex, graph);
+                                },
+                                [memory, graph, maybe_vertex](memgraph::query::SubgraphDbAccessor *impl) {
+                                  return NewRawMgpObject<mgp_vertex>(
+                                      memory, memgraph::query::SubgraphVertexAccessor(*maybe_vertex, impl->getGraph()),
+                                      graph);
+                                }},
+                            graph->impl);
         }
         return nullptr;
       },
@@ -2197,15 +2306,16 @@ mgp_error mgp_graph_create_vertex(struct mgp_graph *graph, mgp_memory *memory, m
         if (!MgpGraphIsMutable(*graph)) {
           throw ImmutableObjectException{"Cannot create a vertex in an immutable graph!"};
         }
-        auto vertex = graph->impl->InsertVertex();
+        auto *vertex = std::visit(
+            [=](auto *impl) { return NewRawMgpObject<mgp_vertex>(memory, impl->InsertVertex(), graph); }, graph->impl);
 
         auto &ctx = graph->ctx;
         ctx->execution_stats[memgraph::query::ExecutionStats::Key::CREATED_NODES] += 1;
 
         if (ctx->trigger_context_collector) {
-          ctx->trigger_context_collector->RegisterCreatedObject(vertex);
+          ctx->trigger_context_collector->RegisterCreatedObject(vertex->getImpl());
         }
-        return NewRawMgpObject<mgp_vertex>(memory, vertex, graph);
+        return vertex;
       },
       result);
 }
@@ -2216,7 +2326,7 @@ mgp_error mgp_graph_delete_vertex(struct mgp_graph *graph, mgp_vertex *vertex) {
 
 #ifdef MG_ENTERPRISE
     if (memgraph::utils::license::global_license_checker.IsValidLicenseFast() && ctx && ctx->auth_checker &&
-        !ctx->auth_checker->Has(vertex->impl, graph->view,
+        !ctx->auth_checker->Has(vertex->getImpl(), graph->view,
                                 memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) {
       throw AuthorizationException{"Insufficient permissions for deleting a vertex!"};
     }
@@ -2225,7 +2335,16 @@ mgp_error mgp_graph_delete_vertex(struct mgp_graph *graph, mgp_vertex *vertex) {
     if (!MgpGraphIsMutable(*graph)) {
       throw ImmutableObjectException{"Cannot remove a vertex from an immutable graph!"};
     }
-    const auto result = graph->impl->RemoveVertex(&vertex->impl);
+
+    const auto result =
+        std::visit(memgraph::utils::Overloaded{
+                       [&](memgraph::query::DbAccessor *impl) {
+                         return impl->RemoveVertex(&std::get<memgraph::query::VertexAccessor>(vertex->impl));
+                       },
+                       [&](memgraph::query::SubgraphDbAccessor *impl) {
+                         return impl->RemoveVertex(&(std::get<memgraph::query::SubgraphVertexAccessor>(vertex->impl)));
+                       }},
+                   graph->impl);
 
     if (result.HasError()) {
       switch (result.GetError()) {
@@ -2258,7 +2377,7 @@ mgp_error mgp_graph_detach_delete_vertex(struct mgp_graph *graph, mgp_vertex *ve
     auto *ctx = graph->ctx;
 #ifdef MG_ENTERPRISE
     if (memgraph::utils::license::global_license_checker.IsValidLicenseFast() && ctx && ctx->auth_checker &&
-        !ctx->auth_checker->Has(vertex->impl, graph->view,
+        !ctx->auth_checker->Has(vertex->getImpl(), graph->view,
                                 memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) {
       throw AuthorizationException{"Insufficient permissions for deleting a vertex!"};
     }
@@ -2267,7 +2386,15 @@ mgp_error mgp_graph_detach_delete_vertex(struct mgp_graph *graph, mgp_vertex *ve
     if (!MgpGraphIsMutable(*graph)) {
       throw ImmutableObjectException{"Cannot remove a vertex from an immutable graph!"};
     }
-    const auto result = graph->impl->DetachRemoveVertex(&vertex->impl);
+    const auto result = std::visit(
+        memgraph::utils::Overloaded{
+            [vertex](memgraph::query::DbAccessor *impl) {
+              return impl->DetachRemoveVertex(&std::get<memgraph::query::VertexAccessor>(vertex->impl));
+            },
+            [vertex](memgraph::query::SubgraphDbAccessor *impl) {
+              return impl->DetachRemoveVertex(&std::get<memgraph::query::SubgraphVertexAccessor>(vertex->impl));
+            }},
+        graph->impl);
 
     if (result.HasError()) {
       switch (result.GetError()) {
@@ -2311,17 +2438,30 @@ mgp_error mgp_graph_create_edge(mgp_graph *graph, mgp_vertex *from, mgp_vertex *
       [=]() -> mgp_edge * {
         auto *ctx = graph->ctx;
 #ifdef MG_ENTERPRISE
+        const auto edge_id =
+            std::visit([type](auto *impl) { return impl->NameToEdgeType(type.name); }, from->graph->impl);
         if (memgraph::utils::license::global_license_checker.IsValidLicenseFast() && ctx && ctx->auth_checker &&
-            !ctx->auth_checker->Has(from->graph->impl->NameToEdgeType(type.name),
-                                    memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) {
+            !ctx->auth_checker->Has(edge_id, memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) {
           throw AuthorizationException{"Insufficient permissions for creating edges!"};
         }
 #endif
         if (!MgpGraphIsMutable(*graph)) {
           throw ImmutableObjectException{"Cannot create an edge in an immutable graph!"};
         }
+        auto edge =
+            std::visit(memgraph::utils::Overloaded{
+                           [from, to, type](memgraph::query::DbAccessor *impl) {
+                             return impl->InsertEdge(&std::get<memgraph::query::VertexAccessor>(from->impl),
+                                                     &std::get<memgraph::query::VertexAccessor>(to->impl),
+                                                     impl->NameToEdgeType(type.name));
+                           },
+                           [from, to, type](memgraph::query::SubgraphDbAccessor *impl) {
+                             return impl->InsertEdge(&std::get<memgraph::query::SubgraphVertexAccessor>(from->impl),
+                                                     &std::get<memgraph::query::SubgraphVertexAccessor>(to->impl),
+                                                     impl->NameToEdgeType(type.name));
+                           }},
+                       graph->impl);
 
-        auto edge = graph->impl->InsertEdge(&from->impl, &to->impl, from->graph->impl->NameToEdgeType(type.name));
         if (edge.HasError()) {
           switch (edge.GetError()) {
             case memgraph::storage::Error::DELETED_OBJECT:
@@ -2341,7 +2481,18 @@ mgp_error mgp_graph_create_edge(mgp_graph *graph, mgp_vertex *from, mgp_vertex *
         if (ctx->trigger_context_collector) {
           ctx->trigger_context_collector->RegisterCreatedObject(*edge);
         }
-        return NewRawMgpObject<mgp_edge>(memory, edge.GetValue(), from->graph);
+        return std::visit(
+            memgraph::utils::Overloaded{
+                [memory, edge, from](memgraph::query::DbAccessor *) {
+                  return NewRawMgpObject<mgp_edge>(memory->impl, edge.GetValue(), from->graph);
+                },
+                [memory, edge, from](memgraph::query::SubgraphDbAccessor *db_impl) {
+                  const auto &v_from =
+                      memgraph::query::SubgraphVertexAccessor(edge.GetValue().From(), db_impl->getGraph());
+                  const auto &v_to = memgraph::query::SubgraphVertexAccessor(edge.GetValue().To(), db_impl->getGraph());
+                  return NewRawMgpObject<mgp_edge>(memory->impl, edge.GetValue(), v_from, v_to, from->graph);
+                }},
+            graph->impl);
       },
       result);
 }
@@ -2358,8 +2509,8 @@ mgp_error mgp_graph_delete_edge(struct mgp_graph *graph, mgp_edge *edge) {
     if (!MgpGraphIsMutable(*graph)) {
       throw ImmutableObjectException{"Cannot remove an edge from an immutable graph!"};
     }
-    const auto result = graph->impl->RemoveEdge(&edge->impl);
 
+    const auto result = std::visit([edge](auto *impl) { return impl->RemoveEdge(&edge->impl); }, graph->impl);
     if (result.HasError()) {
       switch (result.GetError()) {
         case memgraph::storage::Error::NONEXISTENT_OBJECT:
@@ -2407,7 +2558,10 @@ void NextPermitted(mgp_vertices_iterator &it) {
 
 /// @throw anything VerticesIterable may throw
 mgp_vertices_iterator::mgp_vertices_iterator(mgp_graph *graph, memgraph::utils::MemoryResource *memory)
-    : memory(memory), graph(graph), vertices(graph->impl->Vertices(graph->view)), current_it(vertices.begin()) {
+    : memory(memory),
+      graph(graph),
+      vertices(std::visit([graph](auto *impl) { return impl->Vertices(graph->view); }, graph->impl)),
+      current_it(vertices.begin()) {
 #ifdef MG_ENTERPRISE
   if (memgraph::utils::license::global_license_checker.IsValidLicenseFast()) {
     NextPermitted(*this);
@@ -2415,7 +2569,13 @@ mgp_vertices_iterator::mgp_vertices_iterator(mgp_graph *graph, memgraph::utils::
 #endif
 
   if (current_it != vertices.end()) {
-    current_v.emplace(*current_it, graph, memory);
+    std::visit(
+        memgraph::utils::Overloaded{
+            [this, graph, memory](memgraph::query::DbAccessor *) { current_v.emplace(*current_it, graph, memory); },
+            [this, graph, memory](memgraph::query::SubgraphDbAccessor *impl) {
+              current_v.emplace(memgraph::query::SubgraphVertexAccessor(*current_it, impl->getGraph()), graph, memory);
+            }},
+        graph->impl);
   }
 }
 
@@ -2462,7 +2622,17 @@ mgp_error mgp_vertices_iterator_next(mgp_vertices_iterator *it, mgp_vertex **res
         }
 
         memgraph::utils::OnScopeExit clean_up([it] { it->current_v = std::nullopt; });
-        it->current_v.emplace(*it->current_it, it->graph, it->GetMemoryResource());
+        std::visit(memgraph::utils::Overloaded{[it](memgraph::query::DbAccessor *) {
+                                                 it->current_v.emplace(*it->current_it, it->graph,
+                                                                       it->GetMemoryResource());
+                                               },
+                                               [it](memgraph::query::SubgraphDbAccessor *impl) {
+                                                 it->current_v.emplace(memgraph::query::SubgraphVertexAccessor(
+                                                                           *it->current_it, impl->getGraph()),
+                                                                       it->graph, it->GetMemoryResource());
+                                               }},
+                   it->graph->impl);
+
         clean_up.Disable();
         return &*it->current_v;
       },
@@ -2745,6 +2915,7 @@ std::ostream &PrintValue(const TypedValue &value, std::ostream *stream) {
     case TypedValue::Type::Vertex:
     case TypedValue::Type::Edge:
     case TypedValue::Type::Path:
+    case TypedValue::Type::Graph:
       LOG_FATAL("value must not be a graph element");
   }
 }
diff --git a/src/query/procedure/mg_procedure_impl.hpp b/src/query/procedure/mg_procedure_impl.hpp
index 2b2f16b5d..4d2acb77d 100644
--- a/src/query/procedure/mg_procedure_impl.hpp
+++ b/src/query/procedure/mg_procedure_impl.hpp
@@ -32,6 +32,7 @@
 #include "utils/pmr/string.hpp"
 #include "utils/pmr/vector.hpp"
 #include "utils/temporal.hpp"
+#include "utils/variant_helpers.hpp"
 /// Wraps memory resource used in custom procedures.
 ///
 /// This should have been `using mgp_memory = memgraph::utils::MemoryResource`, but that's
@@ -442,6 +443,10 @@ struct mgp_vertex {
   mgp_vertex(memgraph::query::VertexAccessor v, mgp_graph *graph, memgraph::utils::MemoryResource *memory) noexcept
       : memory(memory), impl(v), graph(graph) {}
 
+  mgp_vertex(memgraph::query::SubgraphVertexAccessor v, mgp_graph *graph,
+             memgraph::utils::MemoryResource *memory) noexcept
+      : memory(memory), impl(v), graph(graph) {}
+
   mgp_vertex(const mgp_vertex &other, memgraph::utils::MemoryResource *memory) noexcept
       : memory(memory), impl(other.impl), graph(other.graph) {}
 
@@ -450,13 +455,21 @@ struct mgp_vertex {
 
   mgp_vertex(mgp_vertex &&other) noexcept : memory(other.memory), impl(other.impl), graph(other.graph) {}
 
+  memgraph::query::VertexAccessor getImpl() const {
+    return std::visit(
+        memgraph::utils::Overloaded{[](memgraph::query::VertexAccessor impl) { return impl; },
+                                    [](memgraph::query::SubgraphVertexAccessor impl) { return impl.impl_; }},
+        this->impl);
+  }
+
   /// Copy construction without memgraph::utils::MemoryResource is not allowed.
   mgp_vertex(const mgp_vertex &) = delete;
 
   mgp_vertex &operator=(const mgp_vertex &) = delete;
   mgp_vertex &operator=(mgp_vertex &&) = delete;
 
-  bool operator==(const mgp_vertex &other) const noexcept { return this->impl == other.impl; }
+  bool operator==(const mgp_vertex &other) const noexcept { return other.getImpl() == this->getImpl(); }
+
   bool operator!=(const mgp_vertex &other) const noexcept { return !(*this == other); };
 
   ~mgp_vertex() = default;
@@ -464,7 +477,7 @@ struct mgp_vertex {
   memgraph::utils::MemoryResource *GetMemoryResource() const noexcept { return memory; }
 
   memgraph::utils::MemoryResource *memory;
-  memgraph::query::VertexAccessor impl;
+  std::variant<memgraph::query::VertexAccessor, memgraph::query::SubgraphVertexAccessor> impl;
   mgp_graph *graph;
 };
 
@@ -484,6 +497,16 @@ struct mgp_edge {
            memgraph::utils::MemoryResource *memory) noexcept
       : memory(memory), impl(impl), from(impl.From(), graph, memory), to(impl.To(), graph, memory) {}
 
+  mgp_edge(const memgraph::query::EdgeAccessor &impl, const memgraph::query::VertexAccessor &from_v,
+           const memgraph::query::VertexAccessor &to_v, mgp_graph *graph,
+           memgraph::utils::MemoryResource *memory) noexcept
+      : memory(memory), impl(impl), from(from_v, graph, memory), to(to_v, graph, memory) {}
+
+  mgp_edge(const memgraph::query::EdgeAccessor &impl, const memgraph::query::SubgraphVertexAccessor &from_v,
+           const memgraph::query::SubgraphVertexAccessor &to_v, mgp_graph *graph,
+           memgraph::utils::MemoryResource *memory) noexcept
+      : memory(memory), impl(impl), from(from_v, graph, memory), to(to_v, graph, memory) {}
+
   mgp_edge(const mgp_edge &other, memgraph::utils::MemoryResource *memory) noexcept
       : memory(memory), impl(other.impl), from(other.from, memory), to(other.to, memory) {}
 
@@ -541,6 +564,32 @@ struct mgp_path {
   memgraph::utils::pmr::vector<mgp_edge> edges;
 };
 
+struct mgp_graph {
+  std::variant<memgraph::query::DbAccessor *, memgraph::query::SubgraphDbAccessor *> impl;
+  memgraph::storage::View view;
+  // TODO: Merge `mgp_graph` and `mgp_memory` into a single `mgp_context`. The
+  // `ctx` field is out of place here.
+  memgraph::query::ExecutionContext *ctx;
+
+  static mgp_graph WritableGraph(memgraph::query::DbAccessor &acc, memgraph::storage::View view,
+                                 memgraph::query::ExecutionContext &ctx) {
+    return mgp_graph{&acc, view, &ctx};
+  }
+
+  static mgp_graph NonWritableGraph(memgraph::query::DbAccessor &acc, memgraph::storage::View view) {
+    return mgp_graph{&acc, view, nullptr};
+  }
+
+  static mgp_graph WritableGraph(memgraph::query::SubgraphDbAccessor &acc, memgraph::storage::View view,
+                                 memgraph::query::ExecutionContext &ctx) {
+    return mgp_graph{&acc, view, &ctx};
+  }
+
+  static mgp_graph NonWritableGraph(memgraph::query::SubgraphDbAccessor &acc, memgraph::storage::View view) {
+    return mgp_graph{&acc, view, nullptr};
+  }
+};
+
 struct mgp_result_record {
   /// Result record signature as defined for mgp_proc.
   const memgraph::utils::pmr::map<memgraph::utils::pmr::string,
@@ -570,23 +619,6 @@ struct mgp_func_result {
   std::optional<memgraph::utils::pmr::string> error_msg;
 };
 
-struct mgp_graph {
-  memgraph::query::DbAccessor *impl;
-  memgraph::storage::View view;
-  // TODO: Merge `mgp_graph` and `mgp_memory` into a single `mgp_context`. The
-  // `ctx` field is out of place here.
-  memgraph::query::ExecutionContext *ctx;
-
-  static mgp_graph WritableGraph(memgraph::query::DbAccessor &acc, memgraph::storage::View view,
-                                 memgraph::query::ExecutionContext &ctx) {
-    return mgp_graph{&acc, view, &ctx};
-  }
-
-  static mgp_graph NonWritableGraph(memgraph::query::DbAccessor &acc, memgraph::storage::View view) {
-    return mgp_graph{&acc, view, nullptr};
-  }
-};
-
 // Prevents user to use ExecutionContext in writable callables
 struct mgp_func_context {
   memgraph::query::DbAccessor *impl;
@@ -615,8 +647,13 @@ struct mgp_properties_iterator {
   mgp_properties_iterator(mgp_graph *graph, decltype(pvs) pvs, memgraph::utils::MemoryResource *memory)
       : memory(memory), graph(graph), pvs(std::move(pvs)), current_it(this->pvs.begin()) {
     if (current_it != this->pvs.end()) {
-      current.emplace(memgraph::utils::pmr::string(graph->impl->PropertyToName(current_it->first), memory),
-                      mgp_value(current_it->second, memory));
+      auto value = std::visit(
+          [this, memory](const auto *impl) {
+            return memgraph::utils::pmr::string(impl->PropertyToName(current_it->first), memory);
+          },
+          graph->impl);
+
+      current.emplace(value, mgp_value(current_it->second, memory));
       property.name = current->first.c_str();
       property.value = &current->second;
     }
@@ -635,7 +672,6 @@ struct mgp_properties_iterator {
 
 struct mgp_edges_iterator {
   using allocator_type = memgraph::utils::Allocator<mgp_edges_iterator>;
-
   // Hopefully mgp_vertex copy constructor remains noexcept, so that we can
   // have everything noexcept here.
   static_assert(std::is_nothrow_constructible_v<mgp_vertex, const mgp_vertex &, memgraph::utils::MemoryResource *>);
@@ -662,9 +698,14 @@ struct mgp_edges_iterator {
 
   memgraph::utils::MemoryResource *memory;
   mgp_vertex source_vertex;
-  std::optional<std::remove_reference_t<decltype(*source_vertex.impl.InEdges(source_vertex.graph->view))>> in;
+
+  std::optional<std::remove_reference_t<
+      decltype(*std::get<memgraph::query::VertexAccessor>(source_vertex.impl).InEdges(source_vertex.graph->view))>>
+      in;
   std::optional<decltype(in->begin())> in_it;
-  std::optional<std::remove_reference_t<decltype(*source_vertex.impl.OutEdges(source_vertex.graph->view))>> out;
+  std::optional<std::remove_reference_t<
+      decltype(*std::get<memgraph::query::VertexAccessor>(source_vertex.impl).OutEdges(source_vertex.graph->view))>>
+      out;
   std::optional<decltype(out->begin())> out_it;
   std::optional<mgp_edge> current_e;
 };
@@ -679,7 +720,7 @@ struct mgp_vertices_iterator {
 
   memgraph::utils::MemoryResource *memory;
   mgp_graph *graph;
-  decltype(graph->impl->Vertices(graph->view)) vertices;
+  memgraph::query::VerticesIterable vertices;
   decltype(vertices.begin()) current_it;
   std::optional<mgp_vertex> current_v;
 };
diff --git a/src/query/typed_value.cpp b/src/query/typed_value.cpp
index 1b917d297..6719f52f6 100644
--- a/src/query/typed_value.cpp
+++ b/src/query/typed_value.cpp
@@ -214,6 +214,9 @@ TypedValue::TypedValue(const TypedValue &other, utils::MemoryResource *memory) :
     case Type::Duration:
       new (&duration_v) utils::Duration(other.duration_v);
       return;
+    case Type::Graph:
+      new (&graph_v) Graph(other.graph_v, memory_);
+      return;
   }
   LOG_FATAL("Unsupported TypedValue::Type");
 }
@@ -263,6 +266,8 @@ TypedValue::TypedValue(TypedValue &&other, utils::MemoryResource *memory) : memo
     case Type::Duration:
       new (&duration_v) utils::Duration(other.duration_v);
       break;
+    case Type::Graph:
+      new (&graph_v) Graph(std::move(other.graph_v), memory_);
   }
   other.DestroyValue();
 }
@@ -331,6 +336,7 @@ DEFINE_VALUE_AND_TYPE_GETTERS(utils::Date, Date, date_v)
 DEFINE_VALUE_AND_TYPE_GETTERS(utils::LocalTime, LocalTime, local_time_v)
 DEFINE_VALUE_AND_TYPE_GETTERS(utils::LocalDateTime, LocalDateTime, local_date_time_v)
 DEFINE_VALUE_AND_TYPE_GETTERS(utils::Duration, Duration, duration_v)
+DEFINE_VALUE_AND_TYPE_GETTERS(Graph, Graph, graph_v)
 
 #undef DEFINE_VALUE_AND_TYPE_GETTERS
 
@@ -387,6 +393,8 @@ std::ostream &operator<<(std::ostream &os, const TypedValue::Type &type) {
       return os << "local_date_time";
     case TypedValue::Type::Duration:
       return os << "duration";
+    case TypedValue::Type::Graph:
+      return os << "graph";
   }
   LOG_FATAL("Unsupported TypedValue::Type");
 }
@@ -522,6 +530,9 @@ TypedValue &TypedValue::operator=(const TypedValue &other) {
       case TypedValue::Type::Path:
         new (&path_v) Path(other.path_v, memory_);
         return *this;
+      case TypedValue::Type::Graph:
+        new (&graph_v) Graph(other.graph_v, memory_);
+        return *this;
       case Type::Date:
         new (&date_v) utils::Date(other.date_v);
         return *this;
@@ -593,6 +604,9 @@ TypedValue &TypedValue::operator=(TypedValue &&other) noexcept(false) {
       case Type::Duration:
         new (&duration_v) utils::Duration(other.duration_v);
         break;
+      case Type::Graph:
+        new (&graph_v) Graph(std::move(other.graph_v), memory_);
+        break;
     }
     other.DestroyValue();
   }
@@ -633,6 +647,9 @@ void TypedValue::DestroyValue() {
     case Type::LocalDateTime:
     case Type::Duration:
       break;
+    case Type::Graph:
+      graph_v.~Graph();
+      break;
   }
 
   type_ = TypedValue::Type::Null;
@@ -792,6 +809,8 @@ TypedValue operator==(const TypedValue &a, const TypedValue &b) {
       return TypedValue(a.ValueLocalDateTime() == b.ValueLocalDateTime(), a.GetMemoryResource());
     case TypedValue::Type::Duration:
       return TypedValue(a.ValueDuration() == b.ValueDuration(), a.GetMemoryResource());
+    case TypedValue::Type::Graph:
+      throw TypedValueException("Unsupported comparison operator");
     default:
       LOG_FATAL("Unhandled comparison for types");
   }
@@ -1100,6 +1119,8 @@ size_t TypedValue::Hash::operator()(const TypedValue &value) const {
     case TypedValue::Type::Duration:
       return utils::DurationHash{}(value.ValueDuration());
       break;
+    case TypedValue::Type::Graph:
+      throw TypedValueException("Unsupported hash function for Graph");
   }
   LOG_FATAL("Unhandled TypedValue.type() in hash function");
 }
diff --git a/src/query/typed_value.hpp b/src/query/typed_value.hpp
index e9d8a049e..b0eadf70e 100644
--- a/src/query/typed_value.hpp
+++ b/src/query/typed_value.hpp
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include "query/db_accessor.hpp"
+#include "query/graph.hpp"
 #include "query/path.hpp"
 #include "utils/exceptions.hpp"
 #include "utils/memory.hpp"
@@ -82,7 +83,8 @@ class TypedValue {
     Date,
     LocalTime,
     LocalDateTime,
-    Duration
+    Duration,
+    Graph
   };
 
   // TypedValue at this exact moment of compilation is an incomplete type, and
@@ -401,6 +403,22 @@ class TypedValue {
     new (&path_v) Path(std::move(path), memory_);
   }
 
+  /**
+   * Construct with the value of graph.
+   * utils::MemoryResource is obtained from graph. After the move, graph will be
+   * left empty.
+   */
+  explicit TypedValue(Graph &&graph) noexcept : TypedValue(std::move(graph), graph.GetMemoryResource()) {}
+
+  /**
+   * Construct with the value of graph and use the given MemoryResource.
+   * If `*graph.GetMemoryResource() != *memory`, this call will perform an
+   * element-wise move and graph is not guaranteed to be empty.
+   */
+  TypedValue(Graph &&graph, utils::MemoryResource *memory) : memory_(memory), type_(Type::Graph) {
+    new (&graph_v) Graph(std::move(graph), memory_);
+  }
+
   /**
    * Construct with the value of other.
    * Default utils::NewDeleteResource() is used for allocations. After the move,
@@ -486,6 +504,7 @@ class TypedValue {
   DECLARE_VALUE_AND_TYPE_GETTERS(utils::LocalTime, LocalTime)
   DECLARE_VALUE_AND_TYPE_GETTERS(utils::LocalDateTime, LocalDateTime)
   DECLARE_VALUE_AND_TYPE_GETTERS(utils::Duration, Duration)
+  DECLARE_VALUE_AND_TYPE_GETTERS(Graph, Graph)
 
 #undef DECLARE_VALUE_AND_TYPE_GETTERS
 
@@ -528,6 +547,7 @@ class TypedValue {
     utils::LocalTime local_time_v;
     utils::LocalDateTime local_date_time_v;
     utils::Duration duration_v;
+    Graph graph_v;
   };
 
   /**
diff --git a/tests/e2e/write_procedures/CMakeLists.txt b/tests/e2e/write_procedures/CMakeLists.txt
index 72b640271..27a9a73e2 100644
--- a/tests/e2e/write_procedures/CMakeLists.txt
+++ b/tests/e2e/write_procedures/CMakeLists.txt
@@ -5,5 +5,6 @@ endfunction()
 copy_write_procedures_e2e_python_files(common.py)
 copy_write_procedures_e2e_python_files(conftest.py)
 copy_write_procedures_e2e_python_files(simple_write.py)
+copy_write_procedures_e2e_python_files(read_subgraph.py)
 
 add_subdirectory(procedures)
diff --git a/tests/e2e/write_procedures/procedures/read.py b/tests/e2e/write_procedures/procedures/read.py
index b24138c38..50c89e325 100644
--- a/tests/e2e/write_procedures/procedures/read.py
+++ b/tests/e2e/write_procedures/procedures/read.py
@@ -22,6 +22,65 @@ def graph_is_mutable(ctx: mgp.ProcCtx) -> mgp.Record(mutable=bool):
     return mgp.Record(mutable=ctx.graph.is_mutable())
 
 
+@mgp.read_proc
+def subgraph_empty(ctx: mgp.ProcCtx, arg1: mgp.Any, arg2: mgp.Any, arg3: mgp.Any = 2) -> mgp.Record(result=int):
+    return mgp.Record(result=1)
+
+
+@mgp.read_proc
+def subgraph_get_vertices(ctx: mgp.ProcCtx) -> mgp.Record(node=mgp.Vertex):
+    return [mgp.Record(node=vertex) for vertex in ctx.graph.vertices]
+
+
+@mgp.read_proc
+def subgraph_get_out_edges(ctx: mgp.ProcCtx, vertex: mgp.Vertex) -> mgp.Record(edge=mgp.Edge):
+    return [mgp.Record(edge=edge) for edge in vertex.out_edges]
+
+
+@mgp.read_proc
+def subgraph_get_in_edges(ctx: mgp.ProcCtx, vertex: mgp.Vertex) -> mgp.Record(edge=mgp.Edge):
+    return [mgp.Record(edge=edge) for edge in vertex.in_edges]
+
+
+@mgp.read_proc
+def subgraph_get_2_hop_edges(ctx: mgp.ProcCtx, vertex: mgp.Vertex) -> mgp.Record(edge=mgp.Edge):
+    out_edges = vertex.out_edges
+    records = []
+    for edge in out_edges:
+        vertex = edge.to_vertex
+        properties = vertex.properties
+        print(properties)
+        records.extend([mgp.Record(edge=edge) for edge in edge.to_vertex.out_edges])
+    return records
+
+
+@mgp.read_proc
+def subgraph_get_out_edges_vertex_id(ctx: mgp.ProcCtx, vertex: mgp.Vertex) -> mgp.Record(edge=mgp.Edge):
+    vertex = ctx.graph.get_vertex_by_id(vertex.id)
+    return [mgp.Record(edge=edge) for edge in vertex.out_edges]
+
+
+@mgp.read_proc
+def subgraph_get_path_vertices(ctx: mgp.ProcCtx, path: mgp.Path) -> mgp.Record(node=mgp.Vertex):
+    return [mgp.Record(node=node) for node in path.vertices]
+
+
+@mgp.read_proc
+def subgraph_get_path_edges(ctx: mgp.ProcCtx, path: mgp.Path) -> mgp.Record(edge=mgp.Edge):
+    return [mgp.Record(edge=edge) for edge in path.edges]
+
+
+@mgp.read_proc
+def subgraph_get_path_vertices_in_subgraph(ctx: mgp.ProcCtx, path: mgp.Path) -> mgp.Record(node=mgp.Vertex):
+    path_vertices = path.vertices
+    graph_vertices = ctx.graph.vertices
+    records = []
+    for path_vertex in path_vertices:
+        if path_vertex in graph_vertices:
+            records.append(mgp.Record(node=path_vertex))
+    return records
+
+
 @mgp.read_proc
 def log_message(ctx: mgp.ProcCtx, message: str) -> mgp.Record(success=bool):
     logger = mgp.Logger()
diff --git a/tests/e2e/write_procedures/procedures/write.py b/tests/e2e/write_procedures/procedures/write.py
index e180be40e..90308a615 100644
--- a/tests/e2e/write_procedures/procedures/write.py
+++ b/tests/e2e/write_procedures/procedures/write.py
@@ -35,13 +35,12 @@ def detach_delete_vertex(ctx: mgp.ProcCtx, v: mgp.Any) -> mgp.Record():
 
 
 @mgp.write_proc
-def create_edge(ctx: mgp.ProcCtx, from_vertex: mgp.Vertex,
-                to_vertex: mgp.Vertex,
-                edge_type: str) -> mgp.Record(e=mgp.Any):
+def create_edge(
+    ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, to_vertex: mgp.Vertex, edge_type: str
+) -> mgp.Record(e=mgp.Any):
     e = None
     try:
-        e = ctx.graph.create_edge(
-            from_vertex, to_vertex, mgp.EdgeType(edge_type))
+        e = ctx.graph.create_edge(from_vertex, to_vertex, mgp.EdgeType(edge_type))
     except RuntimeError as ex:
         return mgp.Record(e=str(ex))
     return mgp.Record(e=e)
@@ -54,32 +53,60 @@ def delete_edge(ctx: mgp.ProcCtx, edge: mgp.Edge) -> mgp.Record():
 
 
 @mgp.write_proc
-def set_property(ctx: mgp.ProcCtx, object: mgp.Any,
-                 name: str, value: mgp.Nullable[mgp.Any]) -> mgp.Record():
+def set_property(ctx: mgp.ProcCtx, object: mgp.Any, name: str, value: mgp.Nullable[mgp.Any]) -> mgp.Record():
     object.properties.set(name, value)
     return mgp.Record()
 
 
 @mgp.write_proc
-def add_label(ctx: mgp.ProcCtx, object: mgp.Any,
-              name: str) -> mgp.Record(o=mgp.Any):
+def add_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
     object.add_label(name)
     return mgp.Record(o=object)
 
 
 @mgp.write_proc
-def remove_label(ctx: mgp.ProcCtx, object: mgp.Any,
-                 name: str) -> mgp.Record(o=mgp.Any):
+def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
     object.remove_label(name)
     return mgp.Record(o=object)
 
 
 @mgp.write_proc
-def underlying_graph_is_mutable(ctx: mgp.ProcCtx,
-                                object: mgp.Any) -> mgp.Record(mutable=bool):
+def underlying_graph_is_mutable(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(mutable=bool):
     return mgp.Record(mutable=object.underlying_graph_is_mutable())
 
 
 @mgp.write_proc
 def graph_is_mutable(ctx: mgp.ProcCtx) -> mgp.Record(mutable=bool):
     return mgp.Record(mutable=ctx.graph.is_mutable())
+
+
+@mgp.write_proc
+def subgraph_insert_vertex_get_vertices(ctx: mgp.ProcCtx) -> mgp.Record(node=mgp.Vertex):
+    ctx.graph.create_vertex()
+    return [mgp.Record(node=node) for node in ctx.graph.vertices]
+
+
+@mgp.write_proc
+def subgraph_insert_edge_get_vertex_out_edges(
+    ctx: mgp.ProcCtx, vertex1: mgp.Vertex, vertex2: mgp.Vertex
+) -> mgp.Record(edge=mgp.Edge):
+    ctx.graph.create_edge(vertex1, vertex2, edge_type=mgp.EdgeType("EDGE_TYPE"))
+    return [mgp.Record(edge=edge) for edge in vertex1.out_edges]
+
+
+@mgp.write_proc
+def subgraph_remove_edge_get_vertex_out_edges(ctx: mgp.ProcCtx, edge: mgp.Edge) -> mgp.Record(edge=mgp.Edge):
+    from_vertex = edge.from_vertex
+    ctx.graph.delete_edge(edge)
+    return [mgp.Record(edge=edge) for edge in from_vertex.out_edges]
+
+
+@mgp.write_proc
+def subgraph_remove_vertex_and_out_edges_get_vertices(
+    ctx: mgp.ProcCtx, vertex: mgp.Vertex
+) -> mgp.Record(node=mgp.Vertex):
+    out_edges = vertex.out_edges
+    for edge in out_edges:
+        ctx.graph.delete_edge(edge)
+    ctx.graph.delete_vertex(vertex)
+    return [mgp.Record(node=vertex) for vertex in ctx.graph.vertices]
diff --git a/tests/e2e/write_procedures/read_subgraph.py b/tests/e2e/write_procedures/read_subgraph.py
new file mode 100644
index 000000000..40f315050
--- /dev/null
+++ b/tests/e2e/write_procedures/read_subgraph.py
@@ -0,0 +1,319 @@
+# Copyright 2022 Memgraph Ltd.
+#
+# Use of this software is governed by the Business Source License
+# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+# License, and you may not use this file except in compliance with the Business Source License.
+#
+# As of the Change Date specified in that file, in accordance with
+# the Business Source License, use of this software will be governed
+# by the Apache License, Version 2.0, included in the file
+# licenses/APL.txt.
+
+import typing
+import mgclient
+import sys
+import pytest
+from common import execute_and_fetch_all, has_n_result_row
+
+
+def create_subgraph(cursor):
+    execute_and_fetch_all(cursor, "CREATE (n:Person {id: 1});")
+    execute_and_fetch_all(cursor, "CREATE (n:Person {id: 2});")
+    execute_and_fetch_all(cursor, "CREATE (n:Person {id: 3});")
+    execute_and_fetch_all(cursor, "CREATE (n:Person {id: 4});")
+    execute_and_fetch_all(cursor, "CREATE (n:Team {id: 5});")
+    execute_and_fetch_all(cursor, "CREATE (n:Team {id: 6});")
+    execute_and_fetch_all(cursor, "MATCH (p:Person {id: 1}) MATCH (t:Team {id:5}) CREATE (p)-[:SUPPORTS]->(t);")
+    execute_and_fetch_all(cursor, "MATCH (p:Person {id: 1}) MATCH (t:Team {id:6}) CREATE (p)-[:SUPPORTS]->(t);")
+    execute_and_fetch_all(cursor, "MATCH (p:Person {id: 2}) MATCH (t:Team {id:6}) CREATE (p)-[:SUPPORTS]->(t);")
+    execute_and_fetch_all(cursor, "MATCH (p1:Person {id: 1}) MATCH (p2:Person {id:2}) CREATE (p1)-[:KNOWS]->(p2);")
+    execute_and_fetch_all(cursor, "MATCH (t1:Team {id: 5}) MATCH (t2:Team {id:6}) CREATE (t1)-[:IS_RIVAL_TO]->(t2);")
+    execute_and_fetch_all(cursor, "MATCH (p1:Person {id: 3}) MATCH (p2:Person {id:4}) CREATE (p1)-[:KNOWS]->(p2);")
+
+
+def create_smaller_subgraph(cursor):
+    execute_and_fetch_all(cursor, "CREATE (n:Person {id: 1});")
+    execute_and_fetch_all(cursor, "CREATE (n:Person {id: 2});")
+    execute_and_fetch_all(cursor, "CREATE (n:Team {id: 5});")
+    execute_and_fetch_all(cursor, "CREATE (n:Team {id: 6});")
+    execute_and_fetch_all(cursor, "MATCH (p:Person {id: 1}) MATCH (t:Team {id:5}) CREATE (p)-[:SUPPORTS]->(t);")
+    execute_and_fetch_all(cursor, "MATCH (p:Person {id: 1}) MATCH (t:Team {id:6}) CREATE (p)-[:SUPPORTS]->(t);")
+    execute_and_fetch_all(cursor, "MATCH (p:Person {id: 2}) MATCH (t:Team {id:6}) CREATE (p)-[:SUPPORTS]->(t);")
+
+
+def test_is_callable(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) AS graph CALL read.subgraph_empty(graph, 2, 3) YIELD result RETURN result;",
+        1,
+    )
+
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_incorrect_graph_argument_placement(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    queries = [
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) AS graph CALL read.subgraph_empty(2, graph, 3) YIELD result RETURN result;",
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) AS graph CALL read.subgraph_empty(2, 3, graph) YIELD result RETURN result;",
+    ]
+    for query in queries:
+        with pytest.raises(mgclient.DatabaseError):
+            execute_and_fetch_all(cursor, query)
+
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_get_vertices(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) AS graph CALL read.subgraph_get_vertices(graph) YIELD node RETURN node;",
+        4,
+    )
+
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_get_out_edges(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) AS graph MATCH (n1:Person {{id:1}}) CALL read.subgraph_get_out_edges(graph, n1) YIELD edge RETURN edge;",
+        2,
+    )
+
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_get_in_edges(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) AS graph MATCH (t1:Team {{id:6}}) CALL read.subgraph_get_in_edges(graph, t1) YIELD edge RETURN edge;",
+        2,
+    )
+
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_get_2_hop_edges(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) AS graph MATCH (n1:Person {{id:1}}) CALL read.subgraph_get_2_hop_edges(graph, n1) YIELD edge RETURN edge;",
+        0,
+    )
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_get_out_edges_vertex_id(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor=cursor)
+
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) AS graph MATCH (n1:Person {{id:1}}) CALL read.subgraph_get_out_edges_vertex_id(graph, n1) YIELD edge RETURN edge;",
+        2,
+    )
+
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_subgraph_get_path_vertices(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) as graph MATCH path=(a:Person {{id: 1}})-[:SUPPORTS]->(b:Team {{id:5}}) CALL read.subgraph_get_path_vertices(graph, path) YIELD node RETURN node;",
+        2,
+    )
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_subgraph_get_path_edges(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) as graph MATCH path=(:Person {{id: 1}})-[:SUPPORTS]->(:Team {{id:5}}) CALL read.subgraph_get_path_edges(graph, path) YIELD edge RETURN edge;",
+        1,
+    )
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_subgraph_get_path_vertices_in_subgraph(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) as graph MATCH path=(:Person {{id: 1}})-[:SUPPORTS]->(:Team {{id:5}}) CALL read.subgraph_get_path_vertices_in_subgraph(graph, path) YIELD node RETURN node;",
+        2,
+    )
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_subgraph_insert_vertex_get_vertices(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) as graph CALL write.subgraph_insert_vertex_get_vertices(graph) YIELD node RETURN node;",
+        5,
+    )
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_subgraph_insert_edge_get_vertex_out_edges(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) as graph MATCH (p1:Person {{id:2}}) MATCH (t1:Team {{id:6}}) CALL write.subgraph_insert_edge_get_vertex_out_edges(graph, p1, t1) YIELD edge RETURN edge;",
+        2,
+    )
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_subgraph_create_edge_both_vertices_not_in_projected_graph_error(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    with pytest.raises(mgclient.DatabaseError):
+        execute_and_fetch_all(
+            cursor,
+            f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) as graph MATCH (p1:Person {{id:2}}) MATCH (p2:Person {{id:4}}) CALL write.subgraph_insert_edge_get_vertex_out_edges(graph, p1, p2) YIELD edge RETURN edge;",
+        )
+
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_subgraph_remove_edge_get_vertex_out_edges(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) as graph MATCH (p1:Person {{id:1}})-[e:SUPPORTS]->(t1:Team {{id:5}}) CALL write.subgraph_remove_edge_get_vertex_out_edges(graph, e) YIELD edge RETURN edge;",
+        1,
+    )
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_subgraph_remove_edge_not_in_subgraph_error(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_subgraph(cursor)
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 6)
+    with pytest.raises(mgclient.DatabaseError):
+        execute_and_fetch_all(
+            cursor,
+            f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) as graph MATCH (p1:Person {{id:1}})-[e:KNOWS]->(p2:Person {{id:2}}) CALL write.subgraph_remove_edge_get_vertex_out_edges(graph, e) YIELD edge RETURN edge;",
+        )
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+def test_subgraph_remove_vertex_and_out_edges_get_vertices(connection):
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;")
+    create_smaller_subgraph(cursor)
+    assert has_n_result_row(cursor, "MATCH (n) RETURN n;", 4)
+    assert has_n_result_row(
+        cursor,
+        f"MATCH p=(n:Person)-[:SUPPORTS]->(m:Team) WITH project(p) as graph MATCH (p1:Person {{id:1}}) CALL write.subgraph_remove_vertex_and_out_edges_get_vertices(graph, p1) YIELD node RETURN node;",
+        3,
+    )
+    execute_and_fetch_all(
+        cursor,
+        f"MATCH (n) DETACH DELETE n;",
+    )
+
+
+if __name__ == "__main__":
+    sys.exit(pytest.main([__file__, "-rA"]))
diff --git a/tests/e2e/write_procedures/workloads.yaml b/tests/e2e/write_procedures/workloads.yaml
index a6a57231f..667947920 100644
--- a/tests/e2e/write_procedures/workloads.yaml
+++ b/tests/e2e/write_procedures/workloads.yaml
@@ -12,3 +12,8 @@ workloads:
     proc: "tests/e2e/write_procedures/procedures/"
     args: ["write_procedures/simple_write.py"]
     <<: *template_cluster
+  - name: "Graph projection procedures"
+    binary: "tests/e2e/pytest_runner.sh"
+    proc: "tests/e2e/write_procedures/procedures/"
+    args: ["write_procedures/read_subgraph.py"]
+    <<: *template_cluster
diff --git a/tests/gql_behave/tests/memgraph_V1/features/aggregations.feature b/tests/gql_behave/tests/memgraph_V1/features/aggregations.feature
index 796f598eb..8fe6a47ad 100644
--- a/tests/gql_behave/tests/memgraph_V1/features/aggregations.feature
+++ b/tests/gql_behave/tests/memgraph_V1/features/aggregations.feature
@@ -344,3 +344,61 @@ Feature: Aggregations
             | count(n) < n.property | count(n.property)     | count(n)              | avg(n.property)       | min(n.property)       | max(n.property)       | sum(n.property)       |
             | false                 | 1                     | 1                     | 1.0                   | 1                     | 1                     | 1                     |
             | null                  | 0                     | 1                     | null                  | null                  | null                  | 0                     |
+
+    Scenario: Graph projection test 01:
+        Given an empty graph
+        And having executed
+            """
+            CREATE (a{x: 1}), (b{x: 2}), (c{x: 3}), (d{x: 4}), (a)-[:X]->(b), (b)-[:X]->(c), (c)-[:X]->(a), (a)-[:B]->(d)
+            """
+        When executing query:
+            """
+            MATCH p=()-[:X]->() WITH project(p) as graph WITH graph.nodes as nodes UNWIND nodes as n RETURN n.x as x ORDER BY x DESC
+            """
+        Then the result should be:
+            | x |
+            | 3 |
+            | 2 |
+            | 1 |
+
+    Scenario: Graph projection test 02:
+        Given an empty graph
+        And having executed
+            """
+            CREATE (a{x: 1}), (b{x: 2}), (c{x: 3}), (d{x: 4}), (a)-[:X]->(b), (b)-[:X]->(c), (c)-[:X]->(a), (a)-[:B]->(d)
+            """
+        When executing query:
+            """
+            MATCH p=()-[:Z]->() WITH project(p) as graph WITH graph.nodes as nodes UNWIND nodes as n RETURN n.x as x ORDER BY x DESC
+            """
+        Then the result should be:
+            | x |
+
+    Scenario: Graph projection test 03:
+        Given an empty graph
+        And having executed
+            """
+            CREATE (a{x: 1}), (b{x: 2}), (c{x: 3}), (d{x: 4}), (a)-[:X {prop:1}]->(b), (b)-[:X {prop:2}]->(c), (c)-[:X {prop:3}]->(a), (a)-[:B {prop:4}]->(d)
+            """
+        When executing query:
+            """
+            MATCH p=()-[:X]->() WITH project(p) as graph WITH graph.edges as edges UNWIND edges as e RETURN e.prop as y ORDER BY y DESC
+            """
+        Then the result should be:
+            | y |
+            | 3 |
+            | 2 |
+            | 1 |
+
+    Scenario: Graph projection test 04:
+        Given an empty graph
+        And having executed
+            """
+            CREATE (a{x: 1}), (b{x: 2}), (c{x: 3}), (d{x: 4}), (a)-[:X {prop:1}]->(b), (b)-[:X {prop:2}]->(c), (c)-[:X {prop:3}]->(a), (a)-[:B {prop:4}]->(d)
+            """
+        When executing query:
+            """
+            MATCH p=()-[:Z]->() WITH project(p) as graph WITH graph.edges as edges UNWIND edges as e RETURN e.prop as y ORDER BY y DESC
+            """
+        Then the result should be:
+            | y |
diff --git a/tests/unit/formatters.hpp b/tests/unit/formatters.hpp
index a2c9df807..0b4d25908 100644
--- a/tests/unit/formatters.hpp
+++ b/tests/unit/formatters.hpp
@@ -136,6 +136,8 @@ inline std::string ToString(const memgraph::query::TypedValue &value, const TAcc
     case memgraph::query::TypedValue::Type::Duration:
       os << ToString(value.ValueDuration());
       break;
+    case memgraph::query::TypedValue::Type::Graph:
+      throw std::logic_error{"Not implemented"};
   }
   return os.str();
 }
diff --git a/tests/unit/query_procedures_mgp_graph.cpp b/tests/unit/query_procedures_mgp_graph.cpp
index 9b00ec4f8..b2742e8f1 100644
--- a/tests/unit/query_procedures_mgp_graph.cpp
+++ b/tests/unit/query_procedures_mgp_graph.cpp
@@ -30,6 +30,7 @@
 #include "storage_test_utils.hpp"
 #include "test_utils.hpp"
 #include "utils/memory.hpp"
+#include "utils/variant_helpers.hpp"
 
 #define EXPECT_SUCCESS(...) EXPECT_EQ(__VA_ARGS__, mgp_error::MGP_ERROR_NO_ERROR)
 
@@ -90,11 +91,21 @@ size_t CountMaybeIterables(TMaybeIterable &&maybe_iterable) {
   return std::distance(iterable.begin(), iterable.end());
 }
 
+;
+
 void CheckEdgeCountBetween(const MgpVertexPtr &from, const MgpVertexPtr &to, const size_t number_of_edges_between) {
-  EXPECT_EQ(CountMaybeIterables(from->impl.InEdges(memgraph::storage::View::NEW)), 0);
-  EXPECT_EQ(CountMaybeIterables(from->impl.OutEdges(memgraph::storage::View::NEW)), number_of_edges_between);
-  EXPECT_EQ(CountMaybeIterables(to->impl.InEdges(memgraph::storage::View::NEW)), number_of_edges_between);
-  EXPECT_EQ(CountMaybeIterables(to->impl.OutEdges(memgraph::storage::View::NEW)), 0);
+  EXPECT_EQ(
+      CountMaybeIterables(std::visit([](auto impl) { return impl.InEdges(memgraph::storage::View::NEW); }, from->impl)),
+      0);
+  EXPECT_EQ(CountMaybeIterables(
+                std::visit([](auto impl) { return impl.OutEdges(memgraph::storage::View::NEW); }, from->impl)),
+            number_of_edges_between);
+  EXPECT_EQ(
+      CountMaybeIterables(std::visit([](auto impl) { return impl.InEdges(memgraph::storage::View::NEW); }, to->impl)),
+      number_of_edges_between);
+  EXPECT_EQ(
+      CountMaybeIterables(std::visit([](auto impl) { return impl.OutEdges(memgraph::storage::View::NEW); }, to->impl)),
+      0);
 }
 }  // namespace