From d79dd696079241aee45cad4a252daa3391be6f4f Mon Sep 17 00:00:00 2001
From: Antonio Filipovic <61245998+antoniofilipovic@users.noreply.github.com>
Date: Fri, 24 Feb 2023 15:40:35 +0100
Subject: [PATCH] Improve performance with props init on node|edge creation
 (#788)

---
 src/query/common.hpp                          | 34 +++++++++-
 src/query/db_accessor.hpp                     | 10 ++-
 src/query/plan/operator.cpp                   | 19 +++---
 src/storage/v2/edge_accessor.cpp              | 20 +++++-
 src/storage/v2/edge_accessor.hpp              |  7 ++-
 src/storage/v2/property_store.cpp             | 62 ++++++++++++++++++-
 src/storage/v2/property_store.hpp             |  8 ++-
 src/storage/v2/vertex_accessor.cpp            | 19 +++++-
 src/storage/v2/vertex_accessor.hpp            |  7 ++-
 .../memgraph_V1/features/functions.feature    |  4 +-
 tests/unit/storage_v2_property_store.cpp      | 25 +++++++-
 11 files changed, 195 insertions(+), 20 deletions(-)

diff --git a/src/query/common.hpp b/src/query/common.hpp
index c51c34dee..6154900b5 100644
--- a/src/query/common.hpp
+++ b/src/query/common.hpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Memgraph Ltd.
+// Copyright 2023 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -107,5 +107,37 @@ storage::PropertyValue PropsSetChecked(T *record, const storage::PropertyId &key
   }
 }
 
+template <typename T>
+concept AccessorWithInitProperties = requires(T accessor,
+                                              const std::map<storage::PropertyId, storage::PropertyValue> &properties) {
+  { accessor.InitProperties(properties) } -> std::same_as<storage::Result<bool>>;
+};
+
+/// Set property `values` mapped with given `key` on a `record`.
+///
+/// @throw QueryRuntimeException if value cannot be set as a property value
+template <AccessorWithInitProperties T>
+bool MultiPropsInitChecked(T *record, std::map<storage::PropertyId, storage::PropertyValue> &properties) {
+  try {
+    auto maybe_values = record->InitProperties(properties);
+    if (maybe_values.HasError()) {
+      switch (maybe_values.GetError()) {
+        case storage::Error::SERIALIZATION_ERROR:
+          throw TransactionSerializationException();
+        case storage::Error::DELETED_OBJECT:
+          throw QueryRuntimeException("Trying to set properties on a deleted object.");
+        case storage::Error::PROPERTIES_DISABLED:
+          throw QueryRuntimeException("Can't set property because properties on edges are disabled.");
+        case storage::Error::VERTEX_HAS_EDGES:
+        case storage::Error::NONEXISTENT_OBJECT:
+          throw QueryRuntimeException("Unexpected error when setting a property.");
+      }
+    }
+    return std::move(*maybe_values);
+  } catch (const TypedValueException &) {
+    throw QueryRuntimeException("Cannot set properties.");
+  }
+}
+
 int64_t QueryTimestamp();
 }  // namespace memgraph::query
diff --git a/src/query/db_accessor.hpp b/src/query/db_accessor.hpp
index 2c269a951..91c2ec721 100644
--- a/src/query/db_accessor.hpp
+++ b/src/query/db_accessor.hpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Memgraph Ltd.
+// Copyright 2023 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -71,6 +71,10 @@ class EdgeAccessor final {
     return impl_.SetProperty(key, value);
   }
 
+  storage::Result<bool> InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties) {
+    return impl_.InitProperties(properties);
+  }
+
   storage::Result<storage::PropertyValue> RemoveProperty(storage::PropertyId key) {
     return SetProperty(key, storage::PropertyValue());
   }
@@ -125,6 +129,10 @@ class VertexAccessor final {
     return impl_.SetProperty(key, value);
   }
 
+  storage::Result<bool> InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties) {
+    return impl_.InitProperties(properties);
+  }
+
   storage::Result<storage::PropertyValue> RemoveProperty(storage::PropertyId key) {
     return SetProperty(key, storage::PropertyValue());
   }
diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp
index 645b0a463..846545721 100644
--- a/src/query/plan/operator.cpp
+++ b/src/query/plan/operator.cpp
@@ -25,6 +25,7 @@
 
 #include <cppitertools/chain.hpp>
 #include <cppitertools/imap.hpp>
+#include "query/common.hpp"
 #include "spdlog/spdlog.h"
 
 #include "license/license.hpp"
@@ -208,17 +209,18 @@ VertexAccessor &CreateLocalVertex(const NodeCreationInfo &node_info, Frame *fram
                                 storage::View::NEW);
   // TODO: PropsSetChecked allocates a PropertyValue, make it use context.memory
   // when we update PropertyValue with custom allocator.
+  std::map<storage::PropertyId, storage::PropertyValue> properties;
   if (const auto *node_info_properties = std::get_if<PropertiesMapList>(&node_info.properties)) {
     for (const auto &[key, value_expression] : *node_info_properties) {
-      PropsSetChecked(&new_node, key, value_expression->Accept(evaluator));
+      properties.emplace(key, value_expression->Accept(evaluator));
     }
   } else {
     auto property_map = evaluator.Visit(*std::get<ParameterLookup *>(node_info.properties));
     for (const auto &[key, value] : property_map.ValueMap()) {
-      auto property_id = dba.NameToProperty(key);
-      PropsSetChecked(&new_node, property_id, value);
+      properties.emplace(dba.NameToProperty(key), value);
     }
   }
+  MultiPropsInitChecked(&new_node, properties);
 
   (*frame)[node_info.symbol] = new_node;
   return (*frame)[node_info.symbol].ValueVertex();
@@ -299,17 +301,18 @@ EdgeAccessor CreateEdge(const EdgeCreationInfo &edge_info, DbAccessor *dba, Vert
   auto maybe_edge = dba->InsertEdge(from, to, edge_info.edge_type);
   if (maybe_edge.HasValue()) {
     auto &edge = *maybe_edge;
-    if (const auto *properties = std::get_if<PropertiesMapList>(&edge_info.properties)) {
-      for (const auto &[key, value_expression] : *properties) {
-        PropsSetChecked(&edge, key, value_expression->Accept(*evaluator));
+    std::map<storage::PropertyId, storage::PropertyValue> properties;
+    if (const auto *edge_info_properties = std::get_if<PropertiesMapList>(&edge_info.properties)) {
+      for (const auto &[key, value_expression] : *edge_info_properties) {
+        properties.emplace(key, value_expression->Accept(*evaluator));
       }
     } else {
       auto property_map = evaluator->Visit(*std::get<ParameterLookup *>(edge_info.properties));
       for (const auto &[key, value] : property_map.ValueMap()) {
-        auto property_id = dba->NameToProperty(key);
-        PropsSetChecked(&edge, property_id, value);
+        properties.emplace(dba->NameToProperty(key), value);
       }
     }
+    if (!properties.empty()) MultiPropsInitChecked(&edge, properties);
 
     (*frame)[edge_info.symbol] = edge;
   } else {
diff --git a/src/storage/v2/edge_accessor.cpp b/src/storage/v2/edge_accessor.cpp
index 2a15292a2..abfb67ff7 100644
--- a/src/storage/v2/edge_accessor.cpp
+++ b/src/storage/v2/edge_accessor.cpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Memgraph Ltd.
+// Copyright 2023 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -123,6 +123,24 @@ Result<storage::PropertyValue> EdgeAccessor::SetProperty(PropertyId property, co
   return std::move(current_value);
 }
 
+Result<bool> EdgeAccessor::InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties) {
+  utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
+  if (!config_.properties_on_edges) return Error::PROPERTIES_DISABLED;
+
+  std::lock_guard<utils::SpinLock> guard(edge_.ptr->lock);
+
+  if (!PrepareForWrite(transaction_, edge_.ptr)) return Error::SERIALIZATION_ERROR;
+
+  if (edge_.ptr->deleted) return Error::DELETED_OBJECT;
+
+  if (!edge_.ptr->properties.InitProperties(properties)) return false;
+  for (const auto &[property, _] : properties) {
+    CreateAndLinkDelta(transaction_, edge_.ptr, Delta::SetPropertyTag(), property, PropertyValue());
+  }
+
+  return true;
+}
+
 Result<std::map<PropertyId, PropertyValue>> EdgeAccessor::ClearProperties() {
   if (!config_.properties_on_edges) return Error::PROPERTIES_DISABLED;
 
diff --git a/src/storage/v2/edge_accessor.hpp b/src/storage/v2/edge_accessor.hpp
index b0a1e1151..ee8176365 100644
--- a/src/storage/v2/edge_accessor.hpp
+++ b/src/storage/v2/edge_accessor.hpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Memgraph Ltd.
+// Copyright 2023 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -58,6 +58,11 @@ class EdgeAccessor final {
   /// @throw std::bad_alloc
   Result<storage::PropertyValue> SetProperty(PropertyId property, const PropertyValue &value);
 
+  /// Set property values only if property store is empty. Returns `true` if successully set all values,
+  /// `false` otherwise.
+  /// @throw std::bad_alloc
+  Result<bool> InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties);
+
   /// Remove all properties and return old values for each removed property.
   /// @throw std::bad_alloc
   Result<std::map<PropertyId, PropertyValue>> ClearProperties();
diff --git a/src/storage/v2/property_store.cpp b/src/storage/v2/property_store.cpp
index db8982b6f..f689aa844 100644
--- a/src/storage/v2/property_store.cpp
+++ b/src/storage/v2/property_store.cpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Memgraph Ltd.
+// Copyright 2023 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -1053,7 +1053,7 @@ bool PropertyStore::SetProperty(PropertyId property, const PropertyValue &value)
         in_local_buffer = true;
       } else {
         // Allocate a new external buffer.
-        auto alloc_data = new uint8_t[property_size_to_power_of_8];
+        auto *alloc_data = new uint8_t[property_size_to_power_of_8];
         auto alloc_size = property_size_to_power_of_8;
 
         SetSizeData(buffer_, alloc_size, alloc_data);
@@ -1144,6 +1144,64 @@ bool PropertyStore::SetProperty(PropertyId property, const PropertyValue &value)
   return !existed;
 }
 
+bool PropertyStore::InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties) {
+  uint64_t size = 0;
+  uint8_t *data = nullptr;
+  std::tie(size, data) = GetSizeData(buffer_);
+  if (size != 0) {
+    return false;
+  }
+
+  uint64_t property_size = 0;
+  {
+    Writer writer;
+    for (const auto &[property, value] : properties) {
+      if (value.IsNull()) {
+        continue;
+      }
+      EncodeProperty(&writer, property, value);
+      property_size = writer.Written();
+    }
+  }
+
+  auto property_size_to_power_of_8 = ToPowerOf8(property_size);
+  if (property_size <= sizeof(buffer_) - 1) {
+    // Use the local buffer.
+    buffer_[0] = kUseLocalBuffer;
+    size = sizeof(buffer_) - 1;
+    data = &buffer_[1];
+  } else {
+    // Allocate a new external buffer.
+    auto *alloc_data = new uint8_t[property_size_to_power_of_8];
+    auto alloc_size = property_size_to_power_of_8;
+
+    SetSizeData(buffer_, alloc_size, alloc_data);
+
+    size = alloc_size;
+    data = alloc_data;
+  }
+
+  // Encode the property into the data buffer.
+  Writer writer(data, size);
+
+  for (const auto &[property, value] : properties) {
+    if (value.IsNull()) {
+      continue;
+    }
+    MG_ASSERT(EncodeProperty(&writer, property, value), "Invalid database state!");
+    writer.Written();
+  }
+
+  auto metadata = writer.WriteMetadata();
+  if (metadata) {
+    // If there is any space left in the buffer we add a tombstone to
+    // indicate that there are no more properties to be decoded.
+    metadata->Set({Type::EMPTY});
+  }
+
+  return true;
+}
+
 bool PropertyStore::ClearProperties() {
   bool in_local_buffer = false;
   uint64_t size;
diff --git a/src/storage/v2/property_store.hpp b/src/storage/v2/property_store.hpp
index bd397285f..f0be30df7 100644
--- a/src/storage/v2/property_store.hpp
+++ b/src/storage/v2/property_store.hpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Memgraph Ltd.
+// Copyright 2023 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -59,6 +59,12 @@ class PropertyStore {
   /// @throw std::bad_alloc
   bool SetProperty(PropertyId property, const PropertyValue &value);
 
+  /// Init property values and return `true` if insertion took place. `false` is
+  /// returned if there exists property in property store and insertion couldn't take place. The time complexity of this
+  /// function is O(n).
+  /// @throw std::bad_alloc
+  bool InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties);
+
   /// Remove all properties and return `true` if any removal took place.
   /// `false` is returned if there were no properties to remove. The time
   /// complexity of this function is O(1).
diff --git a/src/storage/v2/vertex_accessor.cpp b/src/storage/v2/vertex_accessor.cpp
index 05ba1ebcc..3b76080cb 100644
--- a/src/storage/v2/vertex_accessor.cpp
+++ b/src/storage/v2/vertex_accessor.cpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Memgraph Ltd.
+// Copyright 2023 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -230,6 +230,23 @@ Result<PropertyValue> VertexAccessor::SetProperty(PropertyId property, const Pro
   return std::move(current_value);
 }
 
+Result<bool> VertexAccessor::InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties) {
+  utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
+  std::lock_guard<utils::SpinLock> guard(vertex_->lock);
+
+  if (!PrepareForWrite(transaction_, vertex_)) return Error::SERIALIZATION_ERROR;
+
+  if (vertex_->deleted) return Error::DELETED_OBJECT;
+
+  if (!vertex_->properties.InitProperties(properties)) return false;
+  for (const auto &[property, value] : properties) {
+    CreateAndLinkDelta(transaction_, vertex_, Delta::SetPropertyTag(), property, PropertyValue());
+    UpdateOnSetProperty(indices_, property, value, vertex_, *transaction_);
+  }
+
+  return true;
+}
+
 Result<std::map<PropertyId, PropertyValue>> VertexAccessor::ClearProperties() {
   std::lock_guard<utils::SpinLock> guard(vertex_->lock);
 
diff --git a/src/storage/v2/vertex_accessor.hpp b/src/storage/v2/vertex_accessor.hpp
index 840eec910..79a391708 100644
--- a/src/storage/v2/vertex_accessor.hpp
+++ b/src/storage/v2/vertex_accessor.hpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Memgraph Ltd.
+// Copyright 2023 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -68,6 +68,11 @@ class VertexAccessor final {
   /// @throw std::bad_alloc
   Result<PropertyValue> SetProperty(PropertyId property, const PropertyValue &value);
 
+  /// Set property values only if property store is empty. Returns `true` if successully set all values,
+  /// `false` otherwise.
+  /// @throw std::bad_alloc
+  Result<bool> InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties);
+
   /// Remove all properties and return the values of the removed properties.
   /// @throw std::bad_alloc
   Result<std::map<PropertyId, PropertyValue>> ClearProperties();
diff --git a/tests/gql_behave/tests/memgraph_V1/features/functions.feature b/tests/gql_behave/tests/memgraph_V1/features/functions.feature
index 6df7e2e0c..169722f88 100644
--- a/tests/gql_behave/tests/memgraph_V1/features/functions.feature
+++ b/tests/gql_behave/tests/memgraph_V1/features/functions.feature
@@ -585,12 +585,12 @@ Feature: Functions
             """
         When executing query:
             """
-            MATCH ()-[r]->() RETURN PROPERTIES(r) AS p
+            MATCH ()-[r]->() RETURN PROPERTIES(r) AS p ORDER BY p.prop;
             """
         Then the result should be:
             | p         |
-            | {b: true} |
             | {}        |
+            | {b: true} |
             | {c: 123}  |
 
     Scenario: Properties test2:
diff --git a/tests/unit/storage_v2_property_store.cpp b/tests/unit/storage_v2_property_store.cpp
index e247806d8..bf2a8f0a9 100644
--- a/tests/unit/storage_v2_property_store.cpp
+++ b/tests/unit/storage_v2_property_store.cpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Memgraph Ltd.
+// Copyright 2023 Memgraph Ltd.
 //
 // Use of this software is governed by the Business Source License
 // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -649,3 +649,26 @@ TEST(PropertyStore, IsPropertyEqualTemporalData) {
   ASSERT_FALSE(props.IsPropertyEqual(prop, memgraph::storage::PropertyValue(memgraph::storage::TemporalData{
                                                memgraph::storage::TemporalType::Date, 30})));
 }
+
+TEST(PropertyStore, SetMultipleProperties) {
+  memgraph::storage::PropertyStore store;
+  std::vector<memgraph::storage::PropertyValue> vec{memgraph::storage::PropertyValue(true),
+                                                    memgraph::storage::PropertyValue(123),
+                                                    memgraph::storage::PropertyValue()};
+  std::map<std::string, memgraph::storage::PropertyValue> map{{"nandare", memgraph::storage::PropertyValue(false)}};
+  const memgraph::storage::TemporalData temporal{memgraph::storage::TemporalType::LocalDateTime, 23};
+  std::map<memgraph::storage::PropertyId, memgraph::storage::PropertyValue> data{
+      {memgraph::storage::PropertyId::FromInt(1), memgraph::storage::PropertyValue(true)},
+      {memgraph::storage::PropertyId::FromInt(2), memgraph::storage::PropertyValue(123)},
+      {memgraph::storage::PropertyId::FromInt(3), memgraph::storage::PropertyValue(123.5)},
+      {memgraph::storage::PropertyId::FromInt(4), memgraph::storage::PropertyValue("nandare")},
+      {memgraph::storage::PropertyId::FromInt(5), memgraph::storage::PropertyValue(vec)},
+      {memgraph::storage::PropertyId::FromInt(6), memgraph::storage::PropertyValue(map)},
+      {memgraph::storage::PropertyId::FromInt(7), memgraph::storage::PropertyValue(temporal)}};
+
+  store.InitProperties(data);
+
+  for (auto &[key, value] : data) {
+    ASSERT_TRUE(store.IsPropertyEqual(key, value));
+  }
+}