From c384d6ad082d84041cb71f2cf9c76916b1c89b04 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 13 May 2026 13:37:52 +0200 Subject: [PATCH 1/2] FieldIDMapper + FieldMask --- dbzero/dbzero/dbzero.py | 2 +- src/dbzero/object_model/class.hpp | 4 +- src/dbzero/object_model/class/FieldID.hpp | 6 +- .../object_model/class/FieldIDMapper.cpp | 408 ++++++++++++++++++ .../object_model/class/FieldIDMapper.hpp | 96 +++++ src/dbzero/object_model/class/FieldMask.cpp | 48 +++ src/dbzero/object_model/class/FieldMask.hpp | 53 +++ tests/unit_tests/FieldIDMapperTest.cpp | 317 ++++++++++++++ tests/unit_tests/FieldMaskTest.cpp | 104 +++++ 9 files changed, 1035 insertions(+), 3 deletions(-) create mode 100644 src/dbzero/object_model/class/FieldIDMapper.cpp create mode 100644 src/dbzero/object_model/class/FieldIDMapper.hpp create mode 100644 src/dbzero/object_model/class/FieldMask.cpp create mode 100644 src/dbzero/object_model/class/FieldMask.hpp create mode 100644 tests/unit_tests/FieldIDMapperTest.cpp create mode 100644 tests/unit_tests/FieldMaskTest.cpp diff --git a/dbzero/dbzero/dbzero.py b/dbzero/dbzero/dbzero.py index c9e4f4dc..6bf38564 100644 --- a/dbzero/dbzero/dbzero.py +++ b/dbzero/dbzero/dbzero.py @@ -10,7 +10,7 @@ def load_dynamic(name, path): def __bootstrap__(): global __bootstrap__, __loader__, __file__ - paths = [os.path.join(os.path.split(__file__)[0]), "/src/dev/build/release", "/usr/local/lib/python3/dist-packages/dbzero/"] + paths = [os.path.join(os.path.split(__file__)[0]), "/src/dev/build/", "/usr/local/lib/python3/dist-packages/dbzero/"] __file__ = None for path in paths: if os.path.isdir(path): diff --git a/src/dbzero/object_model/class.hpp b/src/dbzero/object_model/class.hpp index 7c4f3ab6..575c1c31 100644 --- a/src/dbzero/object_model/class.hpp +++ b/src/dbzero/object_model/class.hpp @@ -6,4 +6,6 @@ #include "class/Class.hpp" #include "class/ClassFactory.hpp" #include "class/Field.hpp" -#include "class/Schema.hpp" \ No newline at end of file +#include "class/FieldIDMapper.hpp" +#include "class/FieldMask.hpp" +#include "class/Schema.hpp" diff --git a/src/dbzero/object_model/class/FieldID.hpp b/src/dbzero/object_model/class/FieldID.hpp index 11c66764..bb807896 100644 --- a/src/dbzero/object_model/class/FieldID.hpp +++ b/src/dbzero/object_model/class/FieldID.hpp @@ -17,6 +17,10 @@ namespace db0::object_model public: static constexpr std::uint32_t MAX_INDEX = 0x40000 - 1; // max 2^22 fields static constexpr std::uint32_t MAX_OFFSET = 0x40 - 1; + + static constexpr std::uint32_t getClusterSize() { + return MAX_OFFSET + 1; + } FieldID() = default; FieldID(std::pair loc) { @@ -88,4 +92,4 @@ namespace std std::ostream &operator<<(std::ostream &os, const db0::object_model::FieldID &field_id); -} \ No newline at end of file +} diff --git a/src/dbzero/object_model/class/FieldIDMapper.cpp b/src/dbzero/object_model/class/FieldIDMapper.cpp new file mode 100644 index 00000000..41dd10e8 --- /dev/null +++ b/src/dbzero/object_model/class/FieldIDMapper.cpp @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#include "FieldIDMapper.hpp" +#include +#include + +namespace db0::object_model + +{ + + FieldIDMapper::FieldIDMapper(db0::Memspace &memspace) + : super_t(memspace) + { + } + + FieldIDMapper::FieldIDMapper(db0::mptr ptr) + : super_t(ptr) + { + } + + FieldClusterOffsetMap *FieldIDMapper::getFieldClusterOffsets() const + { + if ((*this)->m_field_cluster_offsets_ptr.isNull()) { + return nullptr; + } + if (m_field_cluster_offsets.isNull()) { + m_field_cluster_offsets = (*this)->m_field_cluster_offsets_ptr(this->getMemspace()); + } + return &m_field_cluster_offsets; + } + + FieldClusterOffsetMap &FieldIDMapper::getOrCreateFieldClusterOffsets() + { + if (m_field_cluster_offsets.isNull()) { + auto &ptr = this->modify().m_field_cluster_offsets_ptr; + if (ptr.isNull()) { + m_field_cluster_offsets = ptr.create(this->getMemspace()); + } else { + m_field_cluster_offsets = ptr(this->getMemspace()); + } + } + return m_field_cluster_offsets; + } + + FieldExceptionOffsetMap *FieldIDMapper::getFieldExceptionOffsets() const + { + if ((*this)->m_field_exception_offsets_ptr.isNull()) { + return nullptr; + } + if (m_field_exception_offsets.isNull()) { + m_field_exception_offsets = (*this)->m_field_exception_offsets_ptr(this->getMemspace()); + } + return &m_field_exception_offsets; + } + + FieldExceptionOffsetMap &FieldIDMapper::getOrCreateFieldExceptionOffsets() + { + if (m_field_exception_offsets.isNull()) { + auto &ptr = this->modify().m_field_exception_offsets_ptr; + if (ptr.isNull()) { + m_field_exception_offsets = ptr.create(this->getMemspace()); + } else { + m_field_exception_offsets = ptr(this->getMemspace()); + } + } + return m_field_exception_offsets; + } + + NameOffsetMap *FieldIDMapper::getNameOffsets() const + { + if ((*this)->m_name_offsets_ptr.isNull()) { + return nullptr; + } + if (m_name_offsets.isNull()) { + m_name_offsets = (*this)->m_name_offsets_ptr(this->getMemspace()); + } + return &m_name_offsets; + } + + NameOffsetMap &FieldIDMapper::getOrCreateNameOffsets() + { + if (m_name_offsets.isNull()) { + auto &ptr = this->modify().m_name_offsets_ptr; + if (ptr.isNull()) { + m_name_offsets = ptr.create(this->getMemspace()); + } else { + m_name_offsets = ptr(this->getMemspace()); + } + } + return m_name_offsets; + } + + std::optional FieldIDMapper::tryGetFieldClusterOffset(std::uint32_t cluster_id) const + { + if (cluster_id == 0) { + return 0; + } + + auto cached = m_field_cluster_offset_cache.find(cluster_id); + if (cached != m_field_cluster_offset_cache.end()) { + return cached->second; + } + + auto field_cluster_offsets = getFieldClusterOffsets(); + if (!field_cluster_offsets) { + return std::nullopt; + } + + auto it = field_cluster_offsets->find(cluster_id); + if (it == field_cluster_offsets->end()) { + return std::nullopt; + } + + auto cluster_offset = (*it).value; + m_field_cluster_offset_cache[cluster_id] = cluster_offset; + return cluster_offset; + } + + std::optional FieldIDMapper::tryGetFieldExceptionOffset(FieldID field_id) const + { + auto key = field_id.getLongIndex(); + auto cached = m_field_exception_offset_cache.find(key); + if (cached != m_field_exception_offset_cache.end()) { + return cached->second; + } + + auto field_exception_offsets = getFieldExceptionOffsets(); + if (!field_exception_offsets) { + return std::nullopt; + } + + auto it = field_exception_offsets->find(key); + if (it == field_exception_offsets->end()) { + return std::nullopt; + } + + auto offset = (*it).value; + m_field_exception_offset_cache[key] = offset; + return offset; + } + + std::optional FieldIDMapper::tryGetNameOffset(const char *field_name) const + { + auto cached = m_name_offset_cache.find(field_name); + if (cached != m_name_offset_cache.end()) { + return cached->second; + } + + auto name_offsets = getNameOffsets(); + if (!name_offsets) { + return std::nullopt; + } + + auto it = name_offsets->find(field_name); + if (it == name_offsets->end()) { + return std::nullopt; + } + + auto offset = static_cast(it->second()); + m_name_offset_cache[field_name] = offset; + return offset; + } + + namespace + { + std::uint32_t alignClusterOffset(std::uint32_t offset) + { + auto remainder = offset % FieldIDMapper::CLUSTER_SIZE; + if (remainder) { + offset += FieldIDMapper::CLUSTER_SIZE - remainder; + } + return offset; + } + } + + std::uint32_t FieldIDMapper::assignNextNameOffset() + { + auto offset = (*this)->m_next_name_offset; + if (offset < CLUSTER_SIZE) { + offset = CLUSTER_SIZE; + } + if (offset % CLUSTER_SIZE == 0 && offset < (*this)->m_next_cluster_offset) { + offset = (*this)->m_next_cluster_offset; + } + this->modify().m_next_name_offset = offset + 1; + return offset; + } + + std::uint32_t FieldIDMapper::assignNextClusterOffset() + { + auto offset = (*this)->m_next_cluster_offset; + if (offset < CLUSTER_SIZE) { + offset = CLUSTER_SIZE; + } + + auto next_name_cluster_boundary = alignClusterOffset((*this)->m_next_name_offset); + if (offset < next_name_cluster_boundary) { + offset = next_name_cluster_boundary; + } + + this->modify().m_next_cluster_offset = offset + CLUSTER_SIZE; + return offset; + } + + std::uint32_t FieldIDMapper::assignFieldOffset(const char *field_name) + { + assert(field_name); + auto maybe_offset = tryGetNameOffset(field_name); + if (maybe_offset) { + return *maybe_offset; + } + + auto offset = assignNextNameOffset(); + auto &name_offsets = getOrCreateNameOffsets(); + auto [it, inserted] = name_offsets.insert_unique(field_name, offset); + if (!inserted) { + offset = static_cast(it->second()); + } + + m_name_offset_cache[field_name] = offset; + return offset; + } + + bool FieldIDMapper::renameField(const char *old_field_name, const char *new_field_name) + { + assert(old_field_name); + assert(new_field_name); + + auto maybe_offset = tryGetNameOffset(old_field_name); + if (!maybe_offset) { + return false; + } + + if (std::strcmp(old_field_name, new_field_name) == 0) { + return true; + } + + if (tryGetNameOffset(new_field_name)) { + return false; + } + + auto name_offsets = getNameOffsets(); + assert(name_offsets); + + auto it = name_offsets->find(old_field_name); + if (it == name_offsets->end()) { + m_name_offset_cache.erase(old_field_name); + return false; + } + + auto offset = *maybe_offset; + name_offsets->erase(it); + auto [new_it, inserted] = name_offsets->insert_unique(new_field_name, offset); + if (!inserted) { + m_name_offset_cache.erase(old_field_name); + m_name_offset_cache[new_field_name] = static_cast(new_it->second()); + return false; + } + + m_name_offset_cache.erase(old_field_name); + m_name_offset_cache[new_field_name] = offset; + return true; + } + + std::optional FieldIDMapper::tryGetAssignedFieldOffset(FieldID field_id) const + { + auto maybe_exception_offset = tryGetFieldExceptionOffset(field_id); + if (maybe_exception_offset) { + return *maybe_exception_offset; + } + + auto maybe_cluster_offset = tryGetFieldClusterOffset(field_id.getIndex()); + if (maybe_cluster_offset) { + return *maybe_cluster_offset + field_id.getOffset(); + } + + return std::nullopt; + } + + std::uint32_t FieldIDMapper::assignFieldOffset(FieldID field_id) + { + auto maybe_offset = tryGetAssignedFieldOffset(field_id); + if (maybe_offset) { + return *maybe_offset; + } + + auto cluster_offset = assignNextClusterOffset(); + auto &field_cluster_offsets = getOrCreateFieldClusterOffsets(); + auto it = field_cluster_offsets.find(field_id.getIndex()); + if (it != field_cluster_offsets.end()) { + cluster_offset = (*it).value; + } else { + field_cluster_offsets.insert({ field_id.getIndex(), cluster_offset }); + } + + m_field_cluster_offset_cache[field_id.getIndex()] = cluster_offset; + return cluster_offset + field_id.getOffset(); + } + + std::uint32_t FieldIDMapper::assignFieldExceptionOffset(FieldID field_id, std::uint32_t offset) + { + auto &field_exception_offsets = getOrCreateFieldExceptionOffsets(); + auto it = field_exception_offsets.find(field_id.getLongIndex()); + if (it != field_exception_offsets.end()) { + offset = (*it).value; + } else { + field_exception_offsets.insert({ field_id.getLongIndex(), offset }); + } + + m_field_exception_offset_cache[field_id.getLongIndex()] = offset; + return offset; + } + + std::uint32_t FieldIDMapper::onFieldIDAssigned(const char *field_name, FieldID field_id) + { + assert(field_name); + auto maybe_offset = tryGetNameOffset(field_name); + if (!maybe_offset) { + return assignFieldOffset(field_id); + } + + auto offset = *maybe_offset; + auto name_offsets = getNameOffsets(); + if (name_offsets) { + auto it = name_offsets->find(field_name); + if (it != name_offsets->end()) { + name_offsets->erase(it); + } + } + + m_name_offset_cache.erase(field_name); + + auto maybe_exception_offset = tryGetFieldExceptionOffset(field_id); + if (maybe_exception_offset) { + return *maybe_exception_offset; + } + + return assignFieldExceptionOffset(field_id, offset); + } + + std::uint32_t FieldIDMapper::getFieldIDMappingCount() const + { + return getFieldIDClusterMappingCount() + getFieldIDExceptionMappingCount(); + } + + std::uint32_t FieldIDMapper::getFieldIDClusterMappingCount() const + { + auto field_cluster_offsets = getFieldClusterOffsets(); + return field_cluster_offsets ? field_cluster_offsets->size() : 0; + } + + std::uint32_t FieldIDMapper::getFieldIDExceptionMappingCount() const + { + auto field_exception_offsets = getFieldExceptionOffsets(); + return field_exception_offsets ? field_exception_offsets->size() : 0; + } + + std::uint32_t FieldIDMapper::getNameMappingCount() const + { + auto name_offsets = getNameOffsets(); + return name_offsets ? name_offsets->size() : 0; + } + + bool FieldIDMapper::hasFieldIDClusterMappingCollection() const + { + return !(*this)->m_field_cluster_offsets_ptr.isNull(); + } + + bool FieldIDMapper::hasFieldIDExceptionMappingCollection() const + { + return !(*this)->m_field_exception_offsets_ptr.isNull(); + } + + bool FieldIDMapper::hasNameMappingCollection() const + { + return !(*this)->m_name_offsets_ptr.isNull(); + } + + void FieldIDMapper::detach() const + { + if (!m_field_cluster_offsets.isNull()) { + m_field_cluster_offsets.detach(); + } + if (!m_field_exception_offsets.isNull()) { + m_field_exception_offsets.detach(); + } + if (!m_name_offsets.isNull()) { + m_name_offsets.detach(); + } + super_t::detach(); + } + + void FieldIDMapper::commit() const + { + if (!m_field_cluster_offsets.isNull()) { + m_field_cluster_offsets.commit(); + } + if (!m_field_exception_offsets.isNull()) { + m_field_exception_offsets.commit(); + } + if (!m_name_offsets.isNull()) { + m_name_offsets.commit(); + } + super_t::commit(); + } + +} diff --git a/src/dbzero/object_model/class/FieldIDMapper.hpp b/src/dbzero/object_model/class/FieldIDMapper.hpp new file mode 100644 index 00000000..06739ae3 --- /dev/null +++ b/src/dbzero/object_model/class/FieldIDMapper.hpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#pragma once + +#include "FieldID.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace db0::object_model + +{ + + using FieldOffsetMapItem = db0::key_value; + using FieldClusterOffsetMap = db0::v_bindex; + using FieldExceptionOffsetMap = db0::v_bindex; + using NameOffsetMap = db0::v_map, db0::o_string::comp_t>; + +DB0_PACKED_BEGIN + struct DB0_PACKED_ATTR o_field_id_mapper: public db0::o_fixed_versioned + { + db0_ptr m_field_cluster_offsets_ptr; + db0_ptr m_field_exception_offsets_ptr; + db0_ptr m_name_offsets_ptr; + std::uint32_t m_next_name_offset = FieldID::getClusterSize(); + std::uint32_t m_next_cluster_offset = FieldID::getClusterSize(); + + o_field_id_mapper() = default; + }; +DB0_PACKED_END + + class FieldIDMapper: public db0::v_object + { + public: + using super_t = db0::v_object; + + FieldIDMapper() = default; + FieldIDMapper(db0::Memspace &); + FieldIDMapper(db0::mptr); + + static constexpr std::uint32_t CLUSTER_SIZE = FieldID::getClusterSize(); + + // Retrieve existing or assign a new field offset. + std::uint32_t assignFieldOffset(FieldID); + std::uint32_t assignFieldOffset(const char *field_name); + + std::uint32_t onFieldIDAssigned(const char *field_name, FieldID); + bool renameField(const char *old_field_name, const char *new_field_name); + + std::optional tryGetAssignedFieldOffset(FieldID) const; + + void detach() const; + void commit() const; + + protected: + std::uint32_t getFieldIDMappingCount() const; + std::uint32_t getFieldIDClusterMappingCount() const; + std::uint32_t getFieldIDExceptionMappingCount() const; + std::uint32_t getNameMappingCount() const; + bool hasFieldIDClusterMappingCollection() const; + bool hasFieldIDExceptionMappingCollection() const; + bool hasNameMappingCollection() const; + + private: + mutable FieldClusterOffsetMap m_field_cluster_offsets; + mutable FieldExceptionOffsetMap m_field_exception_offsets; + mutable NameOffsetMap m_name_offsets; + + mutable std::unordered_map m_field_cluster_offset_cache; + mutable std::unordered_map m_field_exception_offset_cache; + mutable std::unordered_map m_name_offset_cache; + + std::optional tryGetFieldClusterOffset(std::uint32_t cluster_id) const; + std::optional tryGetFieldExceptionOffset(FieldID) const; + std::optional tryGetNameOffset(const char *field_name) const; + FieldClusterOffsetMap *getFieldClusterOffsets() const; + FieldClusterOffsetMap &getOrCreateFieldClusterOffsets(); + FieldExceptionOffsetMap *getFieldExceptionOffsets() const; + FieldExceptionOffsetMap &getOrCreateFieldExceptionOffsets(); + NameOffsetMap *getNameOffsets() const; + NameOffsetMap &getOrCreateNameOffsets(); + std::uint32_t assignNextNameOffset(); + std::uint32_t assignNextClusterOffset(); + std::uint32_t assignFieldExceptionOffset(FieldID, std::uint32_t offset); + }; + +} diff --git a/src/dbzero/object_model/class/FieldMask.cpp b/src/dbzero/object_model/class/FieldMask.cpp new file mode 100644 index 00000000..9ce6c83f --- /dev/null +++ b/src/dbzero/object_model/class/FieldMask.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#include "FieldMask.hpp" + +DEFINE_ENUM_VALUES(db0::object_model::FieldMaskOptions, "create", "read", "update", "delete") + +namespace db0::object_model + +{ + + void FieldMask::setMask(std::uint32_t field_offset, FieldMaskFlags mask) + { + auto slot = getSlot(field_offset); + auto shift = getShift(field_offset); + auto value = static_cast(mask.value() & VALUE_MASK); + auto slot_value = slot < this->size() ? this->getItem(slot) : std::uint8_t(0); + slot_value &= ~(VALUE_MASK << shift); + slot_value |= value << shift; + this->setItem(slot, slot_value); + } + + std::optional FieldMask::getMask(std::uint32_t field_offset) const + { + auto slot = getSlot(field_offset); + if (slot >= this->size()) { + return FieldMaskFlags {}; + } + + return decode(this->getItem(slot), getShift(field_offset)); + } + + std::uint64_t FieldMask::getSlot(std::uint32_t field_offset) + { + return field_offset / 2; + } + + unsigned int FieldMask::getShift(std::uint32_t field_offset) + { + return field_offset % 2 == 0 ? 0 : 4; + } + + FieldMaskFlags FieldMask::decode(std::uint8_t slot_value, unsigned int shift) + { + return FieldMaskFlags::fromValue((slot_value >> shift) & VALUE_MASK); + } + +} diff --git a/src/dbzero/object_model/class/FieldMask.hpp b/src/dbzero/object_model/class/FieldMask.hpp new file mode 100644 index 00000000..ce75d6aa --- /dev/null +++ b/src/dbzero/object_model/class/FieldMask.hpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#pragma once + +#include +#include +#include +#include + +namespace db0::object_model + +{ + + enum class FieldMaskOptions : std::uint8_t + { + CREATE = 0x01, + READ = 0x02, + UPDATE = 0x04, + DELETE = 0x08, + }; + +} + +DECLARE_ENUM_VALUES(db0::object_model::FieldMaskOptions, 4) + +namespace db0::object_model + +{ + + using FieldMaskFlags = db0::FlagSet; + + class FieldMask: public db0::v_bvector + { + public: + using super_t = db0::v_bvector; + using super_t::super_t; + + FieldMask() = default; + + void setMask(std::uint32_t field_offset, FieldMaskFlags); + std::optional getMask(std::uint32_t field_offset) const; + + private: + static constexpr std::uint8_t VALUE_MASK = 0x0f; + static_assert(db0::FlagSetLimits::count() == 4); + + static std::uint64_t getSlot(std::uint32_t field_offset); + static unsigned int getShift(std::uint32_t field_offset); + static FieldMaskFlags decode(std::uint8_t slot_value, unsigned int shift); + }; + +} diff --git a/tests/unit_tests/FieldIDMapperTest.cpp b/tests/unit_tests/FieldIDMapperTest.cpp new file mode 100644 index 00000000..9971b500 --- /dev/null +++ b/tests/unit_tests/FieldIDMapperTest.cpp @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#include +#include +#include + +namespace tests + +{ + + using namespace db0; + using namespace db0::object_model; + + class TestFieldIDMapper: public FieldIDMapper + { + public: + using FieldIDMapper::FieldIDMapper; + using FieldIDMapper::getFieldIDClusterMappingCount; + using FieldIDMapper::getFieldIDExceptionMappingCount; + using FieldIDMapper::getFieldIDMappingCount; + using FieldIDMapper::getNameMappingCount; + using FieldIDMapper::hasFieldIDClusterMappingCollection; + using FieldIDMapper::hasFieldIDExceptionMappingCollection; + using FieldIDMapper::hasNameMappingCollection; + }; + + class FieldIDMapperTest: public MemspaceTestBase + { + }; + + TEST_F( FieldIDMapperTest , testClusterZeroFieldIDOffsetIsImplicit ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto field_id = FieldID::fromIndex(0, 3); + + ASSERT_FALSE(mapper.hasFieldIDClusterMappingCollection()); + ASSERT_FALSE(mapper.hasFieldIDExceptionMappingCollection()); + ASSERT_FALSE(mapper.hasNameMappingCollection()); + ASSERT_EQ(mapper.assignFieldOffset(field_id), field_id.getOffset()); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(field_id), field_id.getOffset()); + ASSERT_EQ(mapper.getFieldIDMappingCount(), 0u); + ASSERT_EQ(mapper.getFieldIDClusterMappingCount(), 0u); + ASSERT_EQ(mapper.getFieldIDExceptionMappingCount(), 0u); + ASSERT_FALSE(mapper.hasFieldIDClusterMappingCollection()); + ASSERT_FALSE(mapper.hasFieldIDExceptionMappingCollection()); + ASSERT_FALSE(mapper.hasNameMappingCollection()); + } + + TEST_F( FieldIDMapperTest , testInitialFieldIDsInClusterZeroDoNotCreatePersistentCollections ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + + for (std::uint32_t i = 0; i < FieldIDMapper::CLUSTER_SIZE; ++i) { + auto field_id = FieldID::fromIndex(0, i); + ASSERT_EQ(mapper.assignFieldOffset(field_id), i); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(field_id), i); + } + + ASSERT_EQ(mapper.getFieldIDMappingCount(), 0u); + ASSERT_EQ(mapper.getNameMappingCount(), 0u); + ASSERT_FALSE(mapper.hasFieldIDClusterMappingCollection()); + ASSERT_FALSE(mapper.hasFieldIDExceptionMappingCollection()); + ASSERT_FALSE(mapper.hasNameMappingCollection()); + } + + TEST_F( FieldIDMapperTest , testLargeFieldIDOffsetIsPersistedCompactly ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto field_id = FieldID::fromIndex(12, 3); + auto sibling_field_id = FieldID::fromIndex(12, 4); + + auto offset = mapper.assignFieldOffset(field_id); + + ASSERT_NE(offset, field_id.getLongIndex()); + ASSERT_EQ(offset, FieldIDMapper::CLUSTER_SIZE + field_id.getOffset()); + ASSERT_EQ(mapper.assignFieldOffset(sibling_field_id), offset + 1); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(field_id), offset); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(sibling_field_id), offset + 1); + ASSERT_EQ(mapper.getFieldIDMappingCount(), 1u); + ASSERT_EQ(mapper.getFieldIDClusterMappingCount(), 1u); + ASSERT_EQ(mapper.getFieldIDExceptionMappingCount(), 0u); + } + + TEST_F( FieldIDMapperTest , testNamedFieldOffsetIsStable ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + + auto offset = mapper.assignFieldOffset("status"); + + ASSERT_EQ(mapper.assignFieldOffset("status"), offset); + ASSERT_EQ(offset, FieldIDMapper::CLUSTER_SIZE); + ASSERT_EQ(mapper.getNameMappingCount(), 1u); + } + + TEST_F( FieldIDMapperTest , testRenameFieldMovesExistingNameMapping ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + + auto offset = mapper.assignFieldOffset("status"); + + ASSERT_TRUE(mapper.renameField("status", "state")); + ASSERT_EQ(mapper.assignFieldOffset("state"), offset); + ASSERT_EQ(mapper.assignFieldOffset("status"), offset + 1); + ASSERT_EQ(mapper.getNameMappingCount(), 2u); + } + + TEST_F( FieldIDMapperTest , testRenameFieldDoesNothingWhenOldNameIsMissing ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + + ASSERT_FALSE(mapper.renameField("status", "state")); + + ASSERT_EQ(mapper.getNameMappingCount(), 0u); + ASSERT_FALSE(mapper.hasNameMappingCollection()); + } + + TEST_F( FieldIDMapperTest , testRenameFieldDoesNotOverwriteExistingNameMapping ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + + auto status_offset = mapper.assignFieldOffset("status"); + auto state_offset = mapper.assignFieldOffset("state"); + + ASSERT_FALSE(mapper.renameField("status", "state")); + ASSERT_EQ(mapper.assignFieldOffset("status"), status_offset); + ASSERT_EQ(mapper.assignFieldOffset("state"), state_offset); + ASSERT_EQ(mapper.getNameMappingCount(), 2u); + } + + TEST_F( FieldIDMapperTest , testNameFirstAssignmentStillUsesImplicitClusterZero ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto name_offset = mapper.assignFieldOffset("status"); + auto small_field_id = FieldID::fromIndex(0, 3); + + auto field_offset = mapper.assignFieldOffset(small_field_id); + + ASSERT_EQ(name_offset, FieldIDMapper::CLUSTER_SIZE); + ASSERT_EQ(field_offset, small_field_id.getOffset()); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(small_field_id), field_offset); + ASSERT_EQ(mapper.getFieldIDClusterMappingCount(), 0u); + ASSERT_EQ(mapper.getFieldIDExceptionMappingCount(), 0u); + } + + TEST_F( FieldIDMapperTest , testImplicitClusterZeroFieldIDFirstAssignmentReservesClusterZeroRange ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto small_field_id = FieldID::fromIndex(0, 3); + + auto field_offset = mapper.assignFieldOffset(small_field_id); + auto name_offset = mapper.assignFieldOffset("status"); + + ASSERT_EQ(field_offset, small_field_id.getOffset()); + ASSERT_EQ(name_offset, FieldIDMapper::CLUSTER_SIZE); + ASSERT_EQ(mapper.getFieldIDMappingCount(), 0u); + ASSERT_EQ(mapper.getNameMappingCount(), 1u); + } + + TEST_F( FieldIDMapperTest , testDirectFieldIDAssignmentAfterNameMappingIsPersistedForNonZeroCluster ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto name_offset = mapper.assignFieldOffset("status"); + auto field_id = FieldID::fromIndex(1, 0); + + auto field_offset = mapper.assignFieldOffset(field_id); + + ASSERT_NE(field_offset, name_offset); + ASSERT_EQ(mapper.assignFieldOffset(field_id), field_offset); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(field_id), field_offset); + ASSERT_EQ(mapper.getFieldIDMappingCount(), 1u); + ASSERT_EQ(mapper.getFieldIDClusterMappingCount(), 1u); + ASSERT_EQ(mapper.getFieldIDExceptionMappingCount(), 0u); + } + + TEST_F( FieldIDMapperTest , testNamedMappingCanBeMigratedToFieldIDMapping ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto name_offset = mapper.assignFieldOffset("status"); + auto field_id = FieldID::fromIndex(100, 0); + + ASSERT_EQ(mapper.onFieldIDAssigned("status", field_id), name_offset); + + ASSERT_EQ(mapper.assignFieldOffset(field_id), name_offset); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(field_id), name_offset); + ASSERT_EQ(mapper.getNameMappingCount(), 0u); + ASSERT_EQ(mapper.getFieldIDMappingCount(), 1u); + } + + TEST_F( FieldIDMapperTest , testNamedMappingMigrationCreatesExceptionMappingForSingleFieldID ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto other_name_offset = mapper.assignFieldOffset("type"); + auto name_offset = mapper.assignFieldOffset("status"); + auto field_id = FieldID::fromIndex(100, 3); + auto sibling_field_id = FieldID::fromIndex(100, 4); + + ASSERT_EQ(mapper.onFieldIDAssigned("status", field_id), name_offset); + + ASSERT_EQ(mapper.assignFieldOffset(field_id), name_offset); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(field_id), name_offset); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(sibling_field_id), std::nullopt); + ASSERT_EQ(mapper.getNameMappingCount(), 1u); + ASSERT_EQ(mapper.getFieldIDMappingCount(), 1u); + ASSERT_EQ(mapper.getFieldIDClusterMappingCount(), 0u); + ASSERT_EQ(mapper.getFieldIDExceptionMappingCount(), 1u); + ASSERT_EQ(other_name_offset, FieldIDMapper::CLUSTER_SIZE); + } + + TEST_F( FieldIDMapperTest , testDirectFieldIDAssignmentCreatesClusterMappingAfterExceptionMapping ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto name_offset = mapper.assignFieldOffset("status"); + auto exception_field_id = FieldID::fromIndex(100, 3); + auto sibling_field_id = FieldID::fromIndex(100, 4); + + ASSERT_EQ(mapper.onFieldIDAssigned("status", exception_field_id), name_offset); + auto sibling_offset = mapper.assignFieldOffset(sibling_field_id); + + ASSERT_EQ(mapper.assignFieldOffset(exception_field_id), name_offset); + ASSERT_NE(sibling_offset, name_offset + 1); + ASSERT_EQ(mapper.assignFieldOffset(sibling_field_id), sibling_offset); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(sibling_field_id), sibling_offset); + ASSERT_EQ(mapper.getFieldIDMappingCount(), 2u); + ASSERT_EQ(mapper.getFieldIDClusterMappingCount(), 1u); + ASSERT_EQ(mapper.getFieldIDExceptionMappingCount(), 1u); + } + + TEST_F( FieldIDMapperTest , testClusterAndNameAllocationsDoNotOverlap ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto first_cluster_field_id = FieldID::fromIndex(100, 3); + auto first_cluster_sibling_field_id = FieldID::fromIndex(100, 4); + auto second_cluster_field_id = FieldID::fromIndex(101, 0); + + auto first_cluster_offset = mapper.assignFieldOffset(first_cluster_field_id); + auto name_offset = mapper.assignFieldOffset("status"); + auto second_cluster_offset = mapper.assignFieldOffset(second_cluster_field_id); + + ASSERT_EQ(mapper.assignFieldOffset(first_cluster_sibling_field_id), first_cluster_offset + 1); + ASSERT_EQ(name_offset, FieldIDMapper::CLUSTER_SIZE * 2); + ASSERT_EQ(second_cluster_offset, name_offset + FieldIDMapper::CLUSTER_SIZE); + ASSERT_EQ(mapper.getNameMappingCount(), 1u); + ASSERT_EQ(mapper.getFieldIDClusterMappingCount(), 2u); + ASSERT_EQ(mapper.getFieldIDExceptionMappingCount(), 0u); + } + + TEST_F( FieldIDMapperTest , testNameOffsetsAreConsecutiveUntilClusterBoundary ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + + for (std::uint32_t i = 0; i < FieldIDMapper::CLUSTER_SIZE; ++i) { + auto name = std::string("field_") + std::to_string(i); + ASSERT_EQ(mapper.assignFieldOffset(name.c_str()), FieldIDMapper::CLUSTER_SIZE + i); + } + } + + TEST_F( FieldIDMapperTest , testNameOffsetsSkipReservedClusterRanges ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto field_id = FieldID::fromIndex(100, 0); + + ASSERT_EQ(mapper.assignFieldOffset(field_id), FieldIDMapper::CLUSTER_SIZE); + ASSERT_EQ(mapper.assignFieldOffset("status"), FieldIDMapper::CLUSTER_SIZE * 2); + ASSERT_EQ(mapper.assignFieldOffset("type"), FieldIDMapper::CLUSTER_SIZE * 2 + 1); + } + + TEST_F( FieldIDMapperTest , testNameOffsetsRemainConsecutiveWhenClusterAllocatedBeforeBoundary ) + { + auto memspace = getMemspace(); + TestFieldIDMapper mapper(memspace); + auto field_id = FieldID::fromIndex(100, 0); + + ASSERT_EQ(mapper.assignFieldOffset("status"), FieldIDMapper::CLUSTER_SIZE); + ASSERT_EQ(mapper.assignFieldOffset(field_id), FieldIDMapper::CLUSTER_SIZE * 2); + ASSERT_EQ(mapper.assignFieldOffset("type"), FieldIDMapper::CLUSTER_SIZE + 1); + } + + TEST_F( FieldIDMapperTest , testMapperPersistsMappings ) + { + auto memspace = getMemspace(); + Address address; + std::uint32_t name_offset; + std::uint32_t field_offset; + auto field_id = FieldID::fromIndex(100, 0); + + { + TestFieldIDMapper mapper(memspace); + name_offset = mapper.assignFieldOffset("status"); + field_offset = mapper.onFieldIDAssigned("status", field_id); + address = mapper.getAddress(); + } + + TestFieldIDMapper mapper(memspace.myPtr(address)); + + ASSERT_EQ(mapper.assignFieldOffset(field_id), field_offset); + ASSERT_EQ(mapper.tryGetAssignedFieldOffset(field_id), field_offset); + ASSERT_EQ(field_offset, name_offset); + } + +} diff --git a/tests/unit_tests/FieldMaskTest.cpp b/tests/unit_tests/FieldMaskTest.cpp new file mode 100644 index 00000000..fd02f0f2 --- /dev/null +++ b/tests/unit_tests/FieldMaskTest.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#include +#include +#include + +namespace tests + +{ + + using namespace db0::object_model; + + class FieldMaskTest: public MemspaceTestBase + { + }; + + TEST_F( FieldMaskTest , testMaskCanBeAssignedAndRetrievedByFieldOffset ) + { + auto memspace = getMemspace(); + FieldMask mask(memspace); + + mask.setMask(42, { FieldMaskOptions::CREATE, FieldMaskOptions::READ }); + + auto result = mask.getMask(42); + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE((*result)[FieldMaskOptions::CREATE]); + ASSERT_TRUE((*result)[FieldMaskOptions::READ]); + ASSERT_FALSE((*result)[FieldMaskOptions::UPDATE]); + ASSERT_FALSE((*result)[FieldMaskOptions::DELETE]); + } + + TEST_F( FieldMaskTest , testMissingOffsetReturnsDefaultMask ) + { + auto memspace = getMemspace(); + FieldMask mask(memspace); + + mask.setMask(7, { FieldMaskOptions::UPDATE }); + + auto before = mask.getMask(6); + auto after = mask.getMask(8); + + ASSERT_TRUE(before.has_value()); + ASSERT_TRUE(before->none()); + ASSERT_TRUE(after.has_value()); + ASSERT_TRUE(after->none()); + } + + TEST_F( FieldMaskTest , testAssignedEmptyMaskReturnsDefaultMask ) + { + auto memspace = getMemspace(); + FieldMask mask(memspace); + + mask.setMask(3, {}); + + auto result = mask.getMask(3); + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(result->none()); + } + + TEST_F( FieldMaskTest , testTwoMasksAreStoredInOneByteSlot ) + { + auto memspace = getMemspace(); + FieldMask mask(memspace); + + mask.setMask(0, { FieldMaskOptions::CREATE }); + mask.setMask(1, { FieldMaskOptions::DELETE }); + + ASSERT_EQ(mask.size(), 1u); + ASSERT_EQ(mask.getItem(0), 0x81); + ASSERT_TRUE((*mask.getMask(0))[FieldMaskOptions::CREATE]); + ASSERT_TRUE((*mask.getMask(1))[FieldMaskOptions::DELETE]); + } + + TEST_F( FieldMaskTest , testMaskCanBeOverwritten ) + { + auto memspace = getMemspace(); + FieldMask mask(memspace); + + mask.setMask(5, { FieldMaskOptions::CREATE }); + mask.setMask(5, { FieldMaskOptions::DELETE }); + + auto result = mask.getMask(5); + ASSERT_TRUE(result.has_value()); + ASSERT_FALSE((*result)[FieldMaskOptions::CREATE]); + ASSERT_TRUE((*result)[FieldMaskOptions::DELETE]); + } + + TEST_F( FieldMaskTest , testMaskCanBeReadFromReopenedInstance ) + { + auto memspace = getMemspace(); + FieldMask mask(memspace); + auto address = mask.getAddress(); + + mask.setMask(1024, { FieldMaskOptions::READ, FieldMaskOptions::UPDATE }); + + FieldMask reopened(memspace.myPtr(address)); + auto result = reopened.getMask(1024); + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE((*result)[FieldMaskOptions::READ]); + ASSERT_TRUE((*result)[FieldMaskOptions::UPDATE]); + } + +} From c307f3b52209f5b3daebc9c070dab29d2e27b3ce Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 13 May 2026 14:41:46 +0200 Subject: [PATCH 2/2] FieldMaskManager --- AGENTS.md | 7 ++ src/dbzero/object_model/class/FieldMask.cpp | 47 +++++++++++ src/dbzero/object_model/class/FieldMask.hpp | 32 ++++++++ tests/unit_tests/FieldMaskManagerTest.cpp | 88 +++++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 tests/unit_tests/FieldMaskManagerTest.cpp diff --git a/AGENTS.md b/AGENTS.md index 540b5aea..d1832399 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,13 @@ Never mark a task done while tests are failing. ## Implementation notes +### v_object constructor conventions + +Types derived from `v_object` should follow the project-wide constructor pattern: + +- New durable instances are constructed from `Memspace &` plus any type-specific creation arguments. +- Existing durable instances are reopened from `mptr` plus any type-specific runtime dependencies. + ### MorphingBIndex: address and type can change on mutation A `MorphingBIndex` does not behave like a typical container. On mutation (`insert`, `erase`) it may morph into a different internal storage variant (itty / array_2..4 / vector / bindex), and the morph can change both its **address** and its **type**. diff --git a/src/dbzero/object_model/class/FieldMask.cpp b/src/dbzero/object_model/class/FieldMask.cpp index 9ce6c83f..099f712f 100644 --- a/src/dbzero/object_model/class/FieldMask.cpp +++ b/src/dbzero/object_model/class/FieldMask.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2025 DBZero Software sp. z o.o. #include "FieldMask.hpp" +#include DEFINE_ENUM_VALUES(db0::object_model::FieldMaskOptions, "create", "read", "update", "delete") @@ -9,6 +10,11 @@ namespace db0::object_model { + FieldMask::FieldMask(db0::Memspace &memspace, db0::Address address) + : super_t(memspace.myPtr(address)) + { + } + void FieldMask::setMask(std::uint32_t field_offset, FieldMaskFlags mask) { auto slot = getSlot(field_offset); @@ -45,4 +51,45 @@ namespace db0::object_model return FieldMaskFlags::fromValue((slot_value >> shift) & VALUE_MASK); } + FieldMaskManager::FieldMaskManager(db0::Memspace &memspace, db0::VObjectCache &cache) + : super_t(memspace) + , m_cache(&cache) + { + assert(m_cache); + } + + FieldMaskManager::FieldMaskManager(db0::mptr ptr, db0::VObjectCache &cache) + : super_t(ptr, ptr.getPageSize()) + , m_cache(&cache) + { + assert(m_cache); + } + + std::shared_ptr FieldMaskManager::createFieldMask(std::uint64_t account_id) + { + auto result = tryGetFieldMask(account_id); + if (result) { + return result; + } + + result = m_cache->create(true); + super_t::insert(FieldMaskManagerItem(account_id, result->getAddress())); + return result; + } + + std::shared_ptr FieldMaskManager::tryGetFieldMask(std::uint64_t account_id) const + { + auto it = super_t::find(account_id); + if (it == super_t::end()) { + return nullptr; + } + + return fieldMaskFromAddress((*it).value); + } + + std::shared_ptr FieldMaskManager::fieldMaskFromAddress(db0::Address address) const + { + return m_cache->findOrCreate(address, true, address); + } + } diff --git a/src/dbzero/object_model/class/FieldMask.hpp b/src/dbzero/object_model/class/FieldMask.hpp index ce75d6aa..61b14ab1 100644 --- a/src/dbzero/object_model/class/FieldMask.hpp +++ b/src/dbzero/object_model/class/FieldMask.hpp @@ -4,10 +4,22 @@ #pragma once #include +#include #include +#include +#include #include +#include #include +namespace db0 + +{ + + class VObjectCache; + +} + namespace db0::object_model { @@ -37,6 +49,7 @@ namespace db0::object_model using super_t::super_t; FieldMask() = default; + FieldMask(db0::Memspace &, db0::Address); void setMask(std::uint32_t field_offset, FieldMaskFlags); std::optional getMask(std::uint32_t field_offset) const; @@ -50,4 +63,23 @@ namespace db0::object_model static FieldMaskFlags decode(std::uint8_t slot_value, unsigned int shift); }; + using FieldMaskManagerItem = db0::key_value; + + class FieldMaskManager: public db0::v_bindex + { + public: + using super_t = db0::v_bindex; + + FieldMaskManager(db0::Memspace &, db0::VObjectCache &); + FieldMaskManager(db0::mptr, db0::VObjectCache &); + + std::shared_ptr createFieldMask(std::uint64_t account_id); + std::shared_ptr tryGetFieldMask(std::uint64_t account_id) const; + + private: + db0::VObjectCache *m_cache = nullptr; + + std::shared_ptr fieldMaskFromAddress(db0::Address address) const; + }; + } diff --git a/tests/unit_tests/FieldMaskManagerTest.cpp b/tests/unit_tests/FieldMaskManagerTest.cpp new file mode 100644 index 00000000..cbe28efe --- /dev/null +++ b/tests/unit_tests/FieldMaskManagerTest.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#include +#include +#include +#include +#include + +namespace tests + +{ + + using namespace db0::object_model; + + class FieldMaskManagerTest: public testing::Test + { + public: + static constexpr const char *prefix_name = "field-mask-manager-test"; + static constexpr const char *file_name = "field-mask-manager-test.db0"; + + void SetUp() override { + db0::tests::drop(file_name); + } + + void TearDown() override { + db0::tests::drop(file_name); + } + }; + + TEST_F( FieldMaskManagerTest , testCreateFieldMaskCreatesAndRetrievesByAccountID ) + { + db0::Workspace workspace("", {}, {}, {}, {}, db0::object_model::initializer()); + auto fixture = workspace.getFixture(prefix_name); + FieldMaskManager manager(*fixture, fixture->getVObjectCache()); + + auto mask = manager.createFieldMask(123); + mask->setMask(2, { FieldMaskOptions::READ }); + + auto result = manager.tryGetFieldMask(123); + ASSERT_EQ(result, mask); + ASSERT_TRUE((*result->getMask(2))[FieldMaskOptions::READ]); + workspace.close(); + } + + TEST_F( FieldMaskManagerTest , testTryGetFieldMaskReturnsNullptrForMissingAccountID ) + { + db0::Workspace workspace("", {}, {}, {}, {}, db0::object_model::initializer()); + auto fixture = workspace.getFixture(prefix_name); + FieldMaskManager manager(*fixture, fixture->getVObjectCache()); + + ASSERT_EQ(manager.tryGetFieldMask(999), nullptr); + workspace.close(); + } + + TEST_F( FieldMaskManagerTest , testCreateFieldMaskReturnsExistingFieldMask ) + { + db0::Workspace workspace("", {}, {}, {}, {}, db0::object_model::initializer()); + auto fixture = workspace.getFixture(prefix_name); + FieldMaskManager manager(*fixture, fixture->getVObjectCache()); + + auto first = manager.createFieldMask(123); + auto second = manager.createFieldMask(123); + + ASSERT_EQ(second, first); + ASSERT_EQ(manager.size(), 1u); + workspace.close(); + } + + TEST_F( FieldMaskManagerTest , testFieldMaskCanBeRetrievedFromReopenedManager ) + { + db0::Workspace workspace("", {}, {}, {}, {}, db0::object_model::initializer()); + auto fixture = workspace.getFixture(prefix_name); + FieldMaskManager manager(*fixture, fixture->getVObjectCache()); + auto manager_address = manager.getAddress(); + + auto mask = manager.createFieldMask(123); + mask->setMask(4, { FieldMaskOptions::CREATE, FieldMaskOptions::UPDATE }); + + FieldMaskManager reopened(fixture->myPtr(manager_address), fixture->getVObjectCache()); + auto result = reopened.tryGetFieldMask(123); + ASSERT_TRUE(result != nullptr); + ASSERT_TRUE((*result->getMask(4))[FieldMaskOptions::CREATE]); + ASSERT_TRUE((*result->getMask(4))[FieldMaskOptions::UPDATE]); + workspace.close(); + } + +}