diff --git a/src/query/context.hpp b/src/query/context.hpp
index 3040d6e10..8dfe09ecc 100644
--- a/src/query/context.hpp
+++ b/src/query/context.hpp
@@ -51,6 +51,8 @@ struct EvaluationContext {
   /// All counters generated by `counter` function, mutable because the function
   /// modifies the values
   mutable std::unordered_map<std::string, int64_t> counters{};
+  /// Property lookup cache ({symbol: {property_id: property_value, ...}, ...})
+  mutable std::unordered_map<int32_t, std::map<storage::PropertyId, storage::PropertyValue>> property_lookups_cache{};
 };
 
 inline std::vector<storage::PropertyId> NamesToProperties(const std::vector<std::string> &property_names,
diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp
index 36c88dad1..201f4e230 100644
--- a/src/query/frontend/ast/ast.hpp
+++ b/src/query/frontend/ast/ast.hpp
@@ -1186,6 +1186,8 @@ class PropertyLookup : public memgraph::query::Expression {
   static const utils::TypeInfo kType;
   const utils::TypeInfo &GetTypeInfo() const override { return kType; }
 
+  enum class EvaluationMode { GET_OWN_PROPERTY, GET_ALL_PROPERTIES };
+
   PropertyLookup() = default;
 
   DEFVISITABLE(ExpressionVisitor<TypedValue>);
@@ -1200,11 +1202,13 @@ class PropertyLookup : public memgraph::query::Expression {
 
   memgraph::query::Expression *expression_{nullptr};
   memgraph::query::PropertyIx property_;
+  memgraph::query::PropertyLookup::EvaluationMode evaluation_mode_{EvaluationMode::GET_OWN_PROPERTY};
 
   PropertyLookup *Clone(AstStorage *storage) const override {
     PropertyLookup *object = storage->Create<PropertyLookup>();
     object->expression_ = expression_ ? expression_->Clone(storage) : nullptr;
     object->property_ = storage->GetPropertyIx(property_.name);
+    object->evaluation_mode_ = evaluation_mode_;
     return object;
   }
 
diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp
index cde169675..709df16c0 100644
--- a/src/query/frontend/semantic/symbol_generator.cpp
+++ b/src/query/frontend/semantic/symbol_generator.cpp
@@ -400,6 +400,29 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) {
   return true;
 }
 
+bool SymbolGenerator::PostVisit(MapLiteral &map_literal) {
+  std::unordered_map<int32_t, PropertyLookup *> property_lookups{};
+
+  for (const auto &pair : map_literal.elements_) {
+    if (pair.second->GetTypeInfo() != PropertyLookup::kType) continue;
+    auto *property_lookup = static_cast<PropertyLookup *>(pair.second);
+    if (property_lookup->expression_->GetTypeInfo() != Identifier::kType) continue;
+
+    auto symbol_pos = static_cast<Identifier *>(property_lookup->expression_)->symbol_pos_;
+    try {
+      auto *existing_property_lookup = property_lookups.at(symbol_pos);
+      // If already there (no exception), update the original and current PropertyLookups
+      existing_property_lookup->evaluation_mode_ = PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES;
+      property_lookup->evaluation_mode_ = PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES;
+    } catch (const std::out_of_range &) {
+      // Otherwise, add the PropertyLookup to the map
+      property_lookups.emplace(symbol_pos, property_lookup);
+    }
+  }
+
+  return true;
+}
+
 bool SymbolGenerator::PreVisit(Aggregation &aggr) {
   auto &scope = scopes_.back();
   // Check if the aggregation can be used in this context. This check should
diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp
index 5b98958c6..b2f7a928c 100644
--- a/src/query/frontend/semantic/symbol_generator.hpp
+++ b/src/query/frontend/semantic/symbol_generator.hpp
@@ -72,6 +72,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
   // Expressions
   ReturnType Visit(Identifier &) override;
   ReturnType Visit(PrimitiveLiteral &) override { return true; }
+  bool PreVisit(MapLiteral &) override { return true; }
+  bool PostVisit(MapLiteral &) override;
   ReturnType Visit(ParameterLookup &) override { return true; }
   bool PreVisit(Aggregation &) override;
   bool PostVisit(Aggregation &) override;
diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp
index 39e6a7c5f..47a7cb598 100644
--- a/src/query/interpret/eval.hpp
+++ b/src/query/interpret/eval.hpp
@@ -546,9 +546,35 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
       case TypedValue::Type::Null:
         return TypedValue(ctx_->memory);
       case TypedValue::Type::Vertex:
-        return TypedValue(GetProperty(expression_result_ptr->ValueVertex(), property_lookup.property_), ctx_->memory);
+        if (property_lookup.evaluation_mode_ == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES) {
+          auto symbol_pos = static_cast<Identifier *>(property_lookup.expression_)->symbol_pos_;
+          if (!ctx_->property_lookups_cache.contains(symbol_pos)) {
+            ctx_->property_lookups_cache.emplace(symbol_pos, GetAllProperties(expression_result_ptr->ValueVertex()));
+          }
+
+          auto property_id = ctx_->properties[property_lookup.property_.ix];
+          if (ctx_->property_lookups_cache[symbol_pos].contains(property_id)) {
+            return TypedValue(ctx_->property_lookups_cache[symbol_pos][property_id], ctx_->memory);
+          }
+          return TypedValue(ctx_->memory);
+        } else {
+          return TypedValue(GetProperty(expression_result_ptr->ValueVertex(), property_lookup.property_), ctx_->memory);
+        }
       case TypedValue::Type::Edge:
-        return TypedValue(GetProperty(expression_result_ptr->ValueEdge(), property_lookup.property_), ctx_->memory);
+        if (property_lookup.evaluation_mode_ == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES) {
+          auto symbol_pos = static_cast<Identifier *>(property_lookup.expression_)->symbol_pos_;
+          if (!ctx_->property_lookups_cache.contains(symbol_pos)) {
+            ctx_->property_lookups_cache.emplace(symbol_pos, GetAllProperties(expression_result_ptr->ValueEdge()));
+          }
+
+          auto property_id = ctx_->properties[property_lookup.property_.ix];
+          if (ctx_->property_lookups_cache[symbol_pos].contains(property_id)) {
+            return TypedValue(ctx_->property_lookups_cache[symbol_pos][property_id], ctx_->memory);
+          }
+          return TypedValue(ctx_->memory);
+        } else {
+          return TypedValue(GetProperty(expression_result_ptr->ValueEdge(), property_lookup.property_), ctx_->memory);
+        }
       case TypedValue::Type::Map: {
         auto &map = expression_result_ptr->ValueMap();
         auto found = map.find(property_lookup.property_.name.c_str());
@@ -754,7 +780,14 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
 
   TypedValue Visit(MapLiteral &literal) override {
     TypedValue::TMap result(ctx_->memory);
-    for (const auto &pair : literal.elements_) result.emplace(pair.first.name, pair.second->Accept(*this));
+    for (const auto &pair : literal.elements_) {
+      result.emplace(pair.first.name, pair.second->Accept(*this));
+    }
+
+    ctx_->property_lookups_cache.clear();
+    // TODO Don’t clear the cache if there are remaining MapLiterals with PropertyLookups that read the same properties
+    // from the same variable (symbol & value)
+
     return TypedValue(result, ctx_->memory);
   }
 
@@ -1048,6 +1081,33 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
   }
 
  private:
+  template <class TRecordAccessor>
+  std::map<storage::PropertyId, storage::PropertyValue> GetAllProperties(const TRecordAccessor &record_accessor) {
+    auto maybe_props = record_accessor.Properties(view_);
+    if (maybe_props.HasError() && maybe_props.GetError() == storage::Error::NONEXISTENT_OBJECT) {
+      // This is a very nasty and temporary hack in order to make MERGE work.
+      // The old storage had the following logic when returning an `OLD` view:
+      // `return old ? old : new`. That means that if the `OLD` view didn't
+      // exist, it returned the NEW view. With this hack we simulate that
+      // behavior.
+      // TODO (mferencevic, teon.banek): Remove once MERGE is reimplemented.
+      maybe_props = record_accessor.Properties(storage::View::NEW);
+    }
+    if (maybe_props.HasError()) {
+      switch (maybe_props.GetError()) {
+        case storage::Error::DELETED_OBJECT:
+          throw QueryRuntimeException("Trying to get properties from a deleted object.");
+        case storage::Error::NONEXISTENT_OBJECT:
+          throw query::QueryRuntimeException("Trying to get properties from an object that doesn't exist.");
+        case storage::Error::SERIALIZATION_ERROR:
+        case storage::Error::VERTEX_HAS_EDGES:
+        case storage::Error::PROPERTIES_DISABLED:
+          throw QueryRuntimeException("Unexpected error when getting properties.");
+      }
+    }
+    return *maybe_props;
+  }
+
   template <class TRecordAccessor>
   storage::PropertyValue GetProperty(const TRecordAccessor &record_accessor, PropertyIx prop) {
     auto maybe_prop = record_accessor.GetProperty(view_, ctx_->properties[prop.ix]);
diff --git a/tests/gql_behave/tests/memgraph_V1/features/map.feature b/tests/gql_behave/tests/memgraph_V1/features/map.feature
new file mode 100644
index 000000000..2b4327ce6
--- /dev/null
+++ b/tests/gql_behave/tests/memgraph_V1/features/map.feature
@@ -0,0 +1,167 @@
+Feature: Creating map values
+
+  Scenario: Creating a map with multiple properties from a vertex
+    Given an empty graph
+    And having executed
+      """
+      CREATE (:Employee {name: "Andy", surname: "Walker", age: 24, id: 1234});
+      """
+    When executing query:
+      """
+      MATCH (e:Employee) RETURN {name: e.name, surname: e.surname, age: e.age} AS public_data;
+      """
+    Then the result should be:
+      | public_data                                |
+      | {age: 24, name: 'Andy', surname: 'Walker'} |
+
+  Scenario: Creating instances of a map with multiple properties from a vertex
+    Given an empty graph
+    And having executed
+      """
+      CREATE (:Employee {name: "Andy", surname: "Walker", age: 24, id: 1234}), (:Person), (:Person);
+      """
+    When executing query:
+      """
+      MATCH (e:Employee), (n:Person) RETURN {name: e.name, surname: e.surname, age: e.age} AS public_data;
+      """
+    Then the result should be:
+      | public_data                                |
+      | {age: 24, name: 'Andy', surname: 'Walker'} |
+      | {age: 24, name: 'Andy', surname: 'Walker'} |
+
+  Scenario: Creating a map with multiple properties from each vertex
+    Given an empty graph
+    And having executed
+      """
+      CREATE (:Cat {name: "Luigi", age: 11}), (:Dog {name: "Don", age: 10}), (:Owner {name: "Ivan"});
+      """
+    When executing query:
+      """
+      MATCH (m:Cat), (n:Dog), (o:Owner) SET o += {catName: m.name, catAge: m.age, dogName: n.name, dogAge: n.age} RETURN o;
+      """
+    Then the result should be:
+      | o                                                                                 |
+      | (:Owner {catAge: 11, catName: 'Luigi', dogAge: 10, dogName: 'Don', name: 'Ivan'}) |
+
+  Scenario: Creating distinct maps with multiple properties, each from one vertex
+    Given an empty graph
+    And having executed
+      """
+      FOREACH (i in range(1, 5) | CREATE (:Node {prop1: i, prop2: 2 * i}));
+      """
+    When executing query:
+      """
+      MATCH (n) RETURN {prop1: n.prop1, prop2: n.prop2} AS prop_data;
+      """
+    Then the result should be:
+      | prop_data             |
+      | {prop1: 1, prop2: 2}  |
+      | {prop1: 2, prop2: 4}  |
+      | {prop1: 3, prop2: 6}  |
+      | {prop1: 4, prop2: 8}  |
+      | {prop1: 5, prop2: 10} |
+
+  Scenario: Creating a map with multiple properties from a vertex; one property is null
+    Given an empty graph
+    And having executed
+      """
+      CREATE (:Employee {name: "Andy", surname: "Walker", age: 24, id: 1234});
+      """
+    When executing query:
+      """
+      MATCH (e:Employee) RETURN {name: e.name, surname: e.surname, age: e.age, null_prop: e.nonexistent} AS public_data;
+      """
+    Then the result should be:
+      | public_data                                                 |
+      | {age: 24, name: 'Andy', null_prop: null, surname: 'Walker'} |
+
+  Scenario: Creating a map with multiple properties from an edge
+    Given an empty graph
+    And having executed
+      """
+      CREATE (m)-[:ROUTE {km: 466, cross_border: true}]->(n);
+      """
+    When executing query:
+      """
+      MATCH ()-[r:ROUTE]->() RETURN {km: r.km, cross_border: r.cross_border} AS route_data;
+      """
+    Then the result should be:
+      | route_data                    |
+      | {cross_border: true, km: 466} |
+
+  Scenario: Creating instances of a map with multiple properties from an edge
+    Given an empty graph
+    And having executed
+      """
+      CREATE (m)-[:ROUTE {km: 466, cross_border: true}]->(n), (:City), (:City);
+      """
+    When executing query:
+      """
+      MATCH (:City), ()-[r:ROUTE]->() RETURN {km: r.km, cross_border: r.cross_border} AS route_data;
+      """
+    Then the result should be:
+      | route_data                    |
+      | {cross_border: true, km: 466} |
+      | {cross_border: true, km: 466} |
+
+  Scenario: Creating a map with multiple properties from each edge
+    Given an empty graph
+    And having executed
+      """
+      CREATE (m)-[:HIGHWAY {km: 466, cross_border: true}]->(n), (m)-[:FLIGHT {km: 350, daily: true}]->(n);
+      """
+    When executing query:
+      """
+      MATCH ()-[h:HIGHWAY]->(), ()-[f:FLIGHT]->()
+      RETURN {km_hwy: h.km, cross_border: h.cross_border, km_air: f.km, daily_flight: f.daily} AS routes_data;
+      """
+    Then the result should be:
+      | routes_data                                                        |
+      | {cross_border: true, daily_flight: true, km_air: 350, km_hwy: 466} |
+
+  Scenario: Creating distinct maps with multiple properties, each from one edge
+    Given an empty graph
+    And having executed
+      """
+      MERGE (m:City) MERGE (n:Country) FOREACH (i in range(1, 5) | CREATE (m)-[:IN {prop1: i, prop2: 2 * i}]->(n));
+      """
+    When executing query:
+      """
+      MATCH (m)-[r]->(n) RETURN {prop1: r.prop1, prop2: r.prop2} AS prop_data;
+      """
+    Then the result should be:
+      | prop_data             |
+      | {prop1: 1, prop2: 2}  |
+      | {prop1: 2, prop2: 4}  |
+      | {prop1: 3, prop2: 6}  |
+      | {prop1: 4, prop2: 8}  |
+      | {prop1: 5, prop2: 10} |
+
+  Scenario: Creating a map with multiple properties from an edge; one property is null
+    Given an empty graph
+    And having executed
+      """
+      CREATE (m)-[:ROUTE {km: 466, cross_border: true}]->(n);
+      """
+    When executing query:
+      """
+      MATCH ()-[r:ROUTE]->() RETURN {km: r.km, cross_border: r.cross_border, null_prop: r.nonexistent} AS route_data;
+      """
+    Then the result should be:
+      | route_data                                     |
+      | {cross_border: true, km: 466, null_prop: null} |
+
+  Scenario: Creating a map with multiple properties from both a vertex and an edge
+    Given an empty graph
+    And having executed
+      """
+      CREATE (m:City {name: "Split", highway_connected: true})-[:ROUTE {km: 466, cross_border: true}]->(n:City {name: "Ljubljana"});
+      """
+    When executing query:
+      """
+      MATCH (m:City {name: "Split"})-[r:ROUTE]->()
+      RETURN {km: r.km, cross_border: r.cross_border, start_city: m.name, highway_connected: m.highway_connected} AS route_data;
+      """
+    Then the result should be:
+      | route_data                                                                  |
+      | {cross_border: true, highway_connected: true, km: 466, start_city: 'Split'} |
diff --git a/tests/unit/query_plan_create_set_remove_delete.cpp b/tests/unit/query_plan_create_set_remove_delete.cpp
index e3873b20a..cafb711db 100644
--- a/tests/unit/query_plan_create_set_remove_delete.cpp
+++ b/tests/unit/query_plan_create_set_remove_delete.cpp
@@ -1653,6 +1653,21 @@ TYPED_TEST(QueryPlanTest, MergeNoInput) {
   EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
 }
 
+TYPED_TEST(QueryPlanTest, SetPropertyWithCaching) {
+  // SET (Null).prop = 42
+  auto storage_dba = this->db->Access();
+  memgraph::query::DbAccessor dba(storage_dba.get());
+  SymbolTable symbol_table;
+  auto prop = PROPERTY_PAIR(dba, "property");
+  auto null = LITERAL(TypedValue());
+  auto literal = LITERAL(42);
+  auto n_prop = PROPERTY_LOOKUP(dba, null, prop);
+  auto once = std::make_shared<Once>();
+  auto set_op = std::make_shared<plan::SetProperty>(once, prop.second, n_prop, literal);
+  auto context = MakeContext(this->storage, symbol_table, &dba);
+  EXPECT_EQ(1, PullAll(*set_op, &context));
+}
+
 TYPED_TEST(QueryPlanTest, SetPropertyOnNull) {
   // SET (Null).prop = 42
   auto storage_dba = this->db->Access();
@@ -1717,6 +1732,46 @@ TYPED_TEST(QueryPlanTest, UpdateSetPropertiesFromMap) {
   EXPECT_EQ(*new_properties, expected_properties);
 }
 
+TYPED_TEST(QueryPlanTest, SetPropertiesFromMapWithCaching) {
+  auto storage_dba = this->db->Access();
+  memgraph::query::DbAccessor dba(storage_dba.get());
+
+  // Add a single vertex. ({prop1: 43, prop2: 44})
+  auto vertex_accessor = dba.InsertVertex();
+  auto old_value = vertex_accessor.SetProperty(dba.NameToProperty("prop1"), memgraph::storage::PropertyValue{43});
+  old_value = vertex_accessor.SetProperty(dba.NameToProperty("prop2"), memgraph::storage::PropertyValue{44});
+  EXPECT_EQ(old_value.HasError(), false);
+  EXPECT_EQ(*old_value, memgraph::storage::PropertyValue());
+  dba.AdvanceCommand();
+  EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
+
+  SymbolTable symbol_table;
+  // MATCH (n) SET n += {new_prop1: n.prop1, new_prop2: n.prop2};
+  auto n = MakeScanAll(this->storage, symbol_table, "n");
+  auto prop_new_prop1 = PROPERTY_PAIR(dba, "new_prop1");
+  auto prop_new_prop2 = PROPERTY_PAIR(dba, "new_prop2");
+  std::unordered_map<PropertyIx, Expression *> prop_map;
+  prop_map.emplace(this->storage.GetPropertyIx(prop_new_prop1.first), LITERAL(43));
+  prop_map.emplace(this->storage.GetPropertyIx(prop_new_prop2.first), LITERAL(44));
+  auto *rhs = this->storage.template Create<MapLiteral>(prop_map);
+
+  auto op_type{plan::SetProperties::Op::UPDATE};
+  auto set_op = std::make_shared<plan::SetProperties>(n.op_, n.sym_, rhs, op_type);
+  auto context = MakeContext(this->storage, symbol_table, &dba);
+  PullAll(*set_op, &context);
+  dba.AdvanceCommand();
+
+  auto new_properties = vertex_accessor.Properties(memgraph::storage::View::OLD);
+  std::map<memgraph::storage::PropertyId, memgraph::storage::PropertyValue> expected_properties;
+  expected_properties.emplace(dba.NameToProperty("prop1"), memgraph::storage::PropertyValue(43));
+  expected_properties.emplace(dba.NameToProperty("prop2"), memgraph::storage::PropertyValue(44));
+  expected_properties.emplace(dba.NameToProperty("new_prop1"), memgraph::storage::PropertyValue(43));
+  expected_properties.emplace(dba.NameToProperty("new_prop2"), memgraph::storage::PropertyValue(44));
+  EXPECT_EQ(context.evaluation_context.property_lookups_cache.size(), 0);
+  EXPECT_EQ(new_properties.HasError(), false);
+  EXPECT_EQ(*new_properties, expected_properties);
+}
+
 TYPED_TEST(QueryPlanTest, SetLabelsOnNull) {
   // OPTIONAL MATCH (n) SET n :label
   auto storage_dba = this->db->Access();
diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp
index 0c75e7027..d49eb3798 100644
--- a/tests/unit/query_semantic.cpp
+++ b/tests/unit/query_semantic.cpp
@@ -1020,7 +1020,7 @@ TYPED_TEST(TestSymbolGenerator, MatchUnionReturnSymbols) {
   EXPECT_EQ(symbol_table.max_position(), 8);
 }
 
-TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNameThrowSemanticExpcetion) {
+TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNameThrowSemanticException) {
   // WITH 1 as X, 2 AS Y RETURN * UNION RETURN 3 AS Z, 4 AS Y
   auto ret = this->storage.template Create<Return>();
   ret->body_.all_identifiers = true;
@@ -1029,7 +1029,7 @@ TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNameThrowSemanticExpcetion) {
   EXPECT_THROW(memgraph::query::MakeSymbolTable(query), SemanticException);
 }
 
-TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNumberThrowSemanticExpcetion) {
+TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNumberThrowSemanticException) {
   // WITH 1 as X, 2 AS Y RETURN * UNION RETURN 4 AS Y
   auto ret = this->storage.template Create<Return>();
   ret->body_.all_identifiers = true;
@@ -1278,3 +1278,166 @@ TYPED_TEST(TestSymbolGenerator, Subqueries) {
   symbol_table = MakeSymbolTable(query);
   ASSERT_EQ(symbol_table.max_position(), 13);
 }
+
+TYPED_TEST(TestSymbolGenerator, PropertyCachingSingleLookup) {
+  // WITH {icode: 0000} AS item
+  // RETURN {icode: item.icode} AS new_map;
+
+  auto prop1_key = this->storage.GetPropertyIx("icode");
+  auto prop1_val = PROPERTY_LOOKUP(this->dba, "item", this->dba.NameToProperty("icode"));
+
+  auto has_properties = MAP({prop1_key, LITERAL(0000)});
+  auto new_map = MAP({prop1_key, prop1_val});
+  auto query = QUERY(SINGLE_QUERY(WITH(has_properties, AS("item")), RETURN(new_map, AS("new_map"))));
+
+  memgraph::query::MakeSymbolTable(query);
+
+  auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[prop1_key])->evaluation_mode_;
+
+  ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY);
+}
+
+TYPED_TEST(TestSymbolGenerator, PropertyCachingTwoSingleLookups) {
+  // WITH {icode: 0000} AS item1, {icode: 1111} AS item2
+  // RETURN {icode1: item1.icode, icode2: item2.icode} AS new_map;
+
+  auto in_prop1_key = this->storage.GetPropertyIx("icode");
+  auto out_prop1_key = this->storage.GetPropertyIx("icode1");
+  auto out_prop1_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("icode"));
+  auto out_prop2_key = this->storage.GetPropertyIx("icode2");
+  auto out_prop2_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("icode"));
+
+  auto has_properties1 = MAP({in_prop1_key, LITERAL(0000)});
+  auto has_properties2 = MAP({in_prop1_key, LITERAL(1111)});
+  auto new_map = MAP({out_prop1_key, out_prop1_val}, {out_prop2_key, out_prop2_val});
+  auto query = QUERY(
+      SINGLE_QUERY(WITH(has_properties1, AS("item1"), has_properties2, AS("item2")), RETURN(new_map, AS("new_map"))));
+
+  memgraph::query::MakeSymbolTable(query);
+
+  auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop1_key])->evaluation_mode_;
+  auto prop2_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop2_key])->evaluation_mode_;
+
+  ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY);
+  ASSERT_TRUE(prop2_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY);
+}
+
+TYPED_TEST(TestSymbolGenerator, PropertyCachingMultipleLookup) {
+  // WITH {icode: 0000, price: 10} AS item
+  // RETURN {icode: item.icode, price: item.price} AS new_map;
+
+  auto prop1_key = this->storage.GetPropertyIx("icode");
+  auto prop1_val = PROPERTY_LOOKUP(this->dba, "item", this->dba.NameToProperty("icode"));
+  auto prop2_key = this->storage.GetPropertyIx("price");
+  auto prop2_val = PROPERTY_LOOKUP(this->dba, "item", this->dba.NameToProperty("price"));
+
+  auto has_properties = MAP({prop1_key, LITERAL(0000)}, {prop2_key, LITERAL(10)});
+  auto new_map = MAP({prop1_key, prop1_val}, {prop2_key, prop2_val});
+  auto query = QUERY(SINGLE_QUERY(WITH(has_properties, AS("item")), RETURN(new_map, AS("new_map"))));
+
+  memgraph::query::MakeSymbolTable(query);
+
+  auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[prop1_key])->evaluation_mode_;
+  auto prop2_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[prop2_key])->evaluation_mode_;
+
+  ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+  ASSERT_TRUE(prop2_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+}
+
+TYPED_TEST(TestSymbolGenerator, PropertyCachingTwoMultipleLookups) {
+  // WITH {icode: 0000, price: 10} AS item1, {icode: 1111, price: 16} AS item2
+  // RETURN {icode1: item1.icode, price1: item1.price, icode2: item2.icode, price2: item2.price} AS new_map;
+
+  auto in_prop1_key = this->storage.GetPropertyIx("icode");
+  auto in_prop2_key = this->storage.GetPropertyIx("price");
+
+  auto out_prop1_key = this->storage.GetPropertyIx("icode1");
+  auto out_prop1_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("icode"));
+  auto out_prop2_key = this->storage.GetPropertyIx("price1");
+  auto out_prop2_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("price"));
+  auto out_prop3_key = this->storage.GetPropertyIx("icode2");
+  auto out_prop3_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("icode"));
+  auto out_prop4_key = this->storage.GetPropertyIx("price2");
+  auto out_prop4_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("price"));
+
+  auto has_properties1 = MAP({in_prop1_key, LITERAL(0000)}, {in_prop2_key, LITERAL(10)});
+  auto has_properties2 = MAP({in_prop1_key, LITERAL(1111)}, {in_prop2_key, LITERAL(16)});
+  auto new_map = MAP({out_prop1_key, out_prop1_val}, {out_prop2_key, out_prop2_val}, {out_prop3_key, out_prop3_val},
+                     {out_prop4_key, out_prop4_val});
+  auto query = QUERY(
+      SINGLE_QUERY(WITH(has_properties1, AS("item1"), has_properties2, AS("item2")), RETURN(new_map, AS("new_map"))));
+
+  memgraph::query::MakeSymbolTable(query);
+
+  auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop1_key])->evaluation_mode_;
+  auto prop2_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop2_key])->evaluation_mode_;
+  auto prop3_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop3_key])->evaluation_mode_;
+  auto prop4_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop4_key])->evaluation_mode_;
+
+  ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+  ASSERT_TRUE(prop2_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+  ASSERT_TRUE(prop3_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+  ASSERT_TRUE(prop4_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+}
+
+TYPED_TEST(TestSymbolGenerator, PropertyCachingMixedLookups1) {
+  // WITH {icode: 0000, price: 10} AS item1, {icode: 1111, price: 16} AS item2
+  // RETURN {icode1: item1.icode, price1: item1.price, icode2: item2.icode} AS new_map;
+
+  auto in_prop1_key = this->storage.GetPropertyIx("icode");
+  auto in_prop2_key = this->storage.GetPropertyIx("price");
+
+  auto out_prop1_key = this->storage.GetPropertyIx("icode1");
+  auto out_prop1_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("icode"));
+  auto out_prop2_key = this->storage.GetPropertyIx("price1");
+  auto out_prop2_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("price"));
+  auto out_prop3_key = this->storage.GetPropertyIx("icode2");
+  auto out_prop3_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("icode"));
+
+  auto has_properties1 = MAP({in_prop1_key, LITERAL(0000)}, {in_prop2_key, LITERAL(10)});
+  auto has_properties2 = MAP({in_prop1_key, LITERAL(1111)}, {in_prop2_key, LITERAL(16)});
+  auto new_map = MAP({out_prop1_key, out_prop1_val}, {out_prop2_key, out_prop2_val}, {out_prop3_key, out_prop3_val});
+  auto query = QUERY(
+      SINGLE_QUERY(WITH(has_properties1, AS("item1"), has_properties2, AS("item2")), RETURN(new_map, AS("new_map"))));
+
+  memgraph::query::MakeSymbolTable(query);
+
+  auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop1_key])->evaluation_mode_;
+  auto prop2_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop2_key])->evaluation_mode_;
+  auto prop3_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop3_key])->evaluation_mode_;
+
+  ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+  ASSERT_TRUE(prop2_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+  ASSERT_TRUE(prop3_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY);
+}
+
+TYPED_TEST(TestSymbolGenerator, PropertyCachingMixedLookups2) {
+  // WITH {icode: 0000, price: 10} AS item1, {icode: 1111, price: 16} AS item2
+  // RETURN {icode1: item1.icode, icode2: item2.icode, price2: item2.price} AS new_map;
+
+  auto in_prop1_key = this->storage.GetPropertyIx("icode");
+  auto in_prop2_key = this->storage.GetPropertyIx("price");
+
+  auto out_prop1_key = this->storage.GetPropertyIx("icode1");
+  auto out_prop1_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("icode"));
+  auto out_prop3_key = this->storage.GetPropertyIx("icode2");
+  auto out_prop3_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("icode"));
+  auto out_prop4_key = this->storage.GetPropertyIx("price2");
+  auto out_prop4_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("price"));
+
+  auto has_properties1 = MAP({in_prop1_key, LITERAL(0000)}, {in_prop2_key, LITERAL(10)});
+  auto has_properties2 = MAP({in_prop1_key, LITERAL(1111)}, {in_prop2_key, LITERAL(16)});
+  auto new_map = MAP({out_prop1_key, out_prop1_val}, {out_prop3_key, out_prop3_val}, {out_prop4_key, out_prop4_val});
+  auto query = QUERY(
+      SINGLE_QUERY(WITH(has_properties1, AS("item1"), has_properties2, AS("item2")), RETURN(new_map, AS("new_map"))));
+
+  memgraph::query::MakeSymbolTable(query);
+
+  auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop1_key])->evaluation_mode_;
+  auto prop3_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop3_key])->evaluation_mode_;
+  auto prop4_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop4_key])->evaluation_mode_;
+
+  ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY);
+  ASSERT_TRUE(prop3_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+  ASSERT_TRUE(prop4_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES);
+}