From 9ec72bd96985bc3ecea5031f18dd90292daa5bc3 Mon Sep 17 00:00:00 2001
From: Kostas Kyrimis <kostaskyrim@gmail.com>
Date: Wed, 16 Nov 2022 18:41:22 +0200
Subject: [PATCH 1/5] Add GetProperties shard handler and tests

---
 src/query/v2/requests.hpp         |  40 +++--
 src/storage/v3/request_helper.cpp |  62 +++++++-
 src/storage/v3/request_helper.hpp |  11 ++
 src/storage/v3/shard_rsm.cpp      | 138 ++++++++++++++++-
 tests/simulation/shard_rsm.cpp    | 248 +++++++++++++++++++++++++++++-
 5 files changed, 474 insertions(+), 25 deletions(-)

diff --git a/src/query/v2/requests.hpp b/src/query/v2/requests.hpp
index 2ea4efb57..4c9280848 100644
--- a/src/query/v2/requests.hpp
+++ b/src/query/v2/requests.hpp
@@ -321,10 +321,6 @@ struct Expression {
   std::string expression;
 };
 
-struct Filter {
-  std::string filter_expression;
-};
-
 enum class OrderingDirection { ASCENDING = 1, DESCENDING = 2 };
 
 struct OrderBy {
@@ -366,22 +362,42 @@ struct ScanVerticesResponse {
   std::vector<ScanResultRow> results;
 };
 
-using VertexOrEdgeIds = std::variant<VertexId, EdgeId>;
+struct VertexAndEdgeId {
+  VertexId vertex;
+  std::optional<EdgeTypeId> edge;
+};
 
 struct GetPropertiesRequest {
   Hlc transaction_id;
-  // Shouldn't contain mixed vertex and edge ids
-  VertexOrEdgeIds vertex_or_edge_ids;
+  std::vector<VertexAndEdgeId> vertices_and_edges;
+
   std::vector<PropertyId> property_ids;
-  std::vector<Expression> expressions;
-  bool only_unique = false;
-  std::optional<std::vector<OrderBy>> order_by;
+  std::vector<std::string> expressions;
+
+  std::vector<OrderBy> order_by;
   std::optional<size_t> limit;
-  std::optional<Filter> filter;
+
+  // Return only the properties of the vertices or edges that the filter predicate
+  // evaluates to true
+  std::optional<std::string> filter;
+};
+
+struct PropIdValue {
+  std::vector<PropertyId> ids;
+  std::vector<Value> properties;
+};
+
+struct GetPropertiesResultRow {
+  VertexAndEdgeId vertex_and_edge;
+
+  PropIdValue properies_and_ids;
+  std::vector<Value> evaluated_expressions;
 };
 
 struct GetPropertiesResponse {
-  bool success;
+  std::vector<GetPropertiesResultRow> result_row;
+  enum RequestResult : uint16_t { OUT_OF_SHARD_RANGE, SUCCESS, FAILURE };
+  RequestResult result;
 };
 
 enum class EdgeDirection : uint8_t { OUT = 1, IN = 2, BOTH = 3 };
diff --git a/src/storage/v3/request_helper.cpp b/src/storage/v3/request_helper.cpp
index bb1c8bca4..96170dccf 100644
--- a/src/storage/v3/request_helper.cpp
+++ b/src/storage/v3/request_helper.cpp
@@ -11,6 +11,7 @@
 
 #include "storage/v3/request_helper.hpp"
 
+#include <iterator>
 #include <vector>
 
 #include "pretty_print_ast_to_original_expression.hpp"
@@ -43,19 +44,74 @@ std::vector<Element> OrderByElements(Shard::Accessor &acc, DbAccessor &dba, Vert
     properties_order_by.reserve(order_bys.size());
 
     for (const auto &order_by : order_bys) {
-      const auto val =
+      auto val =
           ComputeExpression(dba, *it, std::nullopt, order_by.expression.expression, expr::identifier_node_symbol, "");
-      properties_order_by.push_back(val);
+      properties_order_by.push_back(std::move(val));
     }
     ordered.push_back({std::move(properties_order_by), *it});
   }
 
-  std::sort(ordered.begin(), ordered.end(), [compare_typed_values](const auto &pair1, const auto &pair2) {
+  std::sort(ordered.begin(), ordered.end(), [&compare_typed_values](const auto &pair1, const auto &pair2) {
     return compare_typed_values(pair1.properties_order_by, pair2.properties_order_by);
   });
   return ordered;
 }
 
+std::vector<GetPropElement> OrderByElements(DbAccessor &dba, std::vector<msgs::OrderBy> &order_by,
+                                            std::vector<GetPropElement> &&vertices) {
+  std::vector<Ordering> ordering;
+  ordering.reserve(order_by.size());
+  for (const auto &order : order_by) {
+    switch (order.direction) {
+      case memgraph::msgs::OrderingDirection::ASCENDING: {
+        ordering.push_back(Ordering::ASC);
+        break;
+      }
+      case memgraph::msgs::OrderingDirection::DESCENDING: {
+        ordering.push_back(Ordering::DESC);
+        break;
+      }
+    }
+  }
+  struct PropElement {
+    std::vector<TypedValue> properties_order_by;
+    VertexAccessor vertex_acc;
+    GetPropElement *original_element;
+  };
+
+  std::vector<PropElement> ordered;
+  auto compare_typed_values = TypedValueVectorCompare(ordering);
+  for (auto &vertex : vertices) {
+    std::vector<TypedValue> properties;
+    properties.reserve(order_by.size());
+    const auto *symbol = (vertex.edge_acc) ? expr::identifier_edge_symbol : expr::identifier_node_symbol;
+    for (const auto &order : order_by) {
+      TypedValue val;
+      if (vertex.edge_acc) {
+        val = ComputeExpression(dba, vertex.vertex_acc, vertex.edge_acc, order.expression.expression, "", symbol);
+      } else {
+        val = ComputeExpression(dba, vertex.vertex_acc, vertex.edge_acc, order.expression.expression, symbol, "");
+      }
+      properties.push_back(std::move(val));
+    }
+
+    ordered.push_back({std::move(properties), vertex.vertex_acc, &vertex});
+  }
+
+  std::sort(ordered.begin(), ordered.end(), [&compare_typed_values](const auto &lhs, const auto &rhs) {
+    return compare_typed_values(lhs.properties_order_by, rhs.properties_order_by);
+  });
+
+  std::vector<GetPropElement> results_ordered;
+  results_ordered.reserve(ordered.size());
+
+  for (auto &elem : ordered) {
+    results_ordered.push_back(std::move(*elem.original_element));
+  }
+
+  return results_ordered;
+}
+
 VerticesIterable::Iterator GetStartVertexIterator(VerticesIterable &vertex_iterable,
                                                   const std::vector<PropertyValue> &start_ids, const View view) {
   auto it = vertex_iterable.begin();
diff --git a/src/storage/v3/request_helper.hpp b/src/storage/v3/request_helper.hpp
index 24ed40f8c..f98f18cd3 100644
--- a/src/storage/v3/request_helper.hpp
+++ b/src/storage/v3/request_helper.hpp
@@ -12,6 +12,7 @@
 #include <vector>
 
 #include "ast/ast.hpp"
+#include "query/v2/requests.hpp"
 #include "storage/v3/bindings/typed_value.hpp"
 #include "storage/v3/shard.hpp"
 #include "storage/v3/vertex_accessor.hpp"
@@ -104,9 +105,19 @@ struct Element {
   VertexAccessor vertex_acc;
 };
 
+struct GetPropElement {
+  std::vector<TypedValue> properties_order_by;
+  std::vector<PropertyId> ids;
+  VertexAccessor vertex_acc;
+  std::optional<EdgeAccessor> edge_acc;
+};
+
 std::vector<Element> OrderByElements(Shard::Accessor &acc, DbAccessor &dba, VerticesIterable &vertices_iterable,
                                      std::vector<msgs::OrderBy> &order_bys);
 
+std::vector<GetPropElement> OrderByElements(DbAccessor &dba, std::vector<msgs::OrderBy> &order_bys,
+                                            std::vector<GetPropElement> &&vertices);
+
 VerticesIterable::Iterator GetStartVertexIterator(VerticesIterable &vertex_iterable,
                                                   const std::vector<PropertyValue> &start_ids, View view);
 
diff --git a/src/storage/v3/shard_rsm.cpp b/src/storage/v3/shard_rsm.cpp
index f288e5e27..4036bbd80 100644
--- a/src/storage/v3/shard_rsm.cpp
+++ b/src/storage/v3/shard_rsm.cpp
@@ -15,8 +15,10 @@
 #include <optional>
 #include <unordered_set>
 #include <utility>
+#include <variant>
 
 #include "parser/opencypher/parser.hpp"
+#include "pretty_print_ast_to_original_expression.hpp"
 #include "query/v2/requests.hpp"
 #include "storage/v2/vertex.hpp"
 #include "storage/v2/view.hpp"
@@ -29,6 +31,7 @@
 #include "storage/v3/bindings/symbol_generator.hpp"
 #include "storage/v3/bindings/symbol_table.hpp"
 #include "storage/v3/bindings/typed_value.hpp"
+#include "storage/v3/conversions.hpp"
 #include "storage/v3/expr.hpp"
 #include "storage/v3/id_types.hpp"
 #include "storage/v3/key_store.hpp"
@@ -159,10 +162,15 @@ std::optional<std::map<PropertyId, Value>> CollectAllPropertiesFromAccessor(cons
 }
 
 bool FilterOnVertex(DbAccessor &dba, const storage::v3::VertexAccessor &v_acc, const std::vector<std::string> &filters,
-                    const std::string_view node_name) {
-  return std::ranges::all_of(filters, [&node_name, &dba, &v_acc](const auto &filter_expr) {
-    auto res = ComputeExpression(dba, v_acc, std::nullopt, filter_expr, node_name, "");
-    return res.IsBool() && res.ValueBool();
+                    const std::string_view node_name, const std::optional<EdgeAccessor> &e_acc = std::nullopt) {
+  return std::ranges::all_of(filters, [&node_name, &dba, &v_acc, &e_acc](const auto &filter_expr) {
+    TypedValue result;
+    if (e_acc) {
+      result = ComputeExpression(dba, v_acc, e_acc, filter_expr, "", node_name);
+    } else {
+      result = ComputeExpression(dba, v_acc, e_acc, filter_expr, node_name, "");
+    }
+    return result.IsBool() && result.ValueBool();
   });
 }
 
@@ -936,9 +944,125 @@ msgs::WriteResponses ShardRsm::ApplyWrite(msgs::CommitRequest &&req) {
   return msgs::CommitResponse{true};
 };
 
-// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
-msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest && /*req*/) {
-  return msgs::GetPropertiesResponse{};
+msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
+  if (req.vertices_and_edges.empty()) {
+    return msgs::GetPropertiesResponse{.result = msgs::GetPropertiesResponse::FAILURE};
+  }
+
+  auto shard_acc = shard_->Access(req.transaction_id);
+  auto dba = DbAccessor{&shard_acc};
+  const auto view = storage::v3::View::NEW;
+
+  auto collect_props = [](const VertexAccessor &acc, const std::vector<PropertyId> &props, View view,
+                          const std::optional<EdgeAccessor> &e_acc) mutable -> std::optional<GetPropElement> {
+    std::vector<TypedValue> properties;
+    std::vector<PropertyId> ids;
+    for (const auto &prop : props) {
+      Result<PropertyValue> result{PropertyValue()};
+      if (e_acc) {
+        result = e_acc->GetProperty(prop, view);
+      } else {
+        result = acc.GetProperty(prop, view);
+      }
+      if (result.HasError() && result.GetError() == Error::NONEXISTENT_OBJECT) {
+        continue;
+      }
+      if (result.HasError()) {
+        spdlog::debug("Encountered an Error while trying to get a vertex property.");
+        return std::nullopt;
+      }
+      properties.push_back(PropertyToTypedValue<TypedValue>(result.GetValue()));
+      ids.push_back(prop);
+    }
+    GetPropElement element{std::move(properties), std::move(ids), acc, e_acc};
+    return {std::move(element)};
+  };
+
+  auto find_edge = [](const VertexAccessor &v, const EdgeTypeId &e) -> std::optional<EdgeAccessor> {
+    auto in = v.InEdges(view, {e});
+    MG_ASSERT(in.HasValue());
+    for (auto &edge : in.GetValue()) {
+      if (edge.EdgeType() == e) {
+        return edge;
+      }
+    }
+
+    auto out = v.OutEdges(view, {e});
+    MG_ASSERT(out.HasValue());
+    for (auto &edge : out.GetValue()) {
+      if (edge.EdgeType() == e) {
+        return edge;
+      }
+    }
+    return std::nullopt;
+  };
+
+  std::vector<GetPropElement> elements;
+
+  for (const auto &[vertex, maybe_edge] : req.vertices_and_edges) {
+    const auto &[label, pk_v] = vertex;
+    auto pk = ConvertPropertyVector(pk_v);
+    auto v_acc = dba.FindVertex(pk, view);
+    if (!v_acc) {
+      return msgs::GetPropertiesResponse{.result = msgs::GetPropertiesResponse::OUT_OF_SHARD_RANGE};
+    }
+    std::optional<EdgeAccessor> e_acc;
+    if (maybe_edge) {
+      e_acc = find_edge(*v_acc, *maybe_edge);
+      if (!e_acc) {
+        return msgs::GetPropertiesResponse{.result = msgs::GetPropertiesResponse::OUT_OF_SHARD_RANGE};
+      }
+    }
+
+    const auto *symbol = (maybe_edge) ? expr::identifier_edge_symbol : expr::identifier_node_symbol;
+    if (req.filter && !FilterOnVertex(dba, *v_acc, {*req.filter}, symbol, e_acc)) {
+      continue;
+    }
+
+    std::optional<GetPropElement> collected_properties;
+    collected_properties = collect_props(*v_acc, req.property_ids, view, e_acc);
+    if (!collected_properties) {
+      return msgs::GetPropertiesResponse{.result = msgs::GetPropertiesResponse::FAILURE};
+    }
+    if (collected_properties->ids.empty()) {
+      continue;
+    }
+    elements.push_back(std::move(*collected_properties));
+  }
+
+  if (!req.order_by.empty()) {
+    elements = OrderByElements(dba, req.order_by, std::move(elements));
+  }
+
+  std::vector<msgs::GetPropertiesResultRow> results;
+  results.reserve(elements.size());
+
+  const auto has_expr_to_evaluate = !req.expressions.empty();
+  size_t limit = elements.size();
+  if (req.limit && *req.limit < elements.size()) {
+    limit = *req.limit;
+  }
+  for (size_t index = 0; index != limit; ++index) {
+    auto &element = elements.at(index);
+    const auto id = element.vertex_acc.Id(view).GetValue();
+    std::optional<EdgeTypeId> e_type =
+        (element.edge_acc) ? std::make_optional(element.edge_acc->EdgeType()) : std::nullopt;
+    msgs::VertexId v_id{msgs::Label{id.primary_label}, ConvertValueVector(id.primary_key)};
+    results.push_back(msgs::GetPropertiesResultRow{
+        .vertex_and_edge = {.vertex = std::move(v_id), .edge = e_type},
+        .properies_and_ids = {
+            .ids = std::move(element.ids),
+            .properties = ConvertToValueVectorFromTypedValueVector(std::move(element.properties_order_by))}});
+    if (has_expr_to_evaluate) {
+      auto expression_results = ConvertToValueVectorFromTypedValueVector(
+          EvaluateVertexExpressions(dba, element.vertex_acc, req.expressions, expr::identifier_node_symbol));
+      results.back().evaluated_expressions = std::move(expression_results);
+    }
+  }
+
+  return msgs::GetPropertiesResponse{std::move(results), msgs::GetPropertiesResponse::SUCCESS};
 }
 
+// TODO(kostasrim) Handle edges
+
 }  // namespace memgraph::storage::v3
diff --git a/tests/simulation/shard_rsm.cpp b/tests/simulation/shard_rsm.cpp
index 64d0a0861..f2a90262f 100644
--- a/tests/simulation/shard_rsm.cpp
+++ b/tests/simulation/shard_rsm.cpp
@@ -446,6 +446,56 @@ std::tuple<size_t, std::optional<msgs::VertexId>> AttemptToScanAllWithExpression
   }
 }
 
+msgs::GetPropertiesResponse AttemptToGetProperties(ShardClient &client, std::vector<PropertyId> properties,
+                                                   std::vector<msgs::VertexId> vertices,
+                                                   std::vector<msgs::EdgeTypeId> edges,
+                                                   std::optional<size_t> limit = std::nullopt,
+                                                   std::optional<uint64_t> filter_prop = std::nullopt,
+                                                   bool edge = false,
+                                                   std::optional<std::string> order_by = std::nullopt) {
+  msgs::GetPropertiesRequest req{};
+  req.transaction_id.logical_id = GetTransactionId();
+  req.property_ids = std::move(properties);
+
+  if (filter_prop) {
+    std::string filter_expr = (!edge) ? "MG_SYMBOL_NODE.prop1 >= " : "MG_SYMBOL_EDGE.e_prop = ";
+    filter_expr += std::to_string(*filter_prop);
+    req.filter = std::make_optional(std::move(filter_expr));
+  }
+  if (order_by) {
+    std::string filter_expr = (!edge) ? "MG_SYMBOL_NODE." : "MG_SYMBOL_EDGE.";
+    filter_expr += *order_by;
+    msgs::OrderBy order_by{.expression = {std::move(filter_expr)}, .direction = msgs::OrderingDirection::DESCENDING};
+    std::vector<msgs::OrderBy> request_order_by;
+    request_order_by.push_back(std::move(order_by));
+    req.order_by = std::move(request_order_by);
+  }
+  if (limit) {
+    req.limit = limit;
+  }
+  req.expressions = {std::string("5 = 5")};
+  std::vector<msgs::VertexAndEdgeId> req_v;
+  for (auto &v : vertices) {
+    req_v.push_back(msgs::VertexAndEdgeId{.vertex = std::move(v)});
+  }
+  for (auto index = 0; index != edges.size(); ++index) {
+    req_v[index].edge = edges[index];
+  }
+  req.vertices_and_edges = std::move(req_v);
+
+  while (true) {
+    auto read_res = client.SendReadRequest(req);
+    if (read_res.HasError()) {
+      continue;
+    }
+
+    auto write_response_result = read_res.GetValue();
+    auto write_response = std::get<msgs::GetPropertiesResponse>(write_response_result);
+
+    return write_response;
+  }
+}
+
 void AttemptToScanAllWithOrderByOnPrimaryProperty(ShardClient &client, msgs::VertexId start_id, uint64_t batch_limit) {
   msgs::ScanVerticesRequest scan_req;
   scan_req.batch_limit = batch_limit;
@@ -1064,6 +1114,193 @@ void TestExpandOneGraphTwo(ShardClient &client) {
   }
 }
 
+void TestGetProperties(ShardClient &client) {
+  const auto unique_prop_val_1 = GetUniqueInteger();
+  const auto unique_prop_val_2 = GetUniqueInteger();
+  const auto unique_prop_val_3 = GetUniqueInteger();
+  const auto unique_prop_val_4 = GetUniqueInteger();
+  const auto unique_prop_val_5 = GetUniqueInteger();
+
+  MG_ASSERT(AttemptToCreateVertex(client, unique_prop_val_1));
+  MG_ASSERT(AttemptToCreateVertex(client, unique_prop_val_2));
+  MG_ASSERT(AttemptToCreateVertex(client, unique_prop_val_3));
+  MG_ASSERT(AttemptToCreateVertex(client, unique_prop_val_4));
+  MG_ASSERT(AttemptToCreateVertex(client, unique_prop_val_5));
+
+  const msgs::Label prim_label = {.id = get_primary_label()};
+  const msgs::PrimaryKey prim_key = {msgs::Value(static_cast<int64_t>(unique_prop_val_1))};
+  const msgs::VertexId v_id = {prim_label, prim_key};
+  const msgs::PrimaryKey prim_key_2 = {msgs::Value(static_cast<int64_t>(unique_prop_val_2))};
+  const msgs::VertexId v_id_2 = {prim_label, prim_key_2};
+  const msgs::PrimaryKey prim_key_3 = {msgs::Value(static_cast<int64_t>(unique_prop_val_3))};
+  const msgs::VertexId v_id_3 = {prim_label, prim_key_3};
+  const msgs::PrimaryKey prim_key_4 = {msgs::Value(static_cast<int64_t>(unique_prop_val_4))};
+  const msgs::VertexId v_id_4 = {prim_label, prim_key_4};
+  const msgs::PrimaryKey prim_key_5 = {msgs::Value(static_cast<int64_t>(unique_prop_val_5))};
+  const msgs::VertexId v_id_5 = {prim_label, prim_key_5};
+  const auto prop_id_2 = PropertyId::FromUint(2);
+  const auto prop_id_4 = PropertyId::FromUint(4);
+  const auto prop_id_5 = PropertyId::FromUint(5);
+  // Vertices
+  {
+    // No properties
+    const auto result = AttemptToGetProperties(client, {}, {v_id, v_id_2}, {}, std::nullopt, unique_prop_val_2);
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(result.result_row.empty());
+  }
+  {
+    // All properties
+    const auto result = AttemptToGetProperties(client, {prop_id_2, prop_id_4, prop_id_5}, {v_id, v_id_2, v_id_3}, {});
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 3);
+    for (const auto &elem : result.result_row) {
+      MG_ASSERT(elem.properies_and_ids.ids.size() == 3);
+      MG_ASSERT(elem.properies_and_ids.properties.size() == 3);
+    }
+  }
+  {
+    // Two properties from two vertices with a filter on unique_prop_5
+    const auto result = AttemptToGetProperties(client, {prop_id_2, prop_id_4}, {v_id, v_id_2, v_id_5}, {}, std::nullopt,
+                                               unique_prop_val_5);
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 1);
+  }
+  {
+    // One property from three vertices.
+    const auto result = AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {});
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 3);
+    MG_ASSERT(result.result_row[0].properies_and_ids.ids.size() == 1);
+    MG_ASSERT(result.result_row[1].properies_and_ids.ids.size() == 1);
+    MG_ASSERT(result.result_row[2].properies_and_ids.ids.size() == 1);
+  }
+  {
+    // Same as before but with limit of 1 row
+    const auto result =
+        AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::make_optional<size_t>(1));
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 1);
+  }
+  {
+    // Same as before but with a limit greater than the elements returned
+    const auto result =
+        AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::make_optional<size_t>(5));
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 3);
+  }
+  {
+    // Order by on `prop1` (descending)
+    const auto result = AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::nullopt,
+                                               std::nullopt, false, "prop1");
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 3);
+    MG_ASSERT(result.result_row[0].vertex_and_edge.vertex == v_id_3);
+    MG_ASSERT(result.result_row[1].vertex_and_edge.vertex == v_id_2);
+    MG_ASSERT(result.result_row[2].vertex_and_edge.vertex == v_id);
+  }
+  {
+    // Order by and filter on >= unique_prop_val_3 && assert result row data members
+    const auto result = AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3, v_id_4, v_id_5}, {},
+                                               std::nullopt, unique_prop_val_3, false, "prop1");
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 3);
+    MG_ASSERT(result.result_row[0].vertex_and_edge.vertex == v_id_5);
+    MG_ASSERT(result.result_row[0].properies_and_ids.properties.size() == 1);
+    MG_ASSERT(result.result_row[0].properies_and_ids.properties.front() == prim_key_5.front());
+    MG_ASSERT(result.result_row[0].properies_and_ids.ids.size() == 1);
+    MG_ASSERT(result.result_row[0].properies_and_ids.ids.front() == prop_id_2);
+    MG_ASSERT(result.result_row[0].evaluated_expressions.size() == 1);
+    MG_ASSERT(result.result_row[0].evaluated_expressions.front() == msgs::Value(true));
+
+    MG_ASSERT(result.result_row[1].vertex_and_edge.vertex == v_id_4);
+    MG_ASSERT(result.result_row[1].properies_and_ids.properties.size() == 1);
+    MG_ASSERT(result.result_row[1].properies_and_ids.properties.front() == prim_key_4.front());
+    MG_ASSERT(result.result_row[1].properies_and_ids.ids.size() == 1);
+    MG_ASSERT(result.result_row[1].properies_and_ids.ids.front() == prop_id_2);
+    MG_ASSERT(result.result_row[1].evaluated_expressions.size() == 1);
+    MG_ASSERT(result.result_row[1].evaluated_expressions.front() == msgs::Value(true));
+
+    MG_ASSERT(result.result_row[2].vertex_and_edge.vertex == v_id_3);
+    MG_ASSERT(result.result_row[2].properies_and_ids.properties.size() == 1);
+    MG_ASSERT(result.result_row[2].properies_and_ids.properties.front() == prim_key_3.front());
+    MG_ASSERT(result.result_row[2].properies_and_ids.ids.size() == 1);
+    MG_ASSERT(result.result_row[2].properies_and_ids.ids.front() == prop_id_2);
+    MG_ASSERT(result.result_row[2].evaluated_expressions.size() == 1);
+    MG_ASSERT(result.result_row[2].evaluated_expressions.front() == msgs::Value(true));
+  }
+
+  // Edges
+  const auto edge_gid = GetUniqueInteger();
+  const auto edge_type_id = EdgeTypeId::FromUint(GetUniqueInteger());
+  const auto unique_edge_prop_id = 7;
+  const auto edge_prop_val = GetUniqueInteger();
+  MG_ASSERT(AttemptToAddEdgeWithProperties(client, unique_prop_val_1, unique_prop_val_2, edge_gid, unique_edge_prop_id,
+                                           edge_prop_val, {edge_type_id}));
+  const auto edge_gid_2 = GetUniqueInteger();
+  const auto edge_prop_val_2 = GetUniqueInteger();
+  MG_ASSERT(AttemptToAddEdgeWithProperties(client, unique_prop_val_3, unique_prop_val_4, edge_gid_2,
+                                           unique_edge_prop_id, edge_prop_val_2, {edge_type_id}));
+  const auto edge_prop_id = PropertyId::FromUint(unique_edge_prop_id);
+  // no properties
+  {
+    const auto result = AttemptToGetProperties(client, {}, {v_id_2, v_id_3}, {edge_type_id, edge_type_id});
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(result.result_row.empty());
+  }
+  // properties for two vertices
+  {
+    const auto result = AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, {edge_type_id, edge_type_id});
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 2);
+  }
+  // filter
+  {
+    const auto result = AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, {edge_type_id, edge_type_id},
+                                               {}, {edge_prop_val}, true);
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 1);
+    MG_ASSERT(result.result_row.front().vertex_and_edge.edge);
+    MG_ASSERT(result.result_row.front().vertex_and_edge.edge.value() == edge_type_id);
+    MG_ASSERT(result.result_row.front().properies_and_ids.properties.size() == 1);
+    MG_ASSERT(result.result_row.front().properies_and_ids.properties.front() ==
+              msgs::Value(static_cast<int64_t>(edge_prop_val)));
+  }
+  // Order by
+  {
+    const auto result = AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, {edge_type_id, edge_type_id},
+                                               {}, {}, true, "e_prop");
+    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 2);
+    MG_ASSERT(result.result_row[0].vertex_and_edge.vertex == v_id_3);
+    MG_ASSERT(result.result_row[0].vertex_and_edge.edge);
+    MG_ASSERT(result.result_row[0].vertex_and_edge.edge.value() == edge_type_id);
+    MG_ASSERT(result.result_row[0].properies_and_ids.properties.size() == 1);
+    MG_ASSERT(result.result_row[0].properies_and_ids.properties.front() ==
+              msgs::Value(static_cast<int64_t>(edge_prop_val_2)));
+    MG_ASSERT(result.result_row[0].evaluated_expressions.size() == 1);
+    MG_ASSERT(result.result_row[0].evaluated_expressions.front() == msgs::Value(true));
+
+    MG_ASSERT(result.result_row[1].vertex_and_edge.vertex == v_id_2);
+    MG_ASSERT(result.result_row[1].vertex_and_edge.edge);
+    MG_ASSERT(result.result_row[1].vertex_and_edge.edge.value() == edge_type_id);
+    MG_ASSERT(result.result_row[1].properies_and_ids.properties.size() == 1);
+    MG_ASSERT(result.result_row[1].properies_and_ids.properties.front() ==
+              msgs::Value(static_cast<int64_t>(edge_prop_val)));
+    MG_ASSERT(result.result_row[1].evaluated_expressions.size() == 1);
+    MG_ASSERT(result.result_row[1].evaluated_expressions.front() == msgs::Value(true));
+  }
+}
+
 }  // namespace
 
 int TestMessages() {
@@ -1102,9 +1339,12 @@ int TestMessages() {
   auto shard_ptr2 = std::make_unique<Shard>(get_primary_label(), min_prim_key, max_prim_key, schema_prop);
   auto shard_ptr3 = std::make_unique<Shard>(get_primary_label(), min_prim_key, max_prim_key, schema_prop);
 
-  shard_ptr1->StoreMapping({{1, "label"}, {2, "prop1"}, {3, "label1"}, {4, "prop2"}, {5, "prop3"}, {6, "prop4"}});
-  shard_ptr2->StoreMapping({{1, "label"}, {2, "prop1"}, {3, "label1"}, {4, "prop2"}, {5, "prop3"}, {6, "prop4"}});
-  shard_ptr3->StoreMapping({{1, "label"}, {2, "prop1"}, {3, "label1"}, {4, "prop2"}, {5, "prop3"}, {6, "prop4"}});
+  shard_ptr1->StoreMapping(
+      {{1, "label"}, {2, "prop1"}, {3, "label1"}, {4, "prop2"}, {5, "prop3"}, {6, "prop4"}, {7, "e_prop"}});
+  shard_ptr2->StoreMapping(
+      {{1, "label"}, {2, "prop1"}, {3, "label1"}, {4, "prop2"}, {5, "prop3"}, {6, "prop4"}, {7, "e_prop"}});
+  shard_ptr3->StoreMapping(
+      {{1, "label"}, {2, "prop1"}, {3, "label1"}, {4, "prop2"}, {5, "prop3"}, {6, "prop4"}, {7, "e_prop"}});
 
   std::vector<Address> address_for_1{shard_server_2_address, shard_server_3_address};
   std::vector<Address> address_for_2{shard_server_1_address, shard_server_3_address};
@@ -1145,6 +1385,8 @@ int TestMessages() {
   TestExpandOneGraphOne(client);
   TestExpandOneGraphTwo(client);
 
+  // GetProperties tests
+  TestGetProperties(client);
   simulator.ShutDown();
 
   SimulatorStats stats = simulator.Stats();

From 9621532d3d1993b07ee6a6de77d66a48440fb4c0 Mon Sep 17 00:00:00 2001
From: Kostas Kyrimis <kostaskyrim@gmail.com>
Date: Wed, 30 Nov 2022 14:59:00 +0200
Subject: [PATCH 2/5] Prototype suggested changes and polish PR

---
 src/query/v2/requests.hpp         |  22 +--
 src/storage/v3/request_helper.cpp | 145 ++++++++---------
 src/storage/v3/request_helper.hpp |  72 +++++++--
 src/storage/v3/shard_rsm.cpp      | 251 +++++++++++++++++++-----------
 tests/simulation/shard_rsm.cpp    | 145 +++++++++--------
 5 files changed, 360 insertions(+), 275 deletions(-)

diff --git a/src/query/v2/requests.hpp b/src/query/v2/requests.hpp
index c751b16ec..9ff9a1bae 100644
--- a/src/query/v2/requests.hpp
+++ b/src/query/v2/requests.hpp
@@ -368,16 +368,12 @@ struct ScanVerticesResponse {
   std::vector<ScanResultRow> results;
 };
 
-struct VertexAndEdgeId {
-  VertexId vertex;
-  std::optional<EdgeTypeId> edge;
-};
-
 struct GetPropertiesRequest {
   Hlc transaction_id;
-  std::vector<VertexAndEdgeId> vertices_and_edges;
+  std::vector<VertexId> vertex_ids;
+  std::vector<std::pair<VertexId, EdgeId>> vertices_and_edges;
 
-  std::vector<PropertyId> property_ids;
+  std::optional<std::vector<PropertyId>> property_ids;
   std::vector<std::string> expressions;
 
   std::vector<OrderBy> order_by;
@@ -388,22 +384,16 @@ struct GetPropertiesRequest {
   std::optional<std::string> filter;
 };
 
-struct PropIdValue {
-  std::vector<PropertyId> ids;
-  std::vector<Value> properties;
-};
-
 struct GetPropertiesResultRow {
-  VertexAndEdgeId vertex_and_edge;
+  VertexId vertex;
+  std::optional<EdgeId> edge;
 
-  PropIdValue properies_and_ids;
+  std::vector<std::pair<PropertyId, Value>> props;
   std::vector<Value> evaluated_expressions;
 };
 
 struct GetPropertiesResponse {
   std::vector<GetPropertiesResultRow> result_row;
-  enum RequestResult : uint16_t { OUT_OF_SHARD_RANGE, SUCCESS, FAILURE };
-  RequestResult result;
   std::optional<ShardError> error;
 };
 
diff --git a/src/storage/v3/request_helper.cpp b/src/storage/v3/request_helper.cpp
index 7d367d850..260147a25 100644
--- a/src/storage/v3/request_helper.cpp
+++ b/src/storage/v3/request_helper.cpp
@@ -14,6 +14,7 @@
 #include <iterator>
 #include <vector>
 
+#include "pretty_print_ast_to_original_expression.hpp"
 #include "storage/v3/bindings/db_accessor.hpp"
 #include "storage/v3/bindings/pretty_print_ast_to_original_expression.hpp"
 #include "storage/v3/expr.hpp"
@@ -221,30 +222,39 @@ std::vector<TypedValue> EvaluateVertexExpressions(DbAccessor &dba, const VertexA
   return evaluated_expressions;
 }
 
+std::vector<TypedValue> EvaluateEdgeExpressions(DbAccessor &dba, const VertexAccessor &v_acc, const EdgeAccessor &e_acc,
+                                                const std::vector<std::string> &expressions) {
+  std::vector<TypedValue> evaluated_expressions;
+  evaluated_expressions.reserve(expressions.size());
+
+  std::transform(expressions.begin(), expressions.end(), std::back_inserter(evaluated_expressions),
+                 [&dba, &v_acc, &e_acc](const auto &expression) {
+                   return ComputeExpression(dba, v_acc, e_acc, expression, expr::identifier_node_symbol,
+                                            expr::identifier_edge_symbol);
+                 });
+
+  return evaluated_expressions;
+}
+
 ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesFromAccessor(const VertexAccessor &acc, View view,
                                                                           const Schemas::Schema &schema) {
-  std::map<PropertyId, Value> ret;
-  auto props = acc.Properties(view);
-  if (props.HasError()) {
-    spdlog::debug("Encountered an error while trying to get vertex properties.");
-    return props.GetError();
+  auto ret = impl::CollectAllPropertiesImpl<VertexAccessor>(acc, view);
+  if (ret.HasError()) {
+    return ret.GetError();
   }
 
-  auto &properties = props.GetValue();
-  std::transform(properties.begin(), properties.end(), std::inserter(ret, ret.begin()),
-                 [](std::pair<const PropertyId, PropertyValue> &pair) {
-                   return std::make_pair(pair.first, FromPropertyValueToValue(std::move(pair.second)));
-                 });
-  properties.clear();
-
   auto pks = PrimaryKeysFromAccessor(acc, view, schema);
   if (pks) {
-    ret.merge(*pks);
+    ret.GetValue().merge(*pks);
   }
 
   return ret;
 }
 
+ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesFromAccessor(const VertexAccessor &acc, View view) {
+  return impl::CollectAllPropertiesImpl(acc, view);
+}
+
 EdgeUniquenessFunction InitializeEdgeUniquenessFunction(bool only_unique_neighbor_rows) {
   // Functions to select connecting edges based on uniquness
   EdgeUniquenessFunction maybe_filter_based_on_edge_uniquness;
@@ -351,15 +361,19 @@ EdgeFiller InitializeEdgeFillerFunction(const msgs::ExpandOneRequest &req) {
   return edge_filler;
 }
 
-bool FilterOnVertex(DbAccessor &dba, const storage::v3::VertexAccessor &v_acc, const std::vector<std::string> &filters,
-                    const std::string_view node_name, const std::optional<EdgeAccessor> &e_acc) {
-  return std::ranges::all_of(filters, [&node_name, &dba, &v_acc, &e_acc](const auto &filter_expr) {
-    TypedValue result;
-    if (e_acc) {
-      result = ComputeExpression(dba, v_acc, e_acc, filter_expr, "", node_name);
-    } else {
-      result = ComputeExpression(dba, v_acc, e_acc, filter_expr, node_name, "");
-    }
+bool FilterOnVertex(DbAccessor &dba, const storage::v3::VertexAccessor &v_acc,
+                    const std::vector<std::string> &filters) {
+  return std::ranges::all_of(filters, [&dba, &v_acc](const auto &filter_expr) {
+    const auto result = ComputeExpression(dba, v_acc, std::nullopt, filter_expr, expr::identifier_node_symbol, "");
+    return result.IsBool() && result.ValueBool();
+  });
+}
+
+bool FilterOnEdge(DbAccessor &dba, const storage::v3::VertexAccessor &v_acc, const EdgeAccessor &e_acc,
+                  const std::vector<std::string> &filters) {
+  return std::ranges::all_of(filters, [&dba, &v_acc, &e_acc](const auto &filter_expr) {
+    const auto result =
+        ComputeExpression(dba, v_acc, e_acc, filter_expr, expr::identifier_node_symbol, expr::identifier_edge_symbol);
     return result.IsBool() && result.ValueBool();
   });
 }
@@ -444,61 +458,6 @@ ShardResult<msgs::ExpandOneResultRow> GetExpandOneResult(
   return result_row;
 }
 
-std::vector<GetPropElement> OrderByElements(DbAccessor &dba, std::vector<msgs::OrderBy> &order_by,
-                                            std::vector<GetPropElement> &&vertices) {
-  std::vector<Ordering> ordering;
-  ordering.reserve(order_by.size());
-  for (const auto &order : order_by) {
-    switch (order.direction) {
-      case memgraph::msgs::OrderingDirection::ASCENDING: {
-        ordering.push_back(Ordering::ASC);
-        break;
-      }
-      case memgraph::msgs::OrderingDirection::DESCENDING: {
-        ordering.push_back(Ordering::DESC);
-        break;
-      }
-    }
-  }
-  struct PropElement {
-    std::vector<TypedValue> properties_order_by;
-    VertexAccessor vertex_acc;
-    GetPropElement *original_element;
-  };
-
-  std::vector<PropElement> ordered;
-  auto compare_typed_values = TypedValueVectorCompare(ordering);
-  for (auto &vertex : vertices) {
-    std::vector<TypedValue> properties;
-    properties.reserve(order_by.size());
-    const auto *symbol = (vertex.edge_acc) ? expr::identifier_edge_symbol : expr::identifier_node_symbol;
-    for (const auto &order : order_by) {
-      TypedValue val;
-      if (vertex.edge_acc) {
-        val = ComputeExpression(dba, vertex.vertex_acc, vertex.edge_acc, order.expression.expression, "", symbol);
-      } else {
-        val = ComputeExpression(dba, vertex.vertex_acc, vertex.edge_acc, order.expression.expression, symbol, "");
-      }
-      properties.push_back(std::move(val));
-    }
-
-    ordered.push_back({std::move(properties), vertex.vertex_acc, &vertex});
-  }
-
-  std::sort(ordered.begin(), ordered.end(), [&compare_typed_values](const auto &lhs, const auto &rhs) {
-    return compare_typed_values(lhs.properties_order_by, rhs.properties_order_by);
-  });
-
-  std::vector<GetPropElement> results_ordered;
-  results_ordered.reserve(ordered.size());
-
-  for (auto &elem : ordered) {
-    results_ordered.push_back(std::move(*elem.original_element));
-  }
-
-  return results_ordered;
-}
-
 VerticesIterable::Iterator GetStartVertexIterator(VerticesIterable &vertex_iterable,
                                                   const std::vector<PropertyValue> &primary_key, const View view) {
   auto it = vertex_iterable.begin();
@@ -584,4 +543,36 @@ std::vector<Element<EdgeAccessor>> OrderByEdges(DbAccessor &dba, std::vector<Edg
   return ordered;
 }
 
+std::vector<Element<std::pair<VertexAccessor, EdgeAccessor>>> OrderByEdges(
+    DbAccessor &dba, std::vector<EdgeAccessor> &iterable, std::vector<msgs::OrderBy> &order_by_edges,
+    const std::vector<VertexAccessor> &vertex_acc) {
+  MG_ASSERT(vertex_acc.size() == iterable.size());
+  std::vector<Ordering> ordering;
+  ordering.reserve(order_by_edges.size());
+  std::transform(order_by_edges.begin(), order_by_edges.end(), std::back_inserter(ordering),
+                 [](const auto &order_by) { return ConvertMsgsOrderByToOrdering(order_by.direction); });
+
+  std::vector<Element<std::pair<VertexAccessor, EdgeAccessor>>> ordered;
+  VertexAccessor current = vertex_acc.front();
+  size_t id = 0;
+  for (auto it = iterable.begin(); it != iterable.end(); it++, id++) {
+    current = vertex_acc[id];
+    std::vector<TypedValue> properties_order_by;
+    properties_order_by.reserve(order_by_edges.size());
+    std::transform(order_by_edges.begin(), order_by_edges.end(), std::back_inserter(properties_order_by),
+                   [&dba, it, current](const auto &order_by) {
+                     return ComputeExpression(dba, current, *it, order_by.expression.expression,
+                                              expr::identifier_node_symbol, expr::identifier_edge_symbol);
+                   });
+
+    ordered.push_back({std::move(properties_order_by), {current, *it}});
+  }
+
+  auto compare_typed_values = TypedValueVectorCompare(ordering);
+  std::sort(ordered.begin(), ordered.end(), [compare_typed_values](const auto &pair1, const auto &pair2) {
+    return compare_typed_values(pair1.properties_order_by, pair2.properties_order_by);
+  });
+  return ordered;
+}
+
 }  // namespace memgraph::storage::v3
diff --git a/src/storage/v3/request_helper.hpp b/src/storage/v3/request_helper.hpp
index f14535beb..13871d219 100644
--- a/src/storage/v3/request_helper.hpp
+++ b/src/storage/v3/request_helper.hpp
@@ -20,6 +20,7 @@
 #include "storage/v3/edge_accessor.hpp"
 #include "storage/v3/expr.hpp"
 #include "storage/v3/shard.hpp"
+#include "storage/v3/value_conversions.hpp"
 #include "storage/v3/vertex_accessor.hpp"
 #include "utils/template_utils.hpp"
 
@@ -31,7 +32,7 @@ using EdgeFiller =
 using msgs::Value;
 
 template <typename T>
-concept ObjectAccessor = utils::SameAsAnyOf<T, VertexAccessor, EdgeAccessor>;
+concept ObjectAccessor = utils::SameAsAnyOf<T, VertexAccessor, EdgeAccessor, std::pair<VertexAccessor, EdgeAccessor>>;
 
 inline bool TypedValueCompare(const TypedValue &a, const TypedValue &b) {
   // in ordering null comes after everything else
@@ -131,13 +132,6 @@ struct Element {
   TObjectAccessor object_acc;
 };
 
-struct GetPropElement {
-  std::vector<TypedValue> properties_order_by;
-  std::vector<PropertyId> ids;
-  VertexAccessor vertex_acc;
-  std::optional<EdgeAccessor> edge_acc;
-};
-
 template <typename T>
 concept VerticesIt = utils::SameAsAnyOf<T, VerticesIterable, std::vector<VertexAccessor>>;
 
@@ -170,12 +164,16 @@ std::vector<Element<VertexAccessor>> OrderByVertices(DbAccessor &dba, TIterable
   return ordered;
 }
 
+template <typename T>
+concept EdgeObjectAccessor = utils::SameAsAnyOf<T, EdgeAccessor, std::pair<VertexAccessor, EdgeAccessor>>;
+
 std::vector<Element<EdgeAccessor>> OrderByEdges(DbAccessor &dba, std::vector<EdgeAccessor> &iterable,
                                                 std::vector<msgs::OrderBy> &order_by_edges,
                                                 const VertexAccessor &vertex_acc);
 
-std::vector<GetPropElement> OrderByElements(DbAccessor &dba, std::vector<msgs::OrderBy> &order_bys,
-                                            std::vector<GetPropElement> &&vertices);
+std::vector<Element<std::pair<VertexAccessor, EdgeAccessor>>> OrderByEdges(
+    DbAccessor &dba, std::vector<EdgeAccessor> &iterable, std::vector<msgs::OrderBy> &order_by_edges,
+    const std::vector<VertexAccessor> &vertex_acc);
 
 VerticesIterable::Iterator GetStartVertexIterator(VerticesIterable &vertex_iterable,
                                                   const std::vector<PropertyValue> &primary_key, View view);
@@ -187,19 +185,65 @@ std::vector<Element<VertexAccessor>>::const_iterator GetStartOrderedElementsIter
 std::array<std::vector<EdgeAccessor>, 2> GetEdgesFromVertex(const VertexAccessor &vertex_accessor,
                                                             msgs::EdgeDirection direction);
 
-bool FilterOnVertex(DbAccessor &dba, const storage::v3::VertexAccessor &v_acc, const std::vector<std::string> &filters,
-                    const std::string_view node_name, const std::optional<EdgeAccessor> &e_acc = std::nullopt);
+bool FilterOnVertex(DbAccessor &dba, const storage::v3::VertexAccessor &v_acc, const std::vector<std::string> &filters);
+
+bool FilterOnEdge(DbAccessor &dba, const storage::v3::VertexAccessor &v_acc, const EdgeAccessor &e_acc,
+                  const std::vector<std::string> &filters);
 
 std::vector<TypedValue> EvaluateVertexExpressions(DbAccessor &dba, const VertexAccessor &v_acc,
                                                   const std::vector<std::string> &expressions,
                                                   std::string_view node_name);
 
-ShardResult<std::map<PropertyId, Value>> CollectSpecificPropertiesFromAccessor(const VertexAccessor &acc,
+std::vector<TypedValue> EvaluateEdgeExpressions(DbAccessor &dba, const VertexAccessor &v_acc, const EdgeAccessor &e_acc,
+                                                const std::vector<std::string> &expressions);
+
+template <typename T>
+concept TAccessor = utils::SameAsAnyOf<T, VertexAccessor, EdgeAccessor>;
+
+template <typename TAccessor>
+ShardResult<std::map<PropertyId, Value>> CollectSpecificPropertiesFromAccessor(const TAccessor &acc,
                                                                                const std::vector<PropertyId> &props,
-                                                                               View view);
+                                                                               View view) {
+  std::map<PropertyId, Value> ret;
+
+  for (const auto &prop : props) {
+    auto result = acc.GetProperty(prop, view);
+    if (result.HasError()) {
+      spdlog::debug("Encountered an Error while trying to get a vertex property.");
+      return result.GetError();
+    }
+    auto &value = result.GetValue();
+    ret.emplace(std::make_pair(prop, FromPropertyValueToValue(std::move(value))));
+  }
+
+  return ret;
+}
 
 ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesFromAccessor(const VertexAccessor &acc, View view,
                                                                           const Schemas::Schema &schema);
+namespace impl {
+template <typename TAccessor>
+ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesImpl(const TAccessor &acc, View view) {
+  std::map<PropertyId, Value> ret;
+  auto props = acc.Properties(view);
+  if (props.HasError()) {
+    spdlog::debug("Encountered an error while trying to get vertex properties.");
+    return props.GetError();
+  }
+
+  auto &properties = props.GetValue();
+  std::transform(properties.begin(), properties.end(), std::inserter(ret, ret.begin()),
+                 [](std::pair<const PropertyId, PropertyValue> &pair) {
+                   return std::make_pair(pair.first, conversions::FromPropertyValueToValue(std::move(pair.second)));
+                 });
+  return ret;
+}
+}  // namespace impl
+
+template <typename TAccessor>
+ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesFromAccessor(const TAccessor &acc, View view) {
+  return impl::CollectAllPropertiesImpl<TAccessor>(acc, view);
+}
 
 EdgeUniquenessFunction InitializeEdgeUniquenessFunction(bool only_unique_neighbor_rows);
 
diff --git a/src/storage/v3/shard_rsm.cpp b/src/storage/v3/shard_rsm.cpp
index 3188458ce..b628127af 100644
--- a/src/storage/v3/shard_rsm.cpp
+++ b/src/storage/v3/shard_rsm.cpp
@@ -10,6 +10,8 @@
 // licenses/APL.txt.
 
 #include <algorithm>
+#include <exception>
+#include <experimental/source_location>
 #include <functional>
 #include <iterator>
 #include <optional>
@@ -19,7 +21,6 @@
 
 #include "common/errors.hpp"
 #include "parser/opencypher/parser.hpp"
-#include "pretty_print_ast_to_original_expression.hpp"
 #include "query/v2/requests.hpp"
 #include "storage/v2/vertex.hpp"
 #include "storage/v2/view.hpp"
@@ -330,7 +331,7 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::ScanVerticesRequest &&req) {
     std::vector<Value> expression_results;
     if (!req.filter_expressions.empty()) {
       // NOTE - DbAccessor might get removed in the future.
-      const bool eval = FilterOnVertex(dba, vertex, req.filter_expressions, expr::identifier_node_symbol);
+      const bool eval = FilterOnVertex(dba, vertex, req.filter_expressions);
       if (!eval) {
         return;
       }
@@ -435,7 +436,7 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::ExpandOneRequest &&req) {
     }
     if (!req.filters.empty()) {
       // NOTE - DbAccessor might get removed in the future.
-      const bool eval = FilterOnVertex(dba, src_vertex_acc_opt.value(), req.filters, expr::identifier_node_symbol);
+      const bool eval = FilterOnVertex(dba, src_vertex_acc_opt.value(), req.filters);
       if (!eval) {
         continue;
       }
@@ -515,124 +516,184 @@ msgs::WriteResponses ShardRsm::ApplyWrite(msgs::CommitRequest &&req) {
 };
 
 msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
-  if (req.vertices_and_edges.empty()) {
-    return msgs::GetPropertiesResponse{.result = msgs::GetPropertiesResponse::FAILURE};
+  if (!req.vertex_ids.empty() && !req.vertices_and_edges.empty()) {
+    auto error = CreateErrorResponse(
+        {common::ErrorCode::VERTEX_HAS_EDGES, std::experimental::source_location::current()}, req.transaction_id, "");
+    return msgs::GetPropertiesResponse{.error = {}};
   }
 
   auto shard_acc = shard_->Access(req.transaction_id);
   auto dba = DbAccessor{&shard_acc};
   const auto view = storage::v3::View::NEW;
 
-  auto collect_props = [](const VertexAccessor &acc, const std::vector<PropertyId> &props, View view,
-                          const std::optional<EdgeAccessor> &e_acc) mutable -> std::optional<GetPropElement> {
-    std::vector<TypedValue> properties;
-    std::vector<PropertyId> ids;
-    for (const auto &prop : props) {
-      ShardResult<PropertyValue> result{PropertyValue()};
-      if (e_acc) {
-        result = e_acc->GetProperty(prop, view);
-      } else {
-        result = acc.GetProperty(prop, view);
-      }
-      if (result.HasError() && result.GetError() == common::ErrorCode::NONEXISTENT_OBJECT) {
-        continue;
-      }
-      if (result.HasError()) {
-        spdlog::debug("Encountered an Error while trying to get a vertex property.");
-        return std::nullopt;
-      }
-      properties.push_back(PropertyToTypedValue<TypedValue>(result.GetValue()));
-      ids.push_back(prop);
+  auto transform_props = [](std::map<PropertyId, Value> &&value) {
+    std::vector<std::pair<PropertyId, Value>> result;
+    result.reserve(value.size());
+    for (auto &[id, val] : value) {
+      result.push_back({id, std::move(val)});
     }
-    GetPropElement element{std::move(properties), std::move(ids), acc, e_acc};
-    return {std::move(element)};
+    return result;
   };
 
-  auto find_edge = [](const VertexAccessor &v, const EdgeTypeId &e) -> std::optional<EdgeAccessor> {
-    auto in = v.InEdges(view, {e});
+  auto collect_props = [&req](const VertexAccessor &v_acc, const std::optional<EdgeAccessor> &e_acc) {
+    if (req.property_ids) {
+      if (e_acc) {
+        return CollectAllPropertiesFromAccessor(*e_acc, view);
+      }
+      return CollectAllPropertiesFromAccessor(v_acc, view);
+    }
+
+    if (e_acc) {
+      return CollectSpecificPropertiesFromAccessor(v_acc, *req.property_ids, view);
+    }
+    return CollectSpecificPropertiesFromAccessor(*e_acc, *req.property_ids, view);
+  };
+
+  auto find_edge = [](const VertexAccessor &v, msgs::EdgeId e) -> std::optional<EdgeAccessor> {
+    auto in = v.InEdges(view);
     MG_ASSERT(in.HasValue());
     for (auto &edge : in.GetValue()) {
-      if (edge.EdgeType() == e) {
+      if (edge.Gid().AsUint() == e.gid) {
         return edge;
       }
     }
-
-    auto out = v.OutEdges(view, {e});
+    auto out = v.OutEdges(view);
     MG_ASSERT(out.HasValue());
     for (auto &edge : out.GetValue()) {
-      if (edge.EdgeType() == e) {
+      if (edge.Gid().AsUint() == e.gid) {
         return edge;
       }
     }
     return std::nullopt;
   };
 
-  std::vector<GetPropElement> elements;
-
-  for (const auto &[vertex, maybe_edge] : req.vertices_and_edges) {
-    const auto &[label, pk_v] = vertex;
-    auto pk = ConvertPropertyVector(pk_v);
-    auto v_acc = dba.FindVertex(pk, view);
-    if (!v_acc) {
-      return msgs::GetPropertiesResponse{.result = msgs::GetPropertiesResponse::OUT_OF_SHARD_RANGE};
-    }
-    std::optional<EdgeAccessor> e_acc;
-    if (maybe_edge) {
-      e_acc = find_edge(*v_acc, *maybe_edge);
-      if (!e_acc) {
-        return msgs::GetPropertiesResponse{.result = msgs::GetPropertiesResponse::OUT_OF_SHARD_RANGE};
-      }
-    }
-
-    const auto *symbol = (maybe_edge) ? expr::identifier_edge_symbol : expr::identifier_node_symbol;
-    if (req.filter && !FilterOnVertex(dba, *v_acc, {*req.filter}, symbol, e_acc)) {
-      continue;
-    }
-
-    std::optional<GetPropElement> collected_properties;
-    collected_properties = collect_props(*v_acc, req.property_ids, view, e_acc);
-    if (!collected_properties) {
-      return msgs::GetPropertiesResponse{.result = msgs::GetPropertiesResponse::FAILURE};
-    }
-    if (collected_properties->ids.empty()) {
-      continue;
-    }
-    elements.push_back(std::move(*collected_properties));
-  }
-
-  if (!req.order_by.empty()) {
-    elements = OrderByElements(dba, req.order_by, std::move(elements));
-  }
-
-  std::vector<msgs::GetPropertiesResultRow> results;
-  results.reserve(elements.size());
-
   const auto has_expr_to_evaluate = !req.expressions.empty();
-  size_t limit = elements.size();
-  if (req.limit && *req.limit < elements.size()) {
-    limit = *req.limit;
-  }
-  for (size_t index = 0; index != limit; ++index) {
-    auto &element = elements.at(index);
-    const auto id = element.vertex_acc.Id(view).GetValue();
-    std::optional<EdgeTypeId> e_type =
-        (element.edge_acc) ? std::make_optional(element.edge_acc->EdgeType()) : std::nullopt;
-    msgs::VertexId v_id{msgs::Label{id.primary_label}, ConvertValueVector(id.primary_key)};
-    results.push_back(msgs::GetPropertiesResultRow{
-        .vertex_and_edge = {.vertex = std::move(v_id), .edge = e_type},
-        .properies_and_ids = {
-            .ids = std::move(element.ids),
-            .properties = ConvertToValueVectorFromTypedValueVector(std::move(element.properties_order_by))}});
-    if (has_expr_to_evaluate) {
-      auto expression_results = ConvertToValueVectorFromTypedValueVector(
-          EvaluateVertexExpressions(dba, element.vertex_acc, req.expressions, expr::identifier_node_symbol));
-      results.back().evaluated_expressions = std::move(expression_results);
+  auto emplace_result_row =
+      [dba, transform_props, collect_props, has_expr_to_evaluate, &req](
+          const VertexAccessor &v_acc,
+          const std::optional<EdgeAccessor> e_acc) mutable -> ShardResult<msgs::GetPropertiesResultRow> {
+    auto maybe_id = v_acc.Id(view);
+    if (maybe_id.HasError()) {
+      return {maybe_id.GetError()};
     }
+    const auto &id = maybe_id.GetValue();
+    std::optional<msgs::EdgeId> e_type;
+    if (e_acc) {
+      e_type = msgs::EdgeId{e_acc->Gid().AsUint()};
+    }
+    msgs::VertexId v_id{msgs::Label{id.primary_label}, ConvertValueVector(id.primary_key)};
+    auto maybe_props = collect_props(v_acc, e_acc);
+    if (maybe_props.HasError()) {
+      return {maybe_props.GetError()};
+    }
+    auto props = transform_props(std::move(maybe_props.GetValue()));
+    auto result = msgs::GetPropertiesResultRow{.vertex = std::move(v_id), .edge = e_type, .props = std::move(props)};
+    if (has_expr_to_evaluate) {
+      std::vector<Value> e_results;
+      if (e_acc) {
+        e_results =
+            ConvertToValueVectorFromTypedValueVector(EvaluateEdgeExpressions(dba, v_acc, *e_acc, req.expressions));
+      } else {
+        e_results = ConvertToValueVectorFromTypedValueVector(
+            EvaluateVertexExpressions(dba, v_acc, req.expressions, expr::identifier_node_symbol));
+      }
+      result.evaluated_expressions = std::move(e_results);
+    }
+    return {std::move(result)};
+  };
+
+  auto get_limit = [&req](const auto &elements) {
+    size_t limit = elements.size();
+    if (req.limit && *req.limit < elements.size()) {
+      limit = *req.limit;
+    }
+    return limit;
+  };
+
+  auto collect_response = [get_limit, &req](auto &elements, auto result_row_functor) {
+    msgs::GetPropertiesResponse response;
+    const auto limit = get_limit(elements);
+    for (size_t index = 0; index != limit; ++index) {
+      auto result_row = result_row_functor(elements[index]);
+      if (result_row.HasError()) {
+        return msgs::GetPropertiesResponse{.error = CreateErrorResponse(result_row.GetError(), req.transaction_id, "")};
+      }
+      response.result_row.push_back(std::move(result_row.GetValue()));
+    }
+    return response;
+  };
+
+  std::vector<VertexAccessor> vertices;
+  std::vector<EdgeAccessor> edges;
+
+  auto parse_and_filter = [dba, &vertices](auto &cont, auto projection, auto filter, auto maybe_get_edge) mutable {
+    for (const auto &elem : cont) {
+      const auto &[label, pk_v] = projection(elem);
+      auto pk = ConvertPropertyVector(pk_v);
+      auto v_acc = dba.FindVertex(pk, view);
+      if (!v_acc || filter(*v_acc, maybe_get_edge(elem))) {
+        continue;
+      }
+      vertices.push_back({*v_acc});
+    }
+  };
+  auto identity = [](auto &elem) { return elem; };
+
+  auto filter_vertex = [dba, req](const auto &acc, const auto & /*edge*/) mutable {
+    if (!req.filter) {
+      return false;
+    }
+    return !FilterOnVertex(dba, acc, {*req.filter});
+  };
+
+  auto filter_edge = [dba, &edges, &req, find_edge](const auto &acc, const auto &edge) mutable {
+    auto e_acc = find_edge(acc, edge);
+    if (!req.filter || !e_acc || !FilterOnEdge(dba, acc, *e_acc, {*req.filter})) {
+      return false;
+    }
+    edges.push_back(*e_acc);
+    return true;
+  };
+
+  // Handler logic here
+  if (!req.vertex_ids.empty()) {
+    parse_and_filter(req.vertex_ids, identity, filter_vertex, identity);
+  } else {
+    parse_and_filter(
+        req.vertices_and_edges, [](auto &e) { return e.first; }, filter_edge, [](auto &e) { return e.second; });
   }
 
-  return msgs::GetPropertiesResponse{std::move(results), msgs::GetPropertiesResponse::SUCCESS};
+  if (!req.vertex_ids.empty()) {
+    if (!req.order_by.empty()) {
+      auto elements = OrderByVertices(dba, vertices, req.order_by);
+      return collect_response(elements, [emplace_result_row](auto &element) mutable {
+        return emplace_result_row(element.object_acc, std::nullopt);
+      });
+    }
+    return collect_response(vertices,
+                            [emplace_result_row](auto &acc) mutable { return emplace_result_row(acc, std::nullopt); });
+  }
+  if (!req.order_by.empty()) {
+    auto elements = OrderByEdges(dba, edges, req.order_by, vertices);
+    return collect_response(elements, [emplace_result_row](auto &element) mutable {
+      return emplace_result_row(element.object_acc.first, element.object_acc.second);
+    });
+  }
+
+  struct ZipView {
+    ZipView(std::vector<VertexAccessor> &v, std::vector<EdgeAccessor> &e) : v(v), e(e) {}
+    size_t size() const { return v.size(); }
+    auto operator[](size_t index) { return std::make_pair(v[index], e[index]); }
+
+   private:
+    std::vector<VertexAccessor> &v;
+    std::vector<EdgeAccessor> &e;
+  };
+
+  ZipView vertices_and_edges(vertices, edges);
+  return collect_response(vertices_and_edges, [emplace_result_row](const auto &acc) mutable {
+    return emplace_result_row(acc.first, acc.second);
+  });
 }
 
-// TODO(kostasrim) Handle edges
-
 }  // namespace memgraph::storage::v3
diff --git a/tests/simulation/shard_rsm.cpp b/tests/simulation/shard_rsm.cpp
index 224a84090..80a5cd87d 100644
--- a/tests/simulation/shard_rsm.cpp
+++ b/tests/simulation/shard_rsm.cpp
@@ -482,7 +482,7 @@ std::tuple<size_t, std::optional<msgs::VertexId>> AttemptToScanAllWithExpression
 
 msgs::GetPropertiesResponse AttemptToGetProperties(ShardClient &client, std::vector<PropertyId> properties,
                                                    std::vector<msgs::VertexId> vertices,
-                                                   std::vector<msgs::EdgeTypeId> edges,
+                                                   std::vector<msgs::EdgeId> edges,
                                                    std::optional<size_t> limit = std::nullopt,
                                                    std::optional<uint64_t> filter_prop = std::nullopt,
                                                    bool edge = false,
@@ -508,14 +508,25 @@ msgs::GetPropertiesResponse AttemptToGetProperties(ShardClient &client, std::vec
     req.limit = limit;
   }
   req.expressions = {std::string("5 = 5")};
-  std::vector<msgs::VertexAndEdgeId> req_v;
+  std::vector<msgs::VertexId> req_v;
+  std::vector<msgs::EdgeId> req_e;
   for (auto &v : vertices) {
-    req_v.push_back(msgs::VertexAndEdgeId{.vertex = std::move(v)});
+    req_v.push_back(std::move(v));
   }
-  for (auto index = 0; index != edges.size(); ++index) {
-    req_v[index].edge = edges[index];
+  for (auto &e : edges) {
+    req_e.push_back(std::move(e));
+  }
+
+  if (!edges.empty()) {
+    MG_ASSERT(edges.size() == vertices.size());
+    size_t id = 0;
+    req.vertices_and_edges.reserve(req_v.size());
+    for (auto &v : req_v) {
+      req.vertices_and_edges.push_back({std::move(v), std::move(req_e[id++])});
+    }
+  } else {
+    req.vertex_ids = std::move(req_v);
   }
-  req.vertices_and_edges = std::move(req_v);
 
   while (true) {
     auto read_res = client.SendReadRequest(req);
@@ -1285,93 +1296,86 @@ void TestGetProperties(ShardClient &client) {
   {
     // No properties
     const auto result = AttemptToGetProperties(client, {}, {v_id, v_id_2}, {}, std::nullopt, unique_prop_val_2);
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.empty());
   }
   {
     // All properties
     const auto result = AttemptToGetProperties(client, {prop_id_2, prop_id_4, prop_id_5}, {v_id, v_id_2, v_id_3}, {});
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    MG_ASSERT(!result.error);
     MG_ASSERT(!result.result_row.empty());
     MG_ASSERT(result.result_row.size() == 3);
     for (const auto &elem : result.result_row) {
-      MG_ASSERT(elem.properies_and_ids.ids.size() == 3);
-      MG_ASSERT(elem.properies_and_ids.properties.size() == 3);
+      MG_ASSERT(elem.props.size() == 3);
     }
   }
   {
     // Two properties from two vertices with a filter on unique_prop_5
     const auto result = AttemptToGetProperties(client, {prop_id_2, prop_id_4}, {v_id, v_id_2, v_id_5}, {}, std::nullopt,
                                                unique_prop_val_5);
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
-    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 1);
   }
   {
     // One property from three vertices.
     const auto result = AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {});
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
-    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 3);
-    MG_ASSERT(result.result_row[0].properies_and_ids.ids.size() == 1);
-    MG_ASSERT(result.result_row[1].properies_and_ids.ids.size() == 1);
-    MG_ASSERT(result.result_row[2].properies_and_ids.ids.size() == 1);
+    MG_ASSERT(result.result_row[0].props.size() == 1);
+    MG_ASSERT(result.result_row[1].props.size() == 1);
+    MG_ASSERT(result.result_row[2].props.size() == 1);
   }
   {
     // Same as before but with limit of 1 row
     const auto result =
         AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::make_optional<size_t>(1));
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
-    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 1);
   }
   {
     // Same as before but with a limit greater than the elements returned
     const auto result =
         AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::make_optional<size_t>(5));
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
-    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 3);
   }
   {
     // Order by on `prop1` (descending)
     const auto result = AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::nullopt,
                                                std::nullopt, false, "prop1");
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
-    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 3);
-    MG_ASSERT(result.result_row[0].vertex_and_edge.vertex == v_id_3);
-    MG_ASSERT(result.result_row[1].vertex_and_edge.vertex == v_id_2);
-    MG_ASSERT(result.result_row[2].vertex_and_edge.vertex == v_id);
+    MG_ASSERT(result.result_row[0].vertex == v_id_3);
+    MG_ASSERT(result.result_row[1].vertex == v_id_2);
+    MG_ASSERT(result.result_row[2].vertex == v_id);
   }
   {
     // Order by and filter on >= unique_prop_val_3 && assert result row data members
     const auto result = AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3, v_id_4, v_id_5}, {},
                                                std::nullopt, unique_prop_val_3, false, "prop1");
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
-    MG_ASSERT(!result.result_row.empty());
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 3);
-    MG_ASSERT(result.result_row[0].vertex_and_edge.vertex == v_id_5);
-    MG_ASSERT(result.result_row[0].properies_and_ids.properties.size() == 1);
-    MG_ASSERT(result.result_row[0].properies_and_ids.properties.front() == prim_key_5.front());
-    MG_ASSERT(result.result_row[0].properies_and_ids.ids.size() == 1);
-    MG_ASSERT(result.result_row[0].properies_and_ids.ids.front() == prop_id_2);
+    MG_ASSERT(result.result_row[0].vertex == v_id_5);
+    MG_ASSERT(result.result_row[0].props.size() == 1);
+    MG_ASSERT(result.result_row[0].props.front().second == prim_key_5.front());
+    MG_ASSERT(result.result_row[0].props.size() == 1);
+    MG_ASSERT(result.result_row[0].props.front().first == prop_id_2);
     MG_ASSERT(result.result_row[0].evaluated_expressions.size() == 1);
     MG_ASSERT(result.result_row[0].evaluated_expressions.front() == msgs::Value(true));
 
-    MG_ASSERT(result.result_row[1].vertex_and_edge.vertex == v_id_4);
-    MG_ASSERT(result.result_row[1].properies_and_ids.properties.size() == 1);
-    MG_ASSERT(result.result_row[1].properies_and_ids.properties.front() == prim_key_4.front());
-    MG_ASSERT(result.result_row[1].properies_and_ids.ids.size() == 1);
-    MG_ASSERT(result.result_row[1].properies_and_ids.ids.front() == prop_id_2);
+    MG_ASSERT(result.result_row[1].vertex == v_id_4);
+    MG_ASSERT(result.result_row[1].props.size() == 1);
+    MG_ASSERT(result.result_row[1].props.front().second == prim_key_4.front());
+    MG_ASSERT(result.result_row[1].props.size() == 1);
+    MG_ASSERT(result.result_row[1].props.front().first == prop_id_2);
     MG_ASSERT(result.result_row[1].evaluated_expressions.size() == 1);
     MG_ASSERT(result.result_row[1].evaluated_expressions.front() == msgs::Value(true));
 
-    MG_ASSERT(result.result_row[2].vertex_and_edge.vertex == v_id_3);
-    MG_ASSERT(result.result_row[2].properies_and_ids.properties.size() == 1);
-    MG_ASSERT(result.result_row[2].properies_and_ids.properties.front() == prim_key_3.front());
-    MG_ASSERT(result.result_row[2].properies_and_ids.ids.size() == 1);
-    MG_ASSERT(result.result_row[2].properies_and_ids.ids.front() == prop_id_2);
+    MG_ASSERT(result.result_row[2].vertex == v_id_3);
+    MG_ASSERT(result.result_row[2].props.size() == 1);
+    MG_ASSERT(result.result_row[2].props.front().second == prim_key_3.front());
+    MG_ASSERT(result.result_row[2].props.size() == 1);
+    MG_ASSERT(result.result_row[2].props.front().first == prop_id_2);
     MG_ASSERT(result.result_row[2].evaluated_expressions.size() == 1);
     MG_ASSERT(result.result_row[2].evaluated_expressions.front() == msgs::Value(true));
   }
@@ -1388,54 +1392,49 @@ void TestGetProperties(ShardClient &client) {
   MG_ASSERT(AttemptToAddEdgeWithProperties(client, unique_prop_val_3, unique_prop_val_4, edge_gid_2,
                                            unique_edge_prop_id, edge_prop_val_2, {edge_type_id}));
   const auto edge_prop_id = PropertyId::FromUint(unique_edge_prop_id);
+  std::vector<msgs::EdgeId> edge_ids = {{edge_gid}, {edge_gid_2}};
   // no properties
   {
-    const auto result = AttemptToGetProperties(client, {}, {v_id_2, v_id_3}, {edge_type_id, edge_type_id});
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
+    const auto result = AttemptToGetProperties(client, {}, {v_id_2, v_id_3}, edge_ids);
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.empty());
   }
   // properties for two vertices
   {
-    const auto result = AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, {edge_type_id, edge_type_id});
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
-    MG_ASSERT(!result.result_row.empty());
+    const auto result = AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, edge_ids);
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 2);
   }
   // filter
   {
-    const auto result = AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, {edge_type_id, edge_type_id},
-                                               {}, {edge_prop_val}, true);
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
-    MG_ASSERT(!result.result_row.empty());
+    const auto result =
+        AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, edge_ids, {}, {edge_prop_val}, true);
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 1);
-    MG_ASSERT(result.result_row.front().vertex_and_edge.edge);
-    MG_ASSERT(result.result_row.front().vertex_and_edge.edge.value() == edge_type_id);
-    MG_ASSERT(result.result_row.front().properies_and_ids.properties.size() == 1);
-    MG_ASSERT(result.result_row.front().properies_and_ids.properties.front() ==
-              msgs::Value(static_cast<int64_t>(edge_prop_val)));
+    MG_ASSERT(result.result_row.front().edge);
+    MG_ASSERT(result.result_row.front().edge.value().gid == edge_gid);
+    MG_ASSERT(result.result_row.front().props.size() == 1);
+    MG_ASSERT(result.result_row.front().props.front().second == msgs::Value(static_cast<int64_t>(edge_prop_val)));
   }
   // Order by
   {
-    const auto result = AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, {edge_type_id, edge_type_id},
-                                               {}, {}, true, "e_prop");
-    MG_ASSERT(result.result == msgs::GetPropertiesResponse::SUCCESS);
-    MG_ASSERT(!result.result_row.empty());
+    const auto result =
+        AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, edge_ids, {}, {}, true, "e_prop");
+    MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 2);
-    MG_ASSERT(result.result_row[0].vertex_and_edge.vertex == v_id_3);
-    MG_ASSERT(result.result_row[0].vertex_and_edge.edge);
-    MG_ASSERT(result.result_row[0].vertex_and_edge.edge.value() == edge_type_id);
-    MG_ASSERT(result.result_row[0].properies_and_ids.properties.size() == 1);
-    MG_ASSERT(result.result_row[0].properies_and_ids.properties.front() ==
-              msgs::Value(static_cast<int64_t>(edge_prop_val_2)));
+    MG_ASSERT(result.result_row[0].vertex == v_id_3);
+    MG_ASSERT(result.result_row[0].edge);
+    MG_ASSERT(result.result_row[0].edge.value().gid == edge_gid_2);
+    MG_ASSERT(result.result_row[0].props.size() == 1);
+    MG_ASSERT(result.result_row[0].props.front().second == msgs::Value(static_cast<int64_t>(edge_prop_val_2)));
     MG_ASSERT(result.result_row[0].evaluated_expressions.size() == 1);
     MG_ASSERT(result.result_row[0].evaluated_expressions.front() == msgs::Value(true));
 
-    MG_ASSERT(result.result_row[1].vertex_and_edge.vertex == v_id_2);
-    MG_ASSERT(result.result_row[1].vertex_and_edge.edge);
-    MG_ASSERT(result.result_row[1].vertex_and_edge.edge.value() == edge_type_id);
-    MG_ASSERT(result.result_row[1].properies_and_ids.properties.size() == 1);
-    MG_ASSERT(result.result_row[1].properies_and_ids.properties.front() ==
-              msgs::Value(static_cast<int64_t>(edge_prop_val)));
+    MG_ASSERT(result.result_row[1].vertex == v_id_2);
+    MG_ASSERT(result.result_row[1].edge);
+    MG_ASSERT(result.result_row[1].edge.value().gid == edge_gid);
+    MG_ASSERT(result.result_row[1].props.size() == 1);
+    MG_ASSERT(result.result_row[1].props.front().second == msgs::Value(static_cast<int64_t>(edge_prop_val)));
     MG_ASSERT(result.result_row[1].evaluated_expressions.size() == 1);
     MG_ASSERT(result.result_row[1].evaluated_expressions.front() == msgs::Value(true));
   }

From 94ef57c459eb2d1919bfa015d81137f6007c1f93 Mon Sep 17 00:00:00 2001
From: Kostas Kyrimis <kostaskyrim@gmail.com>
Date: Wed, 30 Nov 2022 17:24:46 +0200
Subject: [PATCH 3/5] Fix small bugs

---
 src/storage/v3/request_helper.cpp |  3 +--
 src/storage/v3/request_helper.hpp | 15 +++++------
 src/storage/v3/shard_rsm.cpp      | 43 +++++++++++++++++++------------
 tests/simulation/shard_rsm.cpp    | 21 ++++++++++-----
 4 files changed, 47 insertions(+), 35 deletions(-)

diff --git a/src/storage/v3/request_helper.cpp b/src/storage/v3/request_helper.cpp
index 260147a25..07ea99d9d 100644
--- a/src/storage/v3/request_helper.cpp
+++ b/src/storage/v3/request_helper.cpp
@@ -14,7 +14,6 @@
 #include <iterator>
 #include <vector>
 
-#include "pretty_print_ast_to_original_expression.hpp"
 #include "storage/v3/bindings/db_accessor.hpp"
 #include "storage/v3/bindings/pretty_print_ast_to_original_expression.hpp"
 #include "storage/v3/expr.hpp"
@@ -245,7 +244,7 @@ ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesFromAccessor(const
 
   auto pks = PrimaryKeysFromAccessor(acc, view, schema);
   if (pks) {
-    ret.GetValue().merge(*pks);
+    ret.GetValue().merge(std::move(*pks));
   }
 
   return ret;
diff --git a/src/storage/v3/request_helper.hpp b/src/storage/v3/request_helper.hpp
index 13871d219..bbe4894e9 100644
--- a/src/storage/v3/request_helper.hpp
+++ b/src/storage/v3/request_helper.hpp
@@ -32,7 +32,7 @@ using EdgeFiller =
 using msgs::Value;
 
 template <typename T>
-concept ObjectAccessor = utils::SameAsAnyOf<T, VertexAccessor, EdgeAccessor, std::pair<VertexAccessor, EdgeAccessor>>;
+concept OrderableObject = utils::SameAsAnyOf<T, VertexAccessor, EdgeAccessor, std::pair<VertexAccessor, EdgeAccessor>>;
 
 inline bool TypedValueCompare(const TypedValue &a, const TypedValue &b) {
   // in ordering null comes after everything else
@@ -126,7 +126,7 @@ class TypedValueVectorCompare final {
   std::vector<Ordering> ordering_;
 };
 
-template <ObjectAccessor TObjectAccessor>
+template <OrderableObject TObjectAccessor>
 struct Element {
   std::vector<TypedValue> properties_order_by;
   TObjectAccessor object_acc;
@@ -164,9 +164,6 @@ std::vector<Element<VertexAccessor>> OrderByVertices(DbAccessor &dba, TIterable
   return ordered;
 }
 
-template <typename T>
-concept EdgeObjectAccessor = utils::SameAsAnyOf<T, EdgeAccessor, std::pair<VertexAccessor, EdgeAccessor>>;
-
 std::vector<Element<EdgeAccessor>> OrderByEdges(DbAccessor &dba, std::vector<EdgeAccessor> &iterable,
                                                 std::vector<msgs::OrderBy> &order_by_edges,
                                                 const VertexAccessor &vertex_acc);
@@ -198,9 +195,9 @@ std::vector<TypedValue> EvaluateEdgeExpressions(DbAccessor &dba, const VertexAcc
                                                 const std::vector<std::string> &expressions);
 
 template <typename T>
-concept TAccessor = utils::SameAsAnyOf<T, VertexAccessor, EdgeAccessor>;
+concept PropertiesAccessor = utils::SameAsAnyOf<T, VertexAccessor, EdgeAccessor>;
 
-template <typename TAccessor>
+template <PropertiesAccessor TAccessor>
 ShardResult<std::map<PropertyId, Value>> CollectSpecificPropertiesFromAccessor(const TAccessor &acc,
                                                                                const std::vector<PropertyId> &props,
                                                                                View view) {
@@ -222,7 +219,7 @@ ShardResult<std::map<PropertyId, Value>> CollectSpecificPropertiesFromAccessor(c
 ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesFromAccessor(const VertexAccessor &acc, View view,
                                                                           const Schemas::Schema &schema);
 namespace impl {
-template <typename TAccessor>
+template <PropertiesAccessor TAccessor>
 ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesImpl(const TAccessor &acc, View view) {
   std::map<PropertyId, Value> ret;
   auto props = acc.Properties(view);
@@ -240,7 +237,7 @@ ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesImpl(const TAccesso
 }
 }  // namespace impl
 
-template <typename TAccessor>
+template <PropertiesAccessor TAccessor>
 ShardResult<std::map<PropertyId, Value>> CollectAllPropertiesFromAccessor(const TAccessor &acc, View view) {
   return impl::CollectAllPropertiesImpl<TAccessor>(acc, view);
 }
diff --git a/src/storage/v3/shard_rsm.cpp b/src/storage/v3/shard_rsm.cpp
index b628127af..4ea874770 100644
--- a/src/storage/v3/shard_rsm.cpp
+++ b/src/storage/v3/shard_rsm.cpp
@@ -517,10 +517,13 @@ msgs::WriteResponses ShardRsm::ApplyWrite(msgs::CommitRequest &&req) {
 
 msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
   if (!req.vertex_ids.empty() && !req.vertices_and_edges.empty()) {
-    auto error = CreateErrorResponse(
-        {common::ErrorCode::VERTEX_HAS_EDGES, std::experimental::source_location::current()}, req.transaction_id, "");
+    auto shard_error = SHARD_ERROR(ErrorCode::NONEXISTENT_OBJECT);
+    auto error = CreateErrorResponse(shard_error, req.transaction_id, "");
     return msgs::GetPropertiesResponse{.error = {}};
   }
+  if (req.property_ids && req.property_ids->empty()) {
+    return {};
+  }
 
   auto shard_acc = shard_->Access(req.transaction_id);
   auto dba = DbAccessor{&shard_acc};
@@ -535,8 +538,9 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
     return result;
   };
 
-  auto collect_props = [&req](const VertexAccessor &v_acc, const std::optional<EdgeAccessor> &e_acc) {
-    if (req.property_ids) {
+  auto collect_props = [&req](const VertexAccessor &v_acc,
+                              const std::optional<EdgeAccessor> &e_acc) -> ShardResult<std::map<PropertyId, Value>> {
+    if (!req.property_ids) {
       if (e_acc) {
         return CollectAllPropertiesFromAccessor(*e_acc, view);
       }
@@ -544,9 +548,9 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
     }
 
     if (e_acc) {
-      return CollectSpecificPropertiesFromAccessor(v_acc, *req.property_ids, view);
+      return CollectSpecificPropertiesFromAccessor(*e_acc, *req.property_ids, view);
     }
-    return CollectSpecificPropertiesFromAccessor(*e_acc, *req.property_ids, view);
+    return CollectSpecificPropertiesFromAccessor(v_acc, *req.property_ids, view);
   };
 
   auto find_edge = [](const VertexAccessor &v, msgs::EdgeId e) -> std::optional<EdgeAccessor> {
@@ -577,9 +581,9 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
       return {maybe_id.GetError()};
     }
     const auto &id = maybe_id.GetValue();
-    std::optional<msgs::EdgeId> e_type;
+    std::optional<msgs::EdgeId> e_id;
     if (e_acc) {
-      e_type = msgs::EdgeId{e_acc->Gid().AsUint()};
+      e_id = msgs::EdgeId{e_acc->Gid().AsUint()};
     }
     msgs::VertexId v_id{msgs::Label{id.primary_label}, ConvertValueVector(id.primary_key)};
     auto maybe_props = collect_props(v_acc, e_acc);
@@ -587,7 +591,7 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
       return {maybe_props.GetError()};
     }
     auto props = transform_props(std::move(maybe_props.GetValue()));
-    auto result = msgs::GetPropertiesResultRow{.vertex = std::move(v_id), .edge = e_type, .props = std::move(props)};
+    auto result = msgs::GetPropertiesResultRow{.vertex = std::move(v_id), .edge = e_id, .props = std::move(props)};
     if (has_expr_to_evaluate) {
       std::vector<Value> e_results;
       if (e_acc) {
@@ -610,11 +614,11 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
     return limit;
   };
 
-  auto collect_response = [get_limit, &req](auto &elements, auto result_row_functor) {
+  auto collect_response = [get_limit, &req](auto &elements, auto create_result_row) {
     msgs::GetPropertiesResponse response;
     const auto limit = get_limit(elements);
     for (size_t index = 0; index != limit; ++index) {
-      auto result_row = result_row_functor(elements[index]);
+      auto result_row = create_result_row(elements[index]);
       if (result_row.HasError()) {
         return msgs::GetPropertiesResponse{.error = CreateErrorResponse(result_row.GetError(), req.transaction_id, "")};
       }
@@ -626,15 +630,15 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
   std::vector<VertexAccessor> vertices;
   std::vector<EdgeAccessor> edges;
 
-  auto parse_and_filter = [dba, &vertices](auto &cont, auto projection, auto filter, auto maybe_get_edge) mutable {
-    for (const auto &elem : cont) {
+  auto parse_and_filter = [dba, &vertices](auto &container, auto projection, auto filter, auto maybe_get_edge) mutable {
+    for (const auto &elem : container) {
       const auto &[label, pk_v] = projection(elem);
       auto pk = ConvertPropertyVector(pk_v);
       auto v_acc = dba.FindVertex(pk, view);
       if (!v_acc || filter(*v_acc, maybe_get_edge(elem))) {
         continue;
       }
-      vertices.push_back({*v_acc});
+      vertices.push_back(*v_acc);
     }
   };
   auto identity = [](auto &elem) { return elem; };
@@ -648,11 +652,15 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
 
   auto filter_edge = [dba, &edges, &req, find_edge](const auto &acc, const auto &edge) mutable {
     auto e_acc = find_edge(acc, edge);
-    if (!req.filter || !e_acc || !FilterOnEdge(dba, acc, *e_acc, {*req.filter})) {
-      return false;
+    if (!e_acc) {
+      return true;
+    }
+
+    if (req.filter && !FilterOnEdge(dba, acc, *e_acc, {*req.filter})) {
+      return true;
     }
     edges.push_back(*e_acc);
-    return true;
+    return false;
   };
 
   // Handler logic here
@@ -673,6 +681,7 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
     return collect_response(vertices,
                             [emplace_result_row](auto &acc) mutable { return emplace_result_row(acc, std::nullopt); });
   }
+
   if (!req.order_by.empty()) {
     auto elements = OrderByEdges(dba, edges, req.order_by, vertices);
     return collect_response(elements, [emplace_result_row](auto &element) mutable {
diff --git a/tests/simulation/shard_rsm.cpp b/tests/simulation/shard_rsm.cpp
index 80a5cd87d..ba36157d0 100644
--- a/tests/simulation/shard_rsm.cpp
+++ b/tests/simulation/shard_rsm.cpp
@@ -489,7 +489,9 @@ msgs::GetPropertiesResponse AttemptToGetProperties(ShardClient &client, std::vec
                                                    std::optional<std::string> order_by = std::nullopt) {
   msgs::GetPropertiesRequest req{};
   req.transaction_id.logical_id = GetTransactionId();
-  req.property_ids = std::move(properties);
+  if (!properties.empty()) {
+    req.property_ids = std::move(properties);
+  }
 
   if (filter_prop) {
     std::string filter_expr = (!edge) ? "MG_SYMBOL_NODE.prop1 >= " : "MG_SYMBOL_EDGE.e_prop = ";
@@ -1294,13 +1296,15 @@ void TestGetProperties(ShardClient &client) {
   const auto prop_id_5 = PropertyId::FromUint(5);
   // Vertices
   {
-    // No properties
-    const auto result = AttemptToGetProperties(client, {}, {v_id, v_id_2}, {}, std::nullopt, unique_prop_val_2);
+    const auto result = AttemptToGetProperties(client, {}, {v_id, v_id_2}, {});
     MG_ASSERT(!result.error);
-    MG_ASSERT(result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 2);
+    for (const auto &elem : result.result_row) {
+      MG_ASSERT(elem.props.size() == 3);
+    }
   }
   {
-    // All properties
+    // Specific properties
     const auto result = AttemptToGetProperties(client, {prop_id_2, prop_id_4, prop_id_5}, {v_id, v_id_2, v_id_3}, {});
     MG_ASSERT(!result.error);
     MG_ASSERT(!result.result_row.empty());
@@ -1393,11 +1397,14 @@ void TestGetProperties(ShardClient &client) {
                                            unique_edge_prop_id, edge_prop_val_2, {edge_type_id}));
   const auto edge_prop_id = PropertyId::FromUint(unique_edge_prop_id);
   std::vector<msgs::EdgeId> edge_ids = {{edge_gid}, {edge_gid_2}};
-  // no properties
+  // all properties
   {
     const auto result = AttemptToGetProperties(client, {}, {v_id_2, v_id_3}, edge_ids);
     MG_ASSERT(!result.error);
-    MG_ASSERT(result.result_row.empty());
+    MG_ASSERT(result.result_row.size() == 2);
+    for (const auto &elem : result.result_row) {
+      MG_ASSERT(elem.props.size() == 1);
+    }
   }
   // properties for two vertices
   {

From 8af635c8d70efce8482a5d8bd0c8174d76270b6f Mon Sep 17 00:00:00 2001
From: Kostas Kyrimis <kostaskyrim@gmail.com>
Date: Wed, 30 Nov 2022 17:44:37 +0200
Subject: [PATCH 4/5] Fix clang-tidy warnings

---
 src/storage/v3/shard_rsm.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/storage/v3/shard_rsm.cpp b/src/storage/v3/shard_rsm.cpp
index 4ea874770..e533008a1 100644
--- a/src/storage/v3/shard_rsm.cpp
+++ b/src/storage/v3/shard_rsm.cpp
@@ -533,7 +533,7 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
     std::vector<std::pair<PropertyId, Value>> result;
     result.reserve(value.size());
     for (auto &[id, val] : value) {
-      result.push_back({id, std::move(val)});
+      result.emplace_back(std::make_pair(id, std::move(val)));
     }
     return result;
   };

From f8cbaaf362237da45683dcd5b35194ef63538047 Mon Sep 17 00:00:00 2001
From: Kostas Kyrimis <kostaskyrim@gmail.com>
Date: Thu, 1 Dec 2022 14:41:21 +0200
Subject: [PATCH 5/5] Allow requests with zero properties

---
 src/storage/v3/shard_rsm.cpp   |  3 --
 tests/simulation/shard_rsm.cpp | 77 ++++++++++++++++++++--------------
 2 files changed, 46 insertions(+), 34 deletions(-)

diff --git a/src/storage/v3/shard_rsm.cpp b/src/storage/v3/shard_rsm.cpp
index e533008a1..639ffd6d8 100644
--- a/src/storage/v3/shard_rsm.cpp
+++ b/src/storage/v3/shard_rsm.cpp
@@ -521,9 +521,6 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::GetPropertiesRequest &&req) {
     auto error = CreateErrorResponse(shard_error, req.transaction_id, "");
     return msgs::GetPropertiesResponse{.error = {}};
   }
-  if (req.property_ids && req.property_ids->empty()) {
-    return {};
-  }
 
   auto shard_acc = shard_->Access(req.transaction_id);
   auto dba = DbAccessor{&shard_acc};
diff --git a/tests/simulation/shard_rsm.cpp b/tests/simulation/shard_rsm.cpp
index ba36157d0..768217945 100644
--- a/tests/simulation/shard_rsm.cpp
+++ b/tests/simulation/shard_rsm.cpp
@@ -480,18 +480,14 @@ std::tuple<size_t, std::optional<msgs::VertexId>> AttemptToScanAllWithExpression
   }
 }
 
-msgs::GetPropertiesResponse AttemptToGetProperties(ShardClient &client, std::vector<PropertyId> properties,
-                                                   std::vector<msgs::VertexId> vertices,
-                                                   std::vector<msgs::EdgeId> edges,
-                                                   std::optional<size_t> limit = std::nullopt,
-                                                   std::optional<uint64_t> filter_prop = std::nullopt,
-                                                   bool edge = false,
-                                                   std::optional<std::string> order_by = std::nullopt) {
+msgs::GetPropertiesResponse AttemptToGetProperties(
+    ShardClient &client, std::optional<std::vector<PropertyId>> properties, std::vector<msgs::VertexId> vertices,
+    std::vector<msgs::EdgeId> edges, std::optional<size_t> limit = std::nullopt,
+    std::optional<uint64_t> filter_prop = std::nullopt, bool edge = false,
+    std::optional<std::string> order_by = std::nullopt) {
   msgs::GetPropertiesRequest req{};
   req.transaction_id.logical_id = GetTransactionId();
-  if (!properties.empty()) {
-    req.property_ids = std::move(properties);
-  }
+  req.property_ids = std::move(properties);
 
   if (filter_prop) {
     std::string filter_expr = (!edge) ? "MG_SYMBOL_NODE.prop1 >= " : "MG_SYMBOL_EDGE.e_prop = ";
@@ -1294,9 +1290,18 @@ void TestGetProperties(ShardClient &client) {
   const auto prop_id_2 = PropertyId::FromUint(2);
   const auto prop_id_4 = PropertyId::FromUint(4);
   const auto prop_id_5 = PropertyId::FromUint(5);
-  // Vertices
+  // No properties
   {
-    const auto result = AttemptToGetProperties(client, {}, {v_id, v_id_2}, {});
+    const auto result = AttemptToGetProperties(client, {{}}, {v_id, v_id_2}, {});
+    MG_ASSERT(!result.error);
+    MG_ASSERT(result.result_row.size() == 2);
+    for (const auto &elem : result.result_row) {
+      MG_ASSERT(elem.props.size() == 0);
+    }
+  }
+  // All properties
+  {
+    const auto result = AttemptToGetProperties(client, std::nullopt, {v_id, v_id_2}, {});
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 2);
     for (const auto &elem : result.result_row) {
@@ -1305,7 +1310,8 @@ void TestGetProperties(ShardClient &client) {
   }
   {
     // Specific properties
-    const auto result = AttemptToGetProperties(client, {prop_id_2, prop_id_4, prop_id_5}, {v_id, v_id_2, v_id_3}, {});
+    const auto result =
+        AttemptToGetProperties(client, std::vector{prop_id_2, prop_id_4, prop_id_5}, {v_id, v_id_2, v_id_3}, {});
     MG_ASSERT(!result.error);
     MG_ASSERT(!result.result_row.empty());
     MG_ASSERT(result.result_row.size() == 3);
@@ -1315,14 +1321,14 @@ void TestGetProperties(ShardClient &client) {
   }
   {
     // Two properties from two vertices with a filter on unique_prop_5
-    const auto result = AttemptToGetProperties(client, {prop_id_2, prop_id_4}, {v_id, v_id_2, v_id_5}, {}, std::nullopt,
-                                               unique_prop_val_5);
+    const auto result = AttemptToGetProperties(client, std::vector{prop_id_2, prop_id_4}, {v_id, v_id_2, v_id_5}, {},
+                                               std::nullopt, unique_prop_val_5);
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 1);
   }
   {
     // One property from three vertices.
-    const auto result = AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {});
+    const auto result = AttemptToGetProperties(client, std::vector{prop_id_2}, {v_id, v_id_2, v_id_3}, {});
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 3);
     MG_ASSERT(result.result_row[0].props.size() == 1);
@@ -1331,21 +1337,21 @@ void TestGetProperties(ShardClient &client) {
   }
   {
     // Same as before but with limit of 1 row
-    const auto result =
-        AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::make_optional<size_t>(1));
+    const auto result = AttemptToGetProperties(client, std::vector{prop_id_2}, {v_id, v_id_2, v_id_3}, {},
+                                               std::make_optional<size_t>(1));
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 1);
   }
   {
     // Same as before but with a limit greater than the elements returned
-    const auto result =
-        AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::make_optional<size_t>(5));
+    const auto result = AttemptToGetProperties(client, std::vector{prop_id_2}, std::vector{v_id, v_id_2, v_id_3}, {},
+                                               std::make_optional<size_t>(5));
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 3);
   }
   {
     // Order by on `prop1` (descending)
-    const auto result = AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::nullopt,
+    const auto result = AttemptToGetProperties(client, std::vector{prop_id_2}, {v_id, v_id_2, v_id_3}, {}, std::nullopt,
                                                std::nullopt, false, "prop1");
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 3);
@@ -1355,8 +1361,8 @@ void TestGetProperties(ShardClient &client) {
   }
   {
     // Order by and filter on >= unique_prop_val_3 && assert result row data members
-    const auto result = AttemptToGetProperties(client, {prop_id_2}, {v_id, v_id_2, v_id_3, v_id_4, v_id_5}, {},
-                                               std::nullopt, unique_prop_val_3, false, "prop1");
+    const auto result = AttemptToGetProperties(client, std::vector{prop_id_2}, {v_id, v_id_2, v_id_3, v_id_4, v_id_5},
+                                               {}, std::nullopt, unique_prop_val_3, false, "prop1");
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 3);
     MG_ASSERT(result.result_row[0].vertex == v_id_5);
@@ -1397,25 +1403,34 @@ void TestGetProperties(ShardClient &client) {
                                            unique_edge_prop_id, edge_prop_val_2, {edge_type_id}));
   const auto edge_prop_id = PropertyId::FromUint(unique_edge_prop_id);
   std::vector<msgs::EdgeId> edge_ids = {{edge_gid}, {edge_gid_2}};
-  // all properties
+  // No properties
   {
-    const auto result = AttemptToGetProperties(client, {}, {v_id_2, v_id_3}, edge_ids);
+    const auto result = AttemptToGetProperties(client, {{}}, {v_id_2, v_id_3}, edge_ids);
+    MG_ASSERT(!result.error);
+    MG_ASSERT(result.result_row.size() == 2);
+    for (const auto &elem : result.result_row) {
+      MG_ASSERT(elem.props.size() == 0);
+    }
+  }
+  // All properties
+  {
+    const auto result = AttemptToGetProperties(client, std::nullopt, {v_id_2, v_id_3}, edge_ids);
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 2);
     for (const auto &elem : result.result_row) {
       MG_ASSERT(elem.props.size() == 1);
     }
   }
-  // properties for two vertices
+  // Properties for two vertices
   {
-    const auto result = AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, edge_ids);
+    const auto result = AttemptToGetProperties(client, std::vector{edge_prop_id}, {v_id_2, v_id_3}, edge_ids);
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 2);
   }
-  // filter
+  // Filter
   {
-    const auto result =
-        AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, edge_ids, {}, {edge_prop_val}, true);
+    const auto result = AttemptToGetProperties(client, std::vector{edge_prop_id}, {v_id_2, v_id_3}, edge_ids, {},
+                                               {edge_prop_val}, true);
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 1);
     MG_ASSERT(result.result_row.front().edge);
@@ -1426,7 +1441,7 @@ void TestGetProperties(ShardClient &client) {
   // Order by
   {
     const auto result =
-        AttemptToGetProperties(client, {edge_prop_id}, {v_id_2, v_id_3}, edge_ids, {}, {}, true, "e_prop");
+        AttemptToGetProperties(client, std::vector{edge_prop_id}, {v_id_2, v_id_3}, edge_ids, {}, {}, true, "e_prop");
     MG_ASSERT(!result.error);
     MG_ASSERT(result.result_row.size() == 2);
     MG_ASSERT(result.result_row[0].vertex == v_id_3);