diff --git a/src/storage/v2/name_id_mapper.hpp b/src/storage/v2/name_id_mapper.hpp new file mode 100644 index 000000000..e447213ef --- /dev/null +++ b/src/storage/v2/name_id_mapper.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include <atomic> +#include <string> + +#include "utils/skip_list.hpp" + +namespace storage { + +class NameIdMapper final { + private: + struct MapNameToId { + std::string name; + uint64_t id; + + bool operator<(const MapNameToId &other) { return name < other.name; } + bool operator==(const MapNameToId &other) { return name == other.name; } + + bool operator<(const std::string &other) { return name < other; } + bool operator==(const std::string &other) { return name == other; } + }; + + struct MapIdToName { + uint64_t id; + std::string name; + + bool operator<(const MapIdToName &other) { return id < other.id; } + bool operator==(const MapIdToName &other) { return id == other.id; } + + bool operator<(uint64_t other) { return id < other; } + bool operator==(uint64_t other) { return id == other; } + }; + + public: + uint64_t NameToId(const std::string &name) { + auto name_to_id_acc = name_to_id_.access(); + auto found = name_to_id_acc.find(name); + uint64_t id; + if (found == name_to_id_acc.end()) { + uint64_t new_id = counter_.fetch_add(1, std::memory_order_acq_rel); + // Try to insert the mapping with the `new_id`, but use the id that is in + // the object itself. The object that cointains the mapping is designed to + // be a map, so that if the inserted name already exists `insert` will + // return an iterator to the existing item. This prevents assignment of + // two IDs to the same name when the mapping is being inserted + // concurrently from two threads. One ID is wasted in that case, though. + id = name_to_id_acc.insert({name, new_id}).first->id; + } else { + id = found->id; + } + auto id_to_name_acc = id_to_name_.access(); + // We have to try to insert the ID to name mapping even if we are not the + // one who assigned the ID because we have to make sure that after this + // method returns that both mappings exist. + id_to_name_acc.insert({id, name}); + return id; + } + + // NOTE: Currently this function returns a `const std::string &` instead of a + // `std::string` to avoid making unnecessary copies of the string. + // Usually, this wouldn't be correct because the accessor to the + // `utils::SkipList` is destroyed in this function and that removes the + // guarantee that the reference to the value contained in the list will be + // valid. + // Currently, we never delete anything from the `utils::SkipList` so the + // references will always be valid. If you change this class to remove unused + // names, be sure to change the signature of this function. + const std::string &IdToName(uint64_t id) { + auto id_to_name_acc = id_to_name_.access(); + auto result = id_to_name_acc.find(id); + CHECK(result != id_to_name_acc.end()) + << "Trying to get a name for an invalid ID!"; + return result->name; + } + + private: + std::atomic<uint64_t> counter_{0}; + utils::SkipList<MapNameToId> name_to_id_; + utils::SkipList<MapIdToName> id_to_name_; +}; +} // namespace storage diff --git a/src/storage/v2/storage.cpp b/src/storage/v2/storage.cpp index fe2130b7b..f711fa029 100644 --- a/src/storage/v2/storage.cpp +++ b/src/storage/v2/storage.cpp @@ -274,6 +274,26 @@ Result<bool> Storage::Accessor::DeleteEdge(EdgeAccessor *edge) { return Result<bool>{true}; } +const std::string &Storage::Accessor::LabelToName(uint64_t label) { + return storage_->name_id_mapper_.IdToName(label); +} +const std::string &Storage::Accessor::PropertyToName(uint64_t property) { + return storage_->name_id_mapper_.IdToName(property); +} +const std::string &Storage::Accessor::EdgeTypeToName(uint64_t edge_type) { + return storage_->name_id_mapper_.IdToName(edge_type); +} + +uint64_t Storage::Accessor::NameToLabel(const std::string &name) { + return storage_->name_id_mapper_.NameToId(name); +} +uint64_t Storage::Accessor::NameToProperty(const std::string &name) { + return storage_->name_id_mapper_.NameToId(name); +} +uint64_t Storage::Accessor::NameToEdgeType(const std::string &name) { + return storage_->name_id_mapper_.NameToId(name); +} + void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; } void Storage::Accessor::Commit() { diff --git a/src/storage/v2/storage.hpp b/src/storage/v2/storage.hpp index fe5207018..37e2d4a80 100644 --- a/src/storage/v2/storage.hpp +++ b/src/storage/v2/storage.hpp @@ -6,6 +6,7 @@ #include "storage/v2/edge.hpp" #include "storage/v2/edge_accessor.hpp" #include "storage/v2/mvcc.hpp" +#include "storage/v2/name_id_mapper.hpp" #include "storage/v2/result.hpp" #include "storage/v2/transaction.hpp" #include "storage/v2/vertex.hpp" @@ -79,6 +80,14 @@ class Storage final { Result<bool> DeleteEdge(EdgeAccessor *edge); + const std::string &LabelToName(uint64_t label); + const std::string &PropertyToName(uint64_t property); + const std::string &EdgeTypeToName(uint64_t edge_type); + + uint64_t NameToLabel(const std::string &name); + uint64_t NameToProperty(const std::string &name); + uint64_t NameToEdgeType(const std::string &name); + void AdvanceCommand(); void Commit(); @@ -113,6 +122,8 @@ class Storage final { // whatever. CommitLog commit_log_; + NameIdMapper name_id_mapper_; + utils::SpinLock committed_transactions_lock_; std::list<Transaction> committed_transactions_; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 4d04f2f2c..34916a69f 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -316,6 +316,9 @@ target_link_libraries(${test_prefix}storage_v2 mg-storage-v2) add_unit_test(storage_v2_gc.cpp) target_link_libraries(${test_prefix}storage_v2_gc mg-storage-v2) +add_unit_test(storage_v2_name_id_mapper.cpp) +target_link_libraries(${test_prefix}storage_v2_name_id_mapper mg-storage-v2) + # Test LCP add_custom_command( diff --git a/tests/unit/storage_v2_name_id_mapper.cpp b/tests/unit/storage_v2_name_id_mapper.cpp new file mode 100644 index 000000000..e2353db07 --- /dev/null +++ b/tests/unit/storage_v2_name_id_mapper.cpp @@ -0,0 +1,36 @@ +#include <gtest/gtest.h> + +#include "storage/v2/name_id_mapper.hpp" + +// NOLINTNEXTLINE(hicpp-special-member-functions) +TEST(NameIdMapper, Basic) { + storage::NameIdMapper mapper; + + ASSERT_EQ(mapper.NameToId("n1"), 0); + ASSERT_EQ(mapper.NameToId("n2"), 1); + ASSERT_EQ(mapper.NameToId("n1"), 0); + ASSERT_EQ(mapper.NameToId("n2"), 1); + ASSERT_EQ(mapper.NameToId("n3"), 2); + + ASSERT_EQ(mapper.IdToName(0), "n1"); + ASSERT_EQ(mapper.IdToName(1), "n2"); +} + +// NOLINTNEXTLINE(hicpp-special-member-functions) +TEST(NameIdMapper, Correctness) { + storage::NameIdMapper mapper; + + ASSERT_DEATH(mapper.IdToName(0), ""); + ASSERT_EQ(mapper.NameToId("n1"), 0); + ASSERT_EQ(mapper.IdToName(0), "n1"); + + ASSERT_DEATH(mapper.IdToName(1), ""); + ASSERT_EQ(mapper.NameToId("n2"), 1); + ASSERT_EQ(mapper.IdToName(1), "n2"); + + ASSERT_EQ(mapper.NameToId("n1"), 0); + ASSERT_EQ(mapper.NameToId("n2"), 1); + + ASSERT_EQ(mapper.IdToName(1), "n2"); + ASSERT_EQ(mapper.IdToName(0), "n1"); +}