From 609b9a20f1a455ce6d3dbf835d9f626a30911901 Mon Sep 17 00:00:00 2001 From: imilinovic <44698587+imilinovic@users.noreply.github.com> Date: Fri, 28 Jul 2023 09:08:36 +0200 Subject: [PATCH] Add hash on mgp::Value (#1093) --- include/_mgp.hpp | 9 +++ include/mgp.hpp | 145 +++++++++++++++++++++++++++++++++++++++++ tests/unit/cpp_api.cpp | 9 +++ 3 files changed, 163 insertions(+) diff --git a/include/_mgp.hpp b/include/_mgp.hpp index 7e6bef95a..152bb71f9 100644 --- a/include/_mgp.hpp +++ b/include/_mgp.hpp @@ -135,6 +135,13 @@ inline int64_t value_get_int(mgp_value *val) { return MgInvoke(mgp_valu inline double value_get_double(mgp_value *val) { return MgInvoke(mgp_value_get_double, val); } +inline double value_get_numeric(mgp_value *val) { + if (MgInvoke(mgp_value_is_int, val)) { + return static_cast(value_get_int(val)); + } + return value_get_double(val); +} + inline const char *value_get_string(mgp_value *val) { return MgInvoke(mgp_value_get_string, val); } inline mgp_list *value_get_list(mgp_value *val) { return MgInvoke(mgp_value_get_list, val); } @@ -171,6 +178,8 @@ inline bool value_is_int(mgp_value *val) { return MgInvoke(mgp_value_is_int inline bool value_is_double(mgp_value *val) { return MgInvoke(mgp_value_is_double, val); } +inline bool value_is_numeric(mgp_value *val) { return value_is_int(val) || value_is_double(val); } + inline bool value_is_string(mgp_value *val) { return MgInvoke(mgp_value_is_string, val); } inline bool value_is_list(mgp_value *val) { return MgInvoke(mgp_value_is_list, val); } diff --git a/include/mgp.hpp b/include/mgp.hpp index 3a5f8f382..afe606da5 100644 --- a/include/mgp.hpp +++ b/include/mgp.hpp @@ -11,14 +11,17 @@ #pragma once +#include #include #include #include #include #include +#include #include #include "_mgp.hpp" +#include "mg_exceptions.hpp" #include "mg_procedure.h" namespace mgp { @@ -1347,6 +1350,67 @@ inline void AddFunction(mgp_func_cb callback, std::string_view name, std::vector /* #endregion */ namespace util { +inline uint64_t Fnv(const std::string_view s) { + // fnv1a is recommended so use it as the default implementation. + uint64_t hash = 14695981039346656037UL; + + for (const auto &ch : s) { + hash = (hash ^ (uint64_t)ch) * 1099511628211UL; + } + + return hash; +} + +/** + * Does FNV-like hashing on a collection. Not truly FNV + * because it operates on 8-bit elements, while this + * implementation uses size_t elements (collection item + * hash). + * + * https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + * + * + * @tparam TIterable A collection type that has begin() and end(). + * @tparam TElement Type of element in the collection. + * @tparam THash Hash type (has operator() that accepts a 'const TEelement &' + * and returns size_t. Defaults to std::hash. + * @param iterable A collection of elements. + * @param element_hash Function for hashing a single element. + * @return The hash of the whole collection. + */ +template > +struct FnvCollection { + size_t operator()(const TIterable &iterable) const { + uint64_t hash = 14695981039346656037u; + THash element_hash; + for (const TElement &element : iterable) { + hash *= fnv_prime; + hash ^= element_hash(element); + } + return hash; + } + + private: + static const uint64_t fnv_prime = 1099511628211u; +}; + +/** + * Like FNV hashing for a collection, just specialized for two elements to avoid + * iteration overhead. + */ +template , typename TBHash = std::hash> +struct HashCombine { + size_t operator()(const TA &a, const TB &b) const { + static constexpr size_t fnv_prime = 1099511628211UL; + static constexpr size_t fnv_offset = 14695981039346656037UL; + size_t ret = fnv_offset; + ret ^= TAHash()(a); + ret *= fnv_prime; + ret ^= TBHash()(b); + return ret; + } +}; + // uint to int conversion in C++ is a bit tricky. Take a look here // https://stackoverflow.com/questions/14623266/why-cant-i-reinterpret-cast-uint-to-int // for more details. @@ -1469,6 +1533,10 @@ inline bool ValuesEqual(mgp_value *value1, mgp_value *value2) { if (value1 == value2) { return true; } + // Make int and double comparable, (ex. this is true -> 1.0 == 1) + if (mgp::value_is_numeric(value1) && mgp::value_is_numeric(value2)) { + return mgp::value_get_numeric(value1) == mgp::value_get_numeric(value2); + } if (mgp::value_get_type(value1) != mgp::value_get_type(value2)) { return false; } @@ -3596,6 +3664,28 @@ struct hash { size_t operator()(const mgp::Relationship &x) const { return hash()(x.Id().AsInt()); }; }; +template <> +struct hash { + size_t operator()(const mgp::Path &x) const { + // https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + // See mgp::util::FnvCollection + constexpr const uint64_t fnv_prime = 1099511628211U; + uint64_t hash = 14695981039346656037U; + + auto multiply_and_xor = [](uint64_t &hash, size_t element_hash) { + hash *= fnv_prime; + hash ^= element_hash; + }; + + for (size_t i = 0; i < x.Length() - 1; ++i) { + multiply_and_xor(hash, std::hash{}(x.GetNodeAt(i))); + multiply_and_xor(hash, std::hash{}(x.GetRelationshipAt(i))); + } + multiply_and_xor(hash, std::hash{}(x.GetNodeAt(x.Length()))); + return hash; + } +}; + template <> struct hash { size_t operator()(const mgp::Date &x) const { return hash()(x.Timestamp()); }; @@ -3620,4 +3710,59 @@ template <> struct hash { size_t operator()(const mgp::MapItem &x) const { return hash()(x.key); }; }; + +template <> +struct hash { + size_t operator()(const mgp::Map &x) const { + return mgp::util::FnvCollection>{}(x); + } +}; + +template <> +struct hash { + size_t operator()(const mgp::Value &x) const { + switch (x.Type()) { + case mgp::Type::Null: + return 31; + case mgp::Type::Any: + throw mg_exception::InvalidArgumentException(); + case mgp::Type::Bool: + return std::hash{}(x.ValueBool()); + case mgp::Type::Int: + // we cast int to double for hashing purposes + // to be consistent with equality (2.0 == 2) == true + return std::hash{}((double)x.ValueInt()); + case mgp::Type::Double: + return std::hash{}(x.ValueDouble()); + case mgp::Type::String: + return std::hash{}(x.ValueString()); + case mgp::Type::List: + return mgp::util::FnvCollection>{}(x.ValueList()); + case mgp::Type::Map: + return std::hash{}(x.ValueMap()); + case mgp::Type::Node: + return std::hash{}(x.ValueNode()); + case mgp::Type::Relationship: + return std::hash{}(x.ValueRelationship()); + case mgp::Type::Path: + return std::hash{}(x.ValuePath()); + case mgp::Type::Date: + return std::hash{}(x.ValueDate()); + case mgp::Type::LocalTime: + return std::hash{}(x.ValueLocalTime()); + case mgp::Type::LocalDateTime: + return std::hash{}(x.ValueLocalDateTime()); + case mgp::Type::Duration: + return std::hash{}(x.ValueDuration()); + } + throw mg_exception::InvalidArgumentException(); + } +}; + +template <> +struct hash { + size_t operator()(const mgp::List &x) { + return mgp::util::FnvCollection>{}(x); + } +}; } // namespace std diff --git a/tests/unit/cpp_api.cpp b/tests/unit/cpp_api.cpp index 7741b2dfd..79a12741a 100644 --- a/tests/unit/cpp_api.cpp +++ b/tests/unit/cpp_api.cpp @@ -473,6 +473,15 @@ TYPED_TEST(CppApiTestFixture, TestNodeProperties) { ASSERT_EQ(node_1.GetProperty("b").ValueString(), "b"); } +TYPED_TEST(CppApiTestFixture, TestNumberEquality) { + mgp::Value double_1{1.0}; + mgp::Value int_1{static_cast(1)}; + ASSERT_TRUE(double_1 == int_1); + mgp::Value double_2{2.01}; + mgp::Value int_2{static_cast(2)}; + ASSERT_FALSE(double_2 == int_2); +} + TYPED_TEST(CppApiTestFixture, TestTypeOperatorStream) { std::string string1 = "string"; int64_t int1 = 4;