From cec0c2b0b7a745169e22735d865293141630c830 Mon Sep 17 00:00:00 2001 From: Adrian Zawadzki Date: Fri, 15 May 2026 21:01:21 +0200 Subject: [PATCH 1/5] feature(protect_fields): Added inheritance handling --- dbzero/dbzero/dbzero.pyi | 10 +- dbzero/dbzero/memo.py | 1 + python_tests/test_memo_protect_fields.py | 148 ++++++++++++++++++ src/dbzero/bindings/python/Memo.cpp | 19 ++- src/dbzero/bindings/python/PyAPI.cpp | 16 +- src/dbzero/object_model/class/Class.cpp | 44 ++++-- .../object_model/class/ClassFactory.cpp | 32 +++- .../object_model/class/FieldIDMapper.cpp | 6 + .../object_model/class/FieldIDMapper.hpp | 1 + 9 files changed, 248 insertions(+), 29 deletions(-) diff --git a/dbzero/dbzero/dbzero.pyi b/dbzero/dbzero/dbzero.pyi index dc2df94c..e544d584 100644 --- a/dbzero/dbzero/dbzero.pyi +++ b/dbzero/dbzero/dbzero.pyi @@ -610,8 +610,9 @@ def rename_field(class_obj: type, from_name: str, to_name: str) -> None: def set_field_access(class_obj: type, account_id: Union[int, Sequence[int]], mode: Tuple[EnumValue, ...], *fields: str) -> None: """Set protected-field access flags for one or more fields of a memo class. - The memo class must be declared with ``protect_fields=True``. Pass an empty - ``mode`` tuple to clear all access flags for the specified account and fields. + The memo class must be declared with ``protect_fields=True`` or inherit it + from a protected memo base. Pass an empty ``mode`` tuple to clear all access + flags for the specified account and fields. """ ... @@ -622,8 +623,9 @@ def get_field_access(class_obj: type, account_id: int) -> Iterable[Tuple[str, Tu def reset_protect_fields(class_obj: type) -> None: """Clear the persisted protect_fields flag for a memo class. - The memo type must no longer be decorated with ``protect_fields=True``. - Remove the argument or set it to ``False`` before calling this function. + The memo type must no longer be decorated with ``protect_fields=True`` and + must not inherit protected fields from a protected memo base. Remove the + argument or set it to ``False`` before calling this function. """ ... diff --git a/dbzero/dbzero/memo.py b/dbzero/dbzero/memo.py index 0f9f36d3..3c123963 100644 --- a/dbzero/dbzero/memo.py +++ b/dbzero/dbzero/memo.py @@ -172,6 +172,7 @@ def memo(cls: Optional[type] = None, **kwargs) -> type: If True, the persistent class is marked for field protection. Once the class is materialized, removing this argument from the Python definition does not clear the persisted flag; use reset_protect_fields on the dbzero Class object instead. + Derived memo classes inherit field protection and cannot disable it. Returns ------- diff --git a/python_tests/test_memo_protect_fields.py b/python_tests/test_memo_protect_fields.py index 74611f76..3d16c9f3 100644 --- a/python_tests/test_memo_protect_fields.py +++ b/python_tests/test_memo_protect_fields.py @@ -81,6 +81,25 @@ def __init__(self, count): setattr(self, f"init_field_{i}", values[i % len(values)](i)) +@db0.memo +@dataclass +class MemoUnprotectedBaseFieldsClass: + base_value: str + + +@db0.memo(protect_fields=True) +@dataclass +class MemoProtectedDerivedFieldsClass(MemoUnprotectedBaseFieldsClass): + name: str + value: int + + +@db0.memo +@dataclass +class MemoImplicitlyProtectedDerivedFieldsClass(MemoProtectedDerivedFieldsClass): + derived_value: float + + def get_memo_class_object(obj): return db0.get_memo_class(obj).get_class() @@ -242,6 +261,60 @@ def test_set_field_access_requires_protected_class(db0_fixture): assert False, "set_field_access should fail for unprotected classes" +def test_protect_fields_is_inherited_by_derived_memo_classes(db0_fixture): + obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) + + assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is True + db0.set_field_access(MemoImplicitlyProtectedDerivedFieldsClass, 123, (FieldAccess.READ,), "derived_value") + + +def test_protect_fields_cannot_be_disabled_in_derived_memo_class(db0_fixture): + with pytest.raises(RuntimeError, match="protect_fields.*base"): + @db0.memo(protect_fields=False) + @dataclass + class ExplicitlyUnprotectedDerived(MemoProtectedDerivedFieldsClass): + extra_value: str + + +def test_set_field_access_still_rejects_unprotected_base_of_protected_class(db0_fixture): + MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) + + with pytest.raises(RuntimeError, match="protected fields"): + db0.set_field_access(MemoUnprotectedBaseFieldsClass, 123, (FieldAccess.READ,), "base_value") + + +def test_reset_protect_fields_rejects_inherited_protection(db0_fixture): + obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) + memo_class = get_memo_class_object(obj) + + with pytest.raises(RuntimeError, match="inherits.*protect_fields"): + db0.reset_protect_fields(MemoImplicitlyProtectedDerivedFieldsClass) + + assert memo_class.get_type_flags()["protect_fields"] is True + + +def test_class_reset_protect_fields_rejects_inherited_protection(db0_fixture): + obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) + memo_class = get_memo_class_object(obj) + + with pytest.raises(RuntimeError, match="inherits.*protect_fields"): + memo_class.reset_protect_fields() + + assert memo_class.get_type_flags()["protect_fields"] is True + + +def test_explicit_true_is_allowed_on_class_derived_from_protected_base(db0_fixture): + @db0.memo(protect_fields=True) + @dataclass + class ExplicitlyProtectedDerived(MemoProtectedDerivedFieldsClass): + extra_value: str + + obj = ExplicitlyProtectedDerived("base", "alpha", 1, "extra") + + assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is True + db0.set_field_access(ExplicitlyProtectedDerived, 123, (FieldAccess.READ,), "extra_value") + + def test_set_field_access_accepts_unknown_field_names(db0_fixture): MemoProtectedFieldsClass("alpha", 1) @@ -376,6 +449,81 @@ def test_protected_field_getter_requires_read_access(db0_fixture): _ = obj.value +def test_protected_field_getter_resolves_masks_upstream_and_allows_derived_override(db0_fixture): + account_id = ContextVar("protected_inheritance_account_id") + obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) + db0.set_field_access(MemoProtectedDerivedFieldsClass, 123, (FieldAccess.READ,), "base_value", "name") + db0.set_field_access(MemoImplicitlyProtectedDerivedFieldsClass, 123, (), "name") + db0.set_field_access(MemoImplicitlyProtectedDerivedFieldsClass, 123, (FieldAccess.READ,), "derived_value") + db0._init_data_masking(account_id) + account_id.set(123) + + assert obj.base_value == "base" + assert obj.derived_value == 1.5 + with pytest.raises(PermissionError, match="read"): + _ = obj.name + with pytest.raises(PermissionError, match="read"): + _ = obj.value + + +def test_protected_field_getter_allows_derived_mask_to_override_base_deny(db0_fixture): + account_id = ContextVar("protected_inheritance_allow_override_account_id") + obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) + db0.set_field_access(MemoProtectedDerivedFieldsClass, 123, (), "name") + db0.set_field_access(MemoImplicitlyProtectedDerivedFieldsClass, 123, (FieldAccess.READ,), "name") + db0._init_data_masking(account_id) + account_id.set(123) + + assert obj.name == "alpha" + with pytest.raises(PermissionError, match="read"): + _ = obj.value + + +def test_inherited_protect_fields_survives_reopen_without_parameter(db0_fixture): + @db0.memo(id="dbzero-software/dbzero/tests/protected-inherited-reopen-base") + @dataclass + class ReopenBase: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-inherited-reopen-mid", protect_fields=True) + @dataclass + class ReopenMid(ReopenBase): + name: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-inherited-reopen-derived") + @dataclass + class ReopenDerived(ReopenMid): + value: int + + obj = ReopenDerived("base", "alpha", 1) + obj_id = db0.uuid(obj) + assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is True + db0.commit() + + db0.close() + db0.init(DB0_DIR) + db0.open("my-test-prefix") + + @db0.memo(id="dbzero-software/dbzero/tests/protected-inherited-reopen-base") + @dataclass + class ReopenBaseAfter: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-inherited-reopen-mid") + @dataclass + class ReopenMidAfter(ReopenBaseAfter): + name: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-inherited-reopen-derived") + @dataclass + class ReopenDerivedAfter(ReopenMidAfter): + value: int + + obj = db0.fetch(ReopenDerivedAfter, obj_id) + assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is True + db0.set_field_access(ReopenDerivedAfter, 123, (FieldAccess.READ,), "value") + + def test_protected_field_getter_returns_missing_value_placeholder(db0_fixture): account_id = ContextVar("protected_placeholder_account_id") missing_value = object() diff --git a/src/dbzero/bindings/python/Memo.cpp b/src/dbzero/bindings/python/Memo.cpp index 1ee36ca5..3b0f0bc7 100644 --- a/src/dbzero/bindings/python/Memo.cpp +++ b/src/dbzero/bindings/python/Memo.cpp @@ -12,6 +12,7 @@ #include "Migration.hpp" #include "PyHash.hpp" #include "DataMasking.hpp" +#include #include #include #include @@ -902,7 +903,7 @@ namespace db0::python PyObject *wrapPyType(PyTypeObject *base_class, bool is_singleton, bool no_default_tags, const char *prefix_name, const char *type_id, const char *file_name, std::vector &&init_vars, PyObject *py_dyn_prefix_callable, - std::vector &&migrations, bool no_cache, bool immutable, bool protect_fields) + std::vector &&migrations, bool no_cache, bool immutable, std::optional protect_fields_option) { auto py_class = Py_BORROW(base_class); auto py_module = Py_OWN(findModule(*Py_OWN(PyObject_GetAttrString((PyObject*)*py_class, "__module__")))); @@ -936,6 +937,15 @@ namespace db0::python return nullptr; } + auto base_memo_type = PyToolkit::getBaseMemoType(*new_type); + bool inherited_protect_fields = base_memo_type + && MemoTypeDecoration::get(base_memo_type).getFlags()[MemoOptions::PROTECT_FIELDS]; + if (inherited_protect_fields && protect_fields_option == false) { + THROWF(db0::InputException) + << "Cannot set protect_fields=False on a class derived from a protect_fields base class"; + } + bool protect_fields = protect_fields_option.value_or(false) || inherited_protect_fields; + MemoFlags type_flags = no_default_tags ? MemoFlags { MemoOptions::NO_DEFAULT_TAGS } : MemoFlags(); if (no_cache) { type_flags.set(MemoOptions::NO_CACHE); @@ -1000,7 +1010,10 @@ namespace db0::python bool no_default_tags = py_no_default_tags && PyObject_IsTrue(py_no_default_tags); bool no_cache = py_no_cache && PyObject_IsTrue(py_no_cache); bool immutable = py_immutable && PyObject_IsTrue(py_immutable); - bool protect_fields = py_protect_fields && PyObject_IsTrue(py_protect_fields); + std::optional protect_fields_option; + if (py_protect_fields) { + protect_fields_option = PyObject_IsTrue(py_protect_fields); + } const char *prefix_name = (py_prefix_name && py_prefix_name != Py_None) ? PyUnicode_AsUTF8(py_prefix_name) : nullptr; const char *type_id = py_type_id ? PyUnicode_AsUTF8(py_type_id) : nullptr; const char *file_name = (py_file_name && py_file_name != Py_None) ? PyUnicode_AsUTF8(py_file_name) : nullptr; @@ -1035,7 +1048,7 @@ namespace db0::python auto migrations = extractMigrations(py_migrations); return wrapPyType(castToType(class_obj), is_singleton, no_default_tags, prefix_name, type_id, file_name, - std::move(init_vars), py_dyn_prefix, std::move(migrations), no_cache, immutable, protect_fields + std::move(init_vars), py_dyn_prefix, std::move(migrations), no_cache, immutable, protect_fields_option ); } diff --git a/src/dbzero/bindings/python/PyAPI.cpp b/src/dbzero/bindings/python/PyAPI.cpp index 7fb86ebc..c8dab534 100644 --- a/src/dbzero/bindings/python/PyAPI.cpp +++ b/src/dbzero/bindings/python/PyAPI.cpp @@ -1019,17 +1019,23 @@ namespace db0::python auto memo_type = reinterpret_cast(py_type); auto &decor = MemoTypeDecoration::get(memo_type); - if (decor.getFlags()[MemoOptions::PROTECT_FIELDS]) { - THROWF(db0::InputException) - << "Type is still decorated with protect_fields=True; remove it or set protect_fields=False first"; - } - using ClassFactory = db0::object_model::ClassFactory; auto fixture_uuid = decor.getFixtureUUID(AccessType::READ_WRITE); auto fixture = PyToolkit::getPyWorkspace().getWorkspace().getFixture(fixture_uuid, AccessType::READ_WRITE); auto &class_factory = fixture->get(); auto type = class_factory.getExistingType(memo_type); + if (type->getBaseClassPtr() && type->getBaseClassPtr()->isProtectFields()) { + THROWF(db0::InputException) + << "Cannot disable protected fields on class " << type->getName() + << " because it inherits from a protect_fields base class"; + } + + if (decor.getFlags()[MemoOptions::PROTECT_FIELDS]) { + THROWF(db0::InputException) + << "Type is still decorated with protect_fields=True; remove it or set protect_fields=False first"; + } + db0::FixtureLock lock(fixture); type->resetProtectFields(); Py_RETURN_NONE; diff --git a/src/dbzero/object_model/class/Class.cpp b/src/dbzero/object_model/class/Class.cpp index 18c87b09..2d9c8b91 100644 --- a/src/dbzero/object_model/class/Class.cpp +++ b/src/dbzero/object_model/class/Class.cpp @@ -346,6 +346,11 @@ namespace db0::object_model } void Class::resetProtectFields() { + if (m_base_class_ptr && m_base_class_ptr->isProtectFields()) { + THROWF(db0::InputException) + << "Cannot disable protected fields on class " << getName() + << " because it inherits from a protect_fields base class"; + } modify().m_flags.set(ClassOptions::PROTECT_FIELDS, false); } @@ -391,7 +396,7 @@ namespace db0::object_model field_offsets.reserve(field_names.size()); for (const auto &field_name: field_names) { auto member = tryGetMember(field_name.c_str()); - if (member) { + if (member && mask.value() != 0) { field_offsets.push_back(field_id_mapper.assignFieldOffset(member->m_field_id)); } else { field_offsets.push_back(field_id_mapper.assignFieldOffset(field_name.c_str())); @@ -412,18 +417,31 @@ namespace db0::object_model return {}; } - auto &field_safe = getFieldSafe(); - auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member.m_field_id); - if (!maybe_offset) { - return {}; + if (hasFieldSafe()) { + auto &field_safe = getFieldSafe(); + auto field_mask = field_safe.getFieldMaskManager().tryGetFieldMask(account_id); + auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member.m_name.c_str()); + if (maybe_offset && field_mask) { + auto mask = field_mask->getAssignedMask(*maybe_offset); + if (mask) { + return mask; + } + } + + maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member.m_field_id); + if (maybe_offset && field_mask) { + auto mask = field_mask->getAssignedMask(*maybe_offset); + if (mask && mask->value() != 0) { + return mask; + } + } } - auto field_mask = field_safe.getFieldMaskManager().tryGetFieldMask(account_id); - if (!field_mask) { - return {}; + if (m_base_class_ptr && m_base_class_ptr->isProtectFields()) { + return m_base_class_ptr->tryGetFieldAccess(account_id, member); } - return field_mask->getAssignedMask(*maybe_offset); + return {}; } std::optional Class::tryGetFieldAccess(std::uint64_t account_id, const MemberLoc &member_loc) const @@ -434,7 +452,13 @@ namespace db0::object_model } auto member = tryGetMember(member_id.primary().first); - return member ? tryGetFieldAccess(account_id, *member) : std::nullopt; + if (member) { + return tryGetFieldAccess(account_id, *member); + } + if (m_base_class_ptr && m_base_class_ptr->isProtectFields()) { + return m_base_class_ptr->tryGetFieldAccess(account_id, member_loc); + } + return {}; } std::vector > Class::getFieldAccess(std::uint64_t account_id) const diff --git a/src/dbzero/object_model/class/ClassFactory.cpp b/src/dbzero/object_model/class/ClassFactory.cpp index e5c27c57..b0ab0e1b 100644 --- a/src/dbzero/object_model/class/ClassFactory.cpp +++ b/src/dbzero/object_model/class/ClassFactory.cpp @@ -155,7 +155,10 @@ namespace db0::object_model if (class_ptr) { // pull existing dbzero class instance by pointer type = getTypeByPtr(class_ptr, lang_type).m_class; - if (LangToolkit::isProtectFields(lang_type) && !type->isProtectFields()) { + auto base_class = type->tryGetBaseClass(); + bool protect_fields = LangToolkit::isProtectFields(lang_type) + || (base_class && base_class->isProtectFields()); + if (protect_fields && !type->isProtectFields()) { type->setProtectFields(); } } else { @@ -172,12 +175,13 @@ namespace db0::object_model } flags.set(ClassOptions::NO_DEFAULT_TAGS, LangToolkit::isNoDefaultTags(lang_type)); flags.set(ClassOptions::IMMUTABLE, LangToolkit::isImmutable(lang_type)); - flags.set(ClassOptions::PROTECT_FIELDS, LangToolkit::isProtectFields(lang_type)); auto memo_base = LangToolkit::getBaseMemoType(lang_type); std::shared_ptr base_class; if (memo_base) { base_class = getOrCreateType(memo_base); } + flags.set(ClassOptions::PROTECT_FIELDS, + LangToolkit::isProtectFields(lang_type) || (base_class && base_class->isProtectFields())); type = std::shared_ptr(new Class(fixture, LangToolkit::getTypeName(lang_type), LangToolkit::tryGetModuleName(lang_type), type_id, prefix_name, init_vars, flags, base_class) ); @@ -202,8 +206,13 @@ namespace db0::object_model it_cached = m_type_cache.insert({lang_type, type}).first; m_pending_types.push_back(lang_type); - } else if (LangToolkit::isProtectFields(lang_type) && !it_cached->second->isProtectFields()) { - it_cached->second->setProtectFields(); + } else { + auto base_class = it_cached->second->tryGetBaseClass(); + bool protect_fields = LangToolkit::isProtectFields(lang_type) + || (base_class && base_class->isProtectFields()); + if (protect_fields && !it_cached->second->isProtectFields()) { + it_cached->second->setProtectFields(); + } } return it_cached->second; } @@ -240,7 +249,10 @@ namespace db0::object_model it_cached->second.m_class->setInitVars(LangToolkit::getInitVars(lang_type)); it_cached->second.m_class->setRuntimeFlags(LangToolkit::getMemoFlags(lang_type)); } - if (lang_type && LangToolkit::isProtectFields(lang_type) && !it_cached->second.m_class->isProtectFields()) { + auto base_class = it_cached->second.m_class->tryGetBaseClass(); + bool protect_fields = lang_type && (LangToolkit::isProtectFields(lang_type) + || (base_class && base_class->isProtectFields())); + if (protect_fields && !it_cached->second.m_class->isProtectFields()) { it_cached->second.m_class->setProtectFields(); } return it_cached->second.m_class; @@ -307,7 +319,10 @@ namespace db0::object_model if (lang_type) { type->setInitVars(LangToolkit::getInitVars(lang_type)); type->setRuntimeFlags(LangToolkit::getMemoFlags(lang_type)); - if (LangToolkit::isProtectFields(lang_type) && !type->isProtectFields()) { + auto base_class = type->tryGetBaseClass(); + bool protect_fields = LangToolkit::isProtectFields(lang_type) + || (base_class && base_class->isProtectFields()); + if (protect_fields && !type->isProtectFields()) { type->setProtectFields(); } } @@ -321,7 +336,10 @@ namespace db0::object_model it_cached->second.m_class->setInitVars(LangToolkit::getInitVars(lang_type)); it_cached->second.m_class->setRuntimeFlags(LangToolkit::getMemoFlags(lang_type)); } - if (lang_type && LangToolkit::isProtectFields(lang_type) && !it_cached->second.m_class->isProtectFields()) { + auto base_class = it_cached->second.m_class->tryGetBaseClass(); + bool protect_fields = lang_type && (LangToolkit::isProtectFields(lang_type) + || (base_class && base_class->isProtectFields())); + if (protect_fields && !it_cached->second.m_class->isProtectFields()) { it_cached->second.m_class->setProtectFields(); } return it_cached->second; diff --git a/src/dbzero/object_model/class/FieldIDMapper.cpp b/src/dbzero/object_model/class/FieldIDMapper.cpp index 3e15489b..114a3a9d 100644 --- a/src/dbzero/object_model/class/FieldIDMapper.cpp +++ b/src/dbzero/object_model/class/FieldIDMapper.cpp @@ -280,6 +280,12 @@ namespace db0::object_model return std::nullopt; } + std::optional FieldIDMapper::tryGetAssignedFieldOffset(const char *field_name) const + { + assert(field_name); + return tryGetNameOffset(field_name); + } + std::uint32_t FieldIDMapper::assignFieldOffset(FieldID field_id) { auto maybe_offset = tryGetAssignedFieldOffset(field_id); diff --git a/src/dbzero/object_model/class/FieldIDMapper.hpp b/src/dbzero/object_model/class/FieldIDMapper.hpp index bb1a07ed..aa50f19e 100644 --- a/src/dbzero/object_model/class/FieldIDMapper.hpp +++ b/src/dbzero/object_model/class/FieldIDMapper.hpp @@ -58,6 +58,7 @@ DB0_PACKED_END bool renameField(const char *old_field_name, const char *new_field_name); std::optional tryGetAssignedFieldOffset(FieldID) const; + std::optional tryGetAssignedFieldOffset(const char *field_name) const; std::unordered_map getAssignedNameOffsets() const; std::uint32_t getFieldOffsetRange() const; From 8c09111a0e81965a86895a369d1a44191108071d Mon Sep 17 00:00:00 2001 From: Adrian Zawadzki Date: Mon, 18 May 2026 09:49:28 +0200 Subject: [PATCH 2/5] fix(mr): merge request fixes --- python_tests/test_memo_protect_fields.py | 75 +++++++++++++++++++ src/dbzero/bindings/python/Memo.cpp | 2 +- src/dbzero/bindings/python/PyAPI.cpp | 6 +- src/dbzero/bindings/python/PyInternalAPI.cpp | 4 +- src/dbzero/object_model/class/Class.cpp | 12 ++- src/dbzero/object_model/class/Class.hpp | 1 + .../object_model/class/ClassFactory.cpp | 59 +++++++++------ .../object_model/class/ClassFactory.hpp | 14 +++- 8 files changed, 141 insertions(+), 32 deletions(-) diff --git a/python_tests/test_memo_protect_fields.py b/python_tests/test_memo_protect_fields.py index 3d16c9f3..99facf99 100644 --- a/python_tests/test_memo_protect_fields.py +++ b/python_tests/test_memo_protect_fields.py @@ -524,6 +524,81 @@ class ReopenDerivedAfter(ReopenMidAfter): db0.set_field_access(ReopenDerivedAfter, 123, (FieldAccess.READ,), "value") +def test_derived_class_inherits_protect_fields_enabled_on_base_after_materialization(db0_fixture): + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-enabled-later-base") + @dataclass + class BaseBefore: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-enabled-later-derived") + @dataclass + class DerivedBefore(BaseBefore): + value: int + + obj = DerivedBefore("base", 1) + obj_id = db0.uuid(obj) + assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is False + with pytest.raises(RuntimeError, match="protected fields"): + db0.set_field_access(DerivedBefore, 123, (FieldAccess.READ,), "value") + db0.commit() + + db0.close() + db0.init(DB0_DIR) + db0.open("my-test-prefix") + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-enabled-later-base", protect_fields=True) + @dataclass + class BaseAfter: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-enabled-later-derived") + @dataclass + class DerivedAfter(BaseAfter): + value: int + + obj = db0.fetch(DerivedAfter, obj_id) + assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is True + db0.set_field_access(DerivedAfter, 123, (FieldAccess.READ,), "value") + + +def test_derived_class_stops_inheriting_protect_fields_after_base_reset(db0_fixture): + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-reset-base", protect_fields=True) + @dataclass + class BaseBefore: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-reset-derived") + @dataclass + class DerivedBefore(BaseBefore): + value: int + + obj = DerivedBefore("base", 1) + obj_id = db0.uuid(obj) + assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is True + db0.commit() + + db0.close() + db0.init(DB0_DIR) + db0.open("my-test-prefix") + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-reset-base", protect_fields=False) + @dataclass + class BaseAfter: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-reset-derived") + @dataclass + class DerivedAfter(BaseAfter): + value: int + + obj = db0.fetch(DerivedAfter, obj_id) + db0.reset_protect_fields(BaseAfter) + + assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is False + with pytest.raises(RuntimeError, match="protected fields"): + db0.set_field_access(DerivedAfter, 123, (FieldAccess.READ,), "value") + + def test_protected_field_getter_returns_missing_value_placeholder(db0_fixture): account_id = ContextVar("protected_placeholder_account_id") missing_value = object() diff --git a/src/dbzero/bindings/python/Memo.cpp b/src/dbzero/bindings/python/Memo.cpp index 3b0f0bc7..3d573133 100644 --- a/src/dbzero/bindings/python/Memo.cpp +++ b/src/dbzero/bindings/python/Memo.cpp @@ -944,7 +944,7 @@ namespace db0::python THROWF(db0::InputException) << "Cannot set protect_fields=False on a class derived from a protect_fields base class"; } - bool protect_fields = protect_fields_option.value_or(false) || inherited_protect_fields; + bool protect_fields = protect_fields_option.value_or(false); MemoFlags type_flags = no_default_tags ? MemoFlags { MemoOptions::NO_DEFAULT_TAGS } : MemoFlags(); if (no_cache) { diff --git a/src/dbzero/bindings/python/PyAPI.cpp b/src/dbzero/bindings/python/PyAPI.cpp index c8dab534..d2ee6281 100644 --- a/src/dbzero/bindings/python/PyAPI.cpp +++ b/src/dbzero/bindings/python/PyAPI.cpp @@ -944,7 +944,7 @@ namespace db0::python auto fixture_uuid = MemoTypeDecoration::get(reinterpret_cast(py_type)).getFixtureUUID(); auto fixture = PyToolkit::getPyWorkspace().getWorkspace().getFixture(fixture_uuid, AccessType::READ_WRITE); auto &class_factory = fixture->get(); - auto type = class_factory.getExistingType(reinterpret_cast(py_type)); + auto type = class_factory.getOrCreateType(reinterpret_cast(py_type)); if (!type->isProtectFields()) { THROWF(db0::InputException) << "Class " << type->getName() << " does not have protected fields enabled"; } @@ -979,7 +979,7 @@ namespace db0::python auto fixture_uuid = MemoTypeDecoration::get(reinterpret_cast(py_type)).getFixtureUUID(); auto fixture = PyToolkit::getPyWorkspace().getWorkspace().getFixture(fixture_uuid, AccessType::READ_WRITE); auto &class_factory = fixture->get(); - auto type = class_factory.getExistingType(reinterpret_cast(py_type)); + auto type = class_factory.getOrCreateType(reinterpret_cast(py_type)); if (!type->isProtectFields()) { THROWF(db0::InputException) << "Class " << type->getName() << " does not have protected fields enabled"; } @@ -1023,7 +1023,7 @@ namespace db0::python auto fixture_uuid = decor.getFixtureUUID(AccessType::READ_WRITE); auto fixture = PyToolkit::getPyWorkspace().getWorkspace().getFixture(fixture_uuid, AccessType::READ_WRITE); auto &class_factory = fixture->get(); - auto type = class_factory.getExistingType(memo_type); + auto type = class_factory.getOrCreateType(memo_type); if (type->getBaseClassPtr() && type->getBaseClassPtr()->isProtectFields()) { THROWF(db0::InputException) diff --git a/src/dbzero/bindings/python/PyInternalAPI.cpp b/src/dbzero/bindings/python/PyInternalAPI.cpp index 3d21a1a1..9dd9558e 100644 --- a/src/dbzero/bindings/python/PyInternalAPI.cpp +++ b/src/dbzero/bindings/python/PyInternalAPI.cpp @@ -186,7 +186,7 @@ namespace db0::python // validate type if requested (no validation for MemoBase) if (py_expected_type && !PyToolkit::getTypeManager().isMemoBase(py_expected_type)) { // in other cases the type must match the actual object type - auto expected_class = class_factory.getExistingType(py_expected_type); + auto expected_class = class_factory.getOrCreateType(py_expected_type); // honor class-specific access flags (e.g. type-level no_cache) auto result = PyToolkit::unloadObject(fixture, addr, class_factory, nullptr, addr.getInstanceId(), expected_class->getInstanceFlags() @@ -1100,4 +1100,4 @@ namespace db0::python template PyObject *getMaterializedMemoObject(MemoObject *); template PyObject *getMaterializedMemoObject(MemoImmutableObject *); -} \ No newline at end of file +} diff --git a/src/dbzero/object_model/class/Class.cpp b/src/dbzero/object_model/class/Class.cpp index 2d9c8b91..5c4b12fd 100644 --- a/src/dbzero/object_model/class/Class.cpp +++ b/src/dbzero/object_model/class/Class.cpp @@ -112,7 +112,7 @@ namespace db0::object_model , m_uid(this->fetchUID()) , m_member_cache(m_members, *this, this->getRefreshCallback()) { - if (isProtectFields()) { + if (hasOwnProtectFields()) { ensureFieldSafe(); } m_schema.postInit(getTotalFunc()); @@ -307,10 +307,14 @@ namespace db0::object_model return (*this)->m_flags[ClassOptions::IMMUTABLE]; } - bool Class::isProtectFields() const { + bool Class::hasOwnProtectFields() const { return (*this)->m_flags[ClassOptions::PROTECT_FIELDS]; } + bool Class::isProtectFields() const { + return getClassFactory(*getFixture()).isProtectFields(*this); + } + void Class::assertFieldSafeSupported() const { if ((*this)->getObjVer() < FIELD_SAFE_MIN_VERSION) { @@ -343,6 +347,7 @@ namespace db0::object_model void Class::setProtectFields() { ensureFieldSafe(); modify().m_flags.set(ClassOptions::PROTECT_FIELDS, true); + getClassFactory(*getFixture()).resetProtectFieldsCache(*this); } void Class::resetProtectFields() { @@ -352,6 +357,7 @@ namespace db0::object_model << " because it inherits from a protect_fields base class"; } modify().m_flags.set(ClassOptions::PROTECT_FIELDS, false); + getClassFactory(*getFixture()).resetProtectFieldsCache(*this); } bool Class::hasFieldSafe() const @@ -388,7 +394,7 @@ namespace db0::object_model THROWF(db0::InputException) << "At least one field name is required"; } - auto &field_safe = getFieldSafe(); + auto &field_safe = ensureFieldSafe(); auto &field_id_mapper = field_safe.getFieldIDMapper(); auto &field_mask_manager = field_safe.getFieldMaskManager(); diff --git a/src/dbzero/object_model/class/Class.hpp b/src/dbzero/object_model/class/Class.hpp index 04a09729..be45bf1e 100644 --- a/src/dbzero/object_model/class/Class.hpp +++ b/src/dbzero/object_model/class/Class.hpp @@ -176,6 +176,7 @@ DB0_PACKED_END bool isImmutable() const; bool assignDefaultTags() const; bool isProtectFields() const; + bool hasOwnProtectFields() const; void setProtectFields(); void resetProtectFields(); bool hasFieldSafe() const; diff --git a/src/dbzero/object_model/class/ClassFactory.cpp b/src/dbzero/object_model/class/ClassFactory.cpp index b0ab0e1b..4ee36c91 100644 --- a/src/dbzero/object_model/class/ClassFactory.cpp +++ b/src/dbzero/object_model/class/ClassFactory.cpp @@ -136,6 +136,29 @@ namespace db0::object_model } return type; } + + bool ClassFactory::isProtectFields(const Class &type) const + { + bool ownProtectFields = type.hasOwnProtectFields(); + auto baseClass = type.getBaseClassPtr(); + bool baseProtectFields = baseClass && isProtectFields(*baseClass); + auto cacheKey = type.getAddress(); + auto it = m_protect_fields_cache.find(cacheKey); + if (it != m_protect_fields_cache.end() + && it->second.m_own_protect_fields == ownProtectFields + && it->second.m_base_protect_fields == baseProtectFields) { + return it->second.m_protect_fields; + } + + bool protectFields = ownProtectFields || baseProtectFields; + m_protect_fields_cache[cacheKey] = { ownProtectFields, baseProtectFields, protectFields }; + return protectFields; + } + + void ClassFactory::resetProtectFieldsCache(const Class &type) const + { + m_protect_fields_cache.erase(type.getAddress()); + } std::shared_ptr ClassFactory::tryGetOrCreateType(TypeObjectPtr lang_type) { @@ -155,10 +178,11 @@ namespace db0::object_model if (class_ptr) { // pull existing dbzero class instance by pointer type = getTypeByPtr(class_ptr, lang_type).m_class; - auto base_class = type->tryGetBaseClass(); - bool protect_fields = LangToolkit::isProtectFields(lang_type) - || (base_class && base_class->isProtectFields()); - if (protect_fields && !type->isProtectFields()) { + auto memo_base = LangToolkit::getBaseMemoType(lang_type); + if (memo_base) { + getOrCreateType(memo_base); + } + if (LangToolkit::isProtectFields(lang_type) && !type->hasOwnProtectFields()) { type->setProtectFields(); } } else { @@ -180,8 +204,7 @@ namespace db0::object_model if (memo_base) { base_class = getOrCreateType(memo_base); } - flags.set(ClassOptions::PROTECT_FIELDS, - LangToolkit::isProtectFields(lang_type) || (base_class && base_class->isProtectFields())); + flags.set(ClassOptions::PROTECT_FIELDS, LangToolkit::isProtectFields(lang_type)); type = std::shared_ptr(new Class(fixture, LangToolkit::getTypeName(lang_type), LangToolkit::tryGetModuleName(lang_type), type_id, prefix_name, init_vars, flags, base_class) ); @@ -207,10 +230,11 @@ namespace db0::object_model it_cached = m_type_cache.insert({lang_type, type}).first; m_pending_types.push_back(lang_type); } else { - auto base_class = it_cached->second->tryGetBaseClass(); - bool protect_fields = LangToolkit::isProtectFields(lang_type) - || (base_class && base_class->isProtectFields()); - if (protect_fields && !it_cached->second->isProtectFields()) { + auto memo_base = LangToolkit::getBaseMemoType(lang_type); + if (memo_base) { + getOrCreateType(memo_base); + } + if (LangToolkit::isProtectFields(lang_type) && !it_cached->second->hasOwnProtectFields()) { it_cached->second->setProtectFields(); } } @@ -249,10 +273,7 @@ namespace db0::object_model it_cached->second.m_class->setInitVars(LangToolkit::getInitVars(lang_type)); it_cached->second.m_class->setRuntimeFlags(LangToolkit::getMemoFlags(lang_type)); } - auto base_class = it_cached->second.m_class->tryGetBaseClass(); - bool protect_fields = lang_type && (LangToolkit::isProtectFields(lang_type) - || (base_class && base_class->isProtectFields())); - if (protect_fields && !it_cached->second.m_class->isProtectFields()) { + if (lang_type && LangToolkit::isProtectFields(lang_type) && !it_cached->second.m_class->hasOwnProtectFields()) { it_cached->second.m_class->setProtectFields(); } return it_cached->second.m_class; @@ -319,10 +340,7 @@ namespace db0::object_model if (lang_type) { type->setInitVars(LangToolkit::getInitVars(lang_type)); type->setRuntimeFlags(LangToolkit::getMemoFlags(lang_type)); - auto base_class = type->tryGetBaseClass(); - bool protect_fields = LangToolkit::isProtectFields(lang_type) - || (base_class && base_class->isProtectFields()); - if (protect_fields && !type->isProtectFields()) { + if (LangToolkit::isProtectFields(lang_type) && !type->hasOwnProtectFields()) { type->setProtectFields(); } } @@ -336,10 +354,7 @@ namespace db0::object_model it_cached->second.m_class->setInitVars(LangToolkit::getInitVars(lang_type)); it_cached->second.m_class->setRuntimeFlags(LangToolkit::getMemoFlags(lang_type)); } - auto base_class = it_cached->second.m_class->tryGetBaseClass(); - bool protect_fields = lang_type && (LangToolkit::isProtectFields(lang_type) - || (base_class && base_class->isProtectFields())); - if (protect_fields && !it_cached->second.m_class->isProtectFields()) { + if (lang_type && LangToolkit::isProtectFields(lang_type) && !it_cached->second.m_class->hasOwnProtectFields()) { it_cached->second.m_class->setProtectFields(); } return it_cached->second; diff --git a/src/dbzero/object_model/class/ClassFactory.hpp b/src/dbzero/object_model/class/ClassFactory.hpp index 2fb99eb8..d2a9c000 100644 --- a/src/dbzero/object_model/class/ClassFactory.hpp +++ b/src/dbzero/object_model/class/ClassFactory.hpp @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include @@ -125,7 +126,17 @@ DB0_PACKED_END // calculate class-ref from its address std::uint32_t getClassRef(Address class_addr) const; + bool isProtectFields(const Class &) const; + void resetProtectFieldsCache(const Class &) const; + private: + struct ProtectFieldsCacheItem + { + bool m_own_protect_fields = false; + bool m_base_protect_fields = false; + bool m_protect_fields = false; + }; + // Language specific type to dbzero class mapping mutable std::unordered_map > m_type_cache; // dbzero Class objects by pointer (may not have language specific type assigned yet) @@ -137,6 +148,7 @@ DB0_PACKED_END // buffers with keys for potential rollback mutable std::vector m_pending_types; mutable std::vector m_pending_ptrs; + mutable std::unordered_map m_protect_fields_cache; // starting address of the "types" slot const std::pair m_type_slot_addr_range; @@ -155,4 +167,4 @@ DB0_PACKED_END std::optional getNameVariant(ClassFactory::TypeObjectPtr lang_type, const char *type_id, int variant_id); -} \ No newline at end of file +} From e7a121ef174ddee7489c6457d53fee0e36f72e1d Mon Sep 17 00:00:00 2001 From: Adrian Zawadzki Date: Mon, 18 May 2026 10:27:58 +0200 Subject: [PATCH 3/5] fix(protected_fields): added cache for isProtectFields --- src/dbzero/object_model/class/ClassFactory.cpp | 16 ++++++---------- src/dbzero/object_model/class/ClassFactory.hpp | 2 -- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/dbzero/object_model/class/ClassFactory.cpp b/src/dbzero/object_model/class/ClassFactory.cpp index 4ee36c91..60aa8219 100644 --- a/src/dbzero/object_model/class/ClassFactory.cpp +++ b/src/dbzero/object_model/class/ClassFactory.cpp @@ -139,25 +139,21 @@ namespace db0::object_model bool ClassFactory::isProtectFields(const Class &type) const { - bool ownProtectFields = type.hasOwnProtectFields(); - auto baseClass = type.getBaseClassPtr(); - bool baseProtectFields = baseClass && isProtectFields(*baseClass); auto cacheKey = type.getAddress(); auto it = m_protect_fields_cache.find(cacheKey); - if (it != m_protect_fields_cache.end() - && it->second.m_own_protect_fields == ownProtectFields - && it->second.m_base_protect_fields == baseProtectFields) { + if (it != m_protect_fields_cache.end()) { return it->second.m_protect_fields; } - bool protectFields = ownProtectFields || baseProtectFields; - m_protect_fields_cache[cacheKey] = { ownProtectFields, baseProtectFields, protectFields }; + auto baseClass = type.getBaseClassPtr(); + bool protectFields = type.hasOwnProtectFields() || (baseClass && isProtectFields(*baseClass)); + m_protect_fields_cache[cacheKey] = { protectFields }; return protectFields; } - void ClassFactory::resetProtectFieldsCache(const Class &type) const + void ClassFactory::resetProtectFieldsCache(const Class &) const { - m_protect_fields_cache.erase(type.getAddress()); + m_protect_fields_cache.clear(); } std::shared_ptr ClassFactory::tryGetOrCreateType(TypeObjectPtr lang_type) diff --git a/src/dbzero/object_model/class/ClassFactory.hpp b/src/dbzero/object_model/class/ClassFactory.hpp index d2a9c000..3089172d 100644 --- a/src/dbzero/object_model/class/ClassFactory.hpp +++ b/src/dbzero/object_model/class/ClassFactory.hpp @@ -132,8 +132,6 @@ DB0_PACKED_END private: struct ProtectFieldsCacheItem { - bool m_own_protect_fields = false; - bool m_base_protect_fields = false; bool m_protect_fields = false; }; From 4cca6843b577f6bb85c5bd3864f8c39976038c74 Mon Sep 17 00:00:00 2001 From: Adrian Zawadzki Date: Mon, 18 May 2026 11:40:26 +0200 Subject: [PATCH 4/5] fix(mr): Merge request fixes --- python_tests/test_memo_protect_fields.py | 8 +-- src/dbzero/object_model/class/Class.cpp | 53 +++++++++---------- src/dbzero/object_model/class/Class.hpp | 2 + .../object_model/class/ClassFactory.cpp | 19 ------- .../object_model/class/ClassFactory.hpp | 9 ---- 5 files changed, 32 insertions(+), 59 deletions(-) diff --git a/python_tests/test_memo_protect_fields.py b/python_tests/test_memo_protect_fields.py index 99facf99..cc847d64 100644 --- a/python_tests/test_memo_protect_fields.py +++ b/python_tests/test_memo_protect_fields.py @@ -449,24 +449,24 @@ def test_protected_field_getter_requires_read_access(db0_fixture): _ = obj.value -def test_protected_field_getter_resolves_masks_upstream_and_allows_derived_override(db0_fixture): +def test_protected_field_getter_does_not_inherit_base_class_masks(db0_fixture): account_id = ContextVar("protected_inheritance_account_id") obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) db0.set_field_access(MemoProtectedDerivedFieldsClass, 123, (FieldAccess.READ,), "base_value", "name") - db0.set_field_access(MemoImplicitlyProtectedDerivedFieldsClass, 123, (), "name") db0.set_field_access(MemoImplicitlyProtectedDerivedFieldsClass, 123, (FieldAccess.READ,), "derived_value") db0._init_data_masking(account_id) account_id.set(123) - assert obj.base_value == "base" assert obj.derived_value == 1.5 + with pytest.raises(PermissionError, match="read"): + _ = obj.base_value with pytest.raises(PermissionError, match="read"): _ = obj.name with pytest.raises(PermissionError, match="read"): _ = obj.value -def test_protected_field_getter_allows_derived_mask_to_override_base_deny(db0_fixture): +def test_protected_field_getter_uses_derived_class_own_masks(db0_fixture): account_id = ContextVar("protected_inheritance_allow_override_account_id") obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) db0.set_field_access(MemoProtectedDerivedFieldsClass, 123, (), "name") diff --git a/src/dbzero/object_model/class/Class.cpp b/src/dbzero/object_model/class/Class.cpp index 5c4b12fd..158de562 100644 --- a/src/dbzero/object_model/class/Class.cpp +++ b/src/dbzero/object_model/class/Class.cpp @@ -112,7 +112,7 @@ namespace db0::object_model , m_uid(this->fetchUID()) , m_member_cache(m_members, *this, this->getRefreshCallback()) { - if (hasOwnProtectFields()) { + if (isProtectFields()) { ensureFieldSafe(); } m_schema.postInit(getTotalFunc()); @@ -312,7 +312,22 @@ namespace db0::object_model } bool Class::isProtectFields() const { - return getClassFactory(*getFixture()).isProtectFields(*this); + if (m_protect_fields_cache) { + return *m_protect_fields_cache; + } + + m_protect_fields_cache = hasOwnProtectFields() + || (m_base_class_ptr && m_base_class_ptr->isProtectFields()); + return *m_protect_fields_cache; + } + + void Class::resetProtectFieldsCache() const + { + getClassFactory(*getFixture()).forAll([this](const Class &type) { + if (type.isBaseClass(*this)) { + type.m_protect_fields_cache.reset(); + } + }); } void Class::assertFieldSafeSupported() const @@ -347,7 +362,7 @@ namespace db0::object_model void Class::setProtectFields() { ensureFieldSafe(); modify().m_flags.set(ClassOptions::PROTECT_FIELDS, true); - getClassFactory(*getFixture()).resetProtectFieldsCache(*this); + resetProtectFieldsCache(); } void Class::resetProtectFields() { @@ -357,7 +372,7 @@ namespace db0::object_model << " because it inherits from a protect_fields base class"; } modify().m_flags.set(ClassOptions::PROTECT_FIELDS, false); - getClassFactory(*getFixture()).resetProtectFieldsCache(*this); + resetProtectFieldsCache(); } bool Class::hasFieldSafe() const @@ -423,28 +438,15 @@ namespace db0::object_model return {}; } - if (hasFieldSafe()) { - auto &field_safe = getFieldSafe(); - auto field_mask = field_safe.getFieldMaskManager().tryGetFieldMask(account_id); - auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member.m_name.c_str()); - if (maybe_offset && field_mask) { - auto mask = field_mask->getAssignedMask(*maybe_offset); - if (mask) { - return mask; - } - } - - maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member.m_field_id); - if (maybe_offset && field_mask) { - auto mask = field_mask->getAssignedMask(*maybe_offset); - if (mask && mask->value() != 0) { - return mask; - } - } + if (!hasFieldSafe()) { + return {}; } - if (m_base_class_ptr && m_base_class_ptr->isProtectFields()) { - return m_base_class_ptr->tryGetFieldAccess(account_id, member); + auto &field_safe = getFieldSafe(); + auto field_mask = field_safe.getFieldMaskManager().tryGetFieldMask(account_id); + auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member.m_field_id); + if (maybe_offset && field_mask) { + return field_mask->getAssignedMask(*maybe_offset); } return {}; @@ -461,9 +463,6 @@ namespace db0::object_model if (member) { return tryGetFieldAccess(account_id, *member); } - if (m_base_class_ptr && m_base_class_ptr->isProtectFields()) { - return m_base_class_ptr->tryGetFieldAccess(account_id, member_loc); - } return {}; } diff --git a/src/dbzero/object_model/class/Class.hpp b/src/dbzero/object_model/class/Class.hpp index be45bf1e..99d0033c 100644 --- a/src/dbzero/object_model/class/Class.hpp +++ b/src/dbzero/object_model/class/Class.hpp @@ -333,6 +333,7 @@ DB0_PACKED_END Schema m_schema; mutable std::optional m_field_safe; std::shared_ptr m_base_class_ptr; + mutable std::optional m_protect_fields_cache; // Field by-name index (cache) // values: member ID / assigned on initialization flag @@ -353,6 +354,7 @@ DB0_PACKED_END // callback for MemberID updates void onMemberIDUpdated(const MemberID &) const; void assertFieldSafeSupported() const; + void resetProtectFieldsCache() const; FieldSafe &ensureFieldSafe(); void openFieldSafe() const; // translate member's field ID into a unique key diff --git a/src/dbzero/object_model/class/ClassFactory.cpp b/src/dbzero/object_model/class/ClassFactory.cpp index 60aa8219..752b9eb9 100644 --- a/src/dbzero/object_model/class/ClassFactory.cpp +++ b/src/dbzero/object_model/class/ClassFactory.cpp @@ -137,25 +137,6 @@ namespace db0::object_model return type; } - bool ClassFactory::isProtectFields(const Class &type) const - { - auto cacheKey = type.getAddress(); - auto it = m_protect_fields_cache.find(cacheKey); - if (it != m_protect_fields_cache.end()) { - return it->second.m_protect_fields; - } - - auto baseClass = type.getBaseClassPtr(); - bool protectFields = type.hasOwnProtectFields() || (baseClass && isProtectFields(*baseClass)); - m_protect_fields_cache[cacheKey] = { protectFields }; - return protectFields; - } - - void ClassFactory::resetProtectFieldsCache(const Class &) const - { - m_protect_fields_cache.clear(); - } - std::shared_ptr ClassFactory::tryGetOrCreateType(TypeObjectPtr lang_type) { // disallow creating MemoBase type diff --git a/src/dbzero/object_model/class/ClassFactory.hpp b/src/dbzero/object_model/class/ClassFactory.hpp index 3089172d..20b7a2d6 100644 --- a/src/dbzero/object_model/class/ClassFactory.hpp +++ b/src/dbzero/object_model/class/ClassFactory.hpp @@ -126,15 +126,7 @@ DB0_PACKED_END // calculate class-ref from its address std::uint32_t getClassRef(Address class_addr) const; - bool isProtectFields(const Class &) const; - void resetProtectFieldsCache(const Class &) const; - private: - struct ProtectFieldsCacheItem - { - bool m_protect_fields = false; - }; - // Language specific type to dbzero class mapping mutable std::unordered_map > m_type_cache; // dbzero Class objects by pointer (may not have language specific type assigned yet) @@ -146,7 +138,6 @@ DB0_PACKED_END // buffers with keys for potential rollback mutable std::vector m_pending_types; mutable std::vector m_pending_ptrs; - mutable std::unordered_map m_protect_fields_cache; // starting address of the "types" slot const std::pair m_type_slot_addr_range; From b4d1fa2ba976d88442ce3808ec6693fd8dfc8bb2 Mon Sep 17 00:00:00 2001 From: Adrian Zawadzki Date: Mon, 18 May 2026 12:20:59 +0200 Subject: [PATCH 5/5] fix(test): fixed unit test --- src/dbzero/object_model/class/Class.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dbzero/object_model/class/Class.cpp b/src/dbzero/object_model/class/Class.cpp index 158de562..768f8e01 100644 --- a/src/dbzero/object_model/class/Class.cpp +++ b/src/dbzero/object_model/class/Class.cpp @@ -323,6 +323,7 @@ namespace db0::object_model void Class::resetProtectFieldsCache() const { + m_protect_fields_cache.reset(); getClassFactory(*getFixture()).forAll([this](const Class &type) { if (type.isBaseClass(*this)) { type.m_protect_fields_cache.reset();