@@ -81,6 +81,25 @@ def __init__(self, count):
8181 setattr (self , f"init_field_{ i } " , values [i % len (values )](i ))
8282
8383
84+ @db0 .memo
85+ @dataclass
86+ class MemoUnprotectedBaseFieldsClass :
87+ base_value : str
88+
89+
90+ @db0 .memo (protect_fields = True )
91+ @dataclass
92+ class MemoProtectedDerivedFieldsClass (MemoUnprotectedBaseFieldsClass ):
93+ name : str
94+ value : int
95+
96+
97+ @db0 .memo
98+ @dataclass
99+ class MemoImplicitlyProtectedDerivedFieldsClass (MemoProtectedDerivedFieldsClass ):
100+ derived_value : float
101+
102+
84103def get_memo_class_object (obj ):
85104 return db0 .get_memo_class (obj ).get_class ()
86105
@@ -242,6 +261,60 @@ def test_set_field_access_requires_protected_class(db0_fixture):
242261 assert False , "set_field_access should fail for unprotected classes"
243262
244263
264+ def test_protect_fields_is_inherited_by_derived_memo_classes (db0_fixture ):
265+ obj = MemoImplicitlyProtectedDerivedFieldsClass ("base" , "alpha" , 1 , 1.5 )
266+
267+ assert get_memo_class_object (obj ).get_type_flags ()["protect_fields" ] is True
268+ db0 .set_field_access (MemoImplicitlyProtectedDerivedFieldsClass , 123 , (FieldAccess .READ ,), "derived_value" )
269+
270+
271+ def test_protect_fields_cannot_be_disabled_in_derived_memo_class (db0_fixture ):
272+ with pytest .raises (RuntimeError , match = "protect_fields.*base" ):
273+ @db0 .memo (protect_fields = False )
274+ @dataclass
275+ class ExplicitlyUnprotectedDerived (MemoProtectedDerivedFieldsClass ):
276+ extra_value : str
277+
278+
279+ def test_set_field_access_still_rejects_unprotected_base_of_protected_class (db0_fixture ):
280+ MemoImplicitlyProtectedDerivedFieldsClass ("base" , "alpha" , 1 , 1.5 )
281+
282+ with pytest .raises (RuntimeError , match = "protected fields" ):
283+ db0 .set_field_access (MemoUnprotectedBaseFieldsClass , 123 , (FieldAccess .READ ,), "base_value" )
284+
285+
286+ def test_reset_protect_fields_rejects_inherited_protection (db0_fixture ):
287+ obj = MemoImplicitlyProtectedDerivedFieldsClass ("base" , "alpha" , 1 , 1.5 )
288+ memo_class = get_memo_class_object (obj )
289+
290+ with pytest .raises (RuntimeError , match = "inherits.*protect_fields" ):
291+ db0 .reset_protect_fields (MemoImplicitlyProtectedDerivedFieldsClass )
292+
293+ assert memo_class .get_type_flags ()["protect_fields" ] is True
294+
295+
296+ def test_class_reset_protect_fields_rejects_inherited_protection (db0_fixture ):
297+ obj = MemoImplicitlyProtectedDerivedFieldsClass ("base" , "alpha" , 1 , 1.5 )
298+ memo_class = get_memo_class_object (obj )
299+
300+ with pytest .raises (RuntimeError , match = "inherits.*protect_fields" ):
301+ memo_class .reset_protect_fields ()
302+
303+ assert memo_class .get_type_flags ()["protect_fields" ] is True
304+
305+
306+ def test_explicit_true_is_allowed_on_class_derived_from_protected_base (db0_fixture ):
307+ @db0 .memo (protect_fields = True )
308+ @dataclass
309+ class ExplicitlyProtectedDerived (MemoProtectedDerivedFieldsClass ):
310+ extra_value : str
311+
312+ obj = ExplicitlyProtectedDerived ("base" , "alpha" , 1 , "extra" )
313+
314+ assert get_memo_class_object (obj ).get_type_flags ()["protect_fields" ] is True
315+ db0 .set_field_access (ExplicitlyProtectedDerived , 123 , (FieldAccess .READ ,), "extra_value" )
316+
317+
245318def test_set_field_access_accepts_unknown_field_names (db0_fixture ):
246319 MemoProtectedFieldsClass ("alpha" , 1 )
247320
@@ -376,6 +449,156 @@ def test_protected_field_getter_requires_read_access(db0_fixture):
376449 _ = obj .value
377450
378451
452+ def test_protected_field_getter_does_not_inherit_base_class_masks (db0_fixture ):
453+ account_id = ContextVar ("protected_inheritance_account_id" )
454+ obj = MemoImplicitlyProtectedDerivedFieldsClass ("base" , "alpha" , 1 , 1.5 )
455+ db0 .set_field_access (MemoProtectedDerivedFieldsClass , 123 , (FieldAccess .READ ,), "base_value" , "name" )
456+ db0 .set_field_access (MemoImplicitlyProtectedDerivedFieldsClass , 123 , (FieldAccess .READ ,), "derived_value" )
457+ db0 ._init_data_masking (account_id )
458+ account_id .set (123 )
459+
460+ assert obj .derived_value == 1.5
461+ with pytest .raises (PermissionError , match = "read" ):
462+ _ = obj .base_value
463+ with pytest .raises (PermissionError , match = "read" ):
464+ _ = obj .name
465+ with pytest .raises (PermissionError , match = "read" ):
466+ _ = obj .value
467+
468+
469+ def test_protected_field_getter_uses_derived_class_own_masks (db0_fixture ):
470+ account_id = ContextVar ("protected_inheritance_allow_override_account_id" )
471+ obj = MemoImplicitlyProtectedDerivedFieldsClass ("base" , "alpha" , 1 , 1.5 )
472+ db0 .set_field_access (MemoProtectedDerivedFieldsClass , 123 , (), "name" )
473+ db0 .set_field_access (MemoImplicitlyProtectedDerivedFieldsClass , 123 , (FieldAccess .READ ,), "name" )
474+ db0 ._init_data_masking (account_id )
475+ account_id .set (123 )
476+
477+ assert obj .name == "alpha"
478+ with pytest .raises (PermissionError , match = "read" ):
479+ _ = obj .value
480+
481+
482+ def test_inherited_protect_fields_survives_reopen_without_parameter (db0_fixture ):
483+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-inherited-reopen-base" )
484+ @dataclass
485+ class ReopenBase :
486+ base_value : str
487+
488+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-inherited-reopen-mid" , protect_fields = True )
489+ @dataclass
490+ class ReopenMid (ReopenBase ):
491+ name : str
492+
493+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-inherited-reopen-derived" )
494+ @dataclass
495+ class ReopenDerived (ReopenMid ):
496+ value : int
497+
498+ obj = ReopenDerived ("base" , "alpha" , 1 )
499+ obj_id = db0 .uuid (obj )
500+ assert get_memo_class_object (obj ).get_type_flags ()["protect_fields" ] is True
501+ db0 .commit ()
502+
503+ db0 .close ()
504+ db0 .init (DB0_DIR )
505+ db0 .open ("my-test-prefix" )
506+
507+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-inherited-reopen-base" )
508+ @dataclass
509+ class ReopenBaseAfter :
510+ base_value : str
511+
512+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-inherited-reopen-mid" )
513+ @dataclass
514+ class ReopenMidAfter (ReopenBaseAfter ):
515+ name : str
516+
517+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-inherited-reopen-derived" )
518+ @dataclass
519+ class ReopenDerivedAfter (ReopenMidAfter ):
520+ value : int
521+
522+ obj = db0 .fetch (ReopenDerivedAfter , obj_id )
523+ assert get_memo_class_object (obj ).get_type_flags ()["protect_fields" ] is True
524+ db0 .set_field_access (ReopenDerivedAfter , 123 , (FieldAccess .READ ,), "value" )
525+
526+
527+ def test_derived_class_inherits_protect_fields_enabled_on_base_after_materialization (db0_fixture ):
528+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-base-enabled-later-base" )
529+ @dataclass
530+ class BaseBefore :
531+ base_value : str
532+
533+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-base-enabled-later-derived" )
534+ @dataclass
535+ class DerivedBefore (BaseBefore ):
536+ value : int
537+
538+ obj = DerivedBefore ("base" , 1 )
539+ obj_id = db0 .uuid (obj )
540+ assert get_memo_class_object (obj ).get_type_flags ()["protect_fields" ] is False
541+ with pytest .raises (RuntimeError , match = "protected fields" ):
542+ db0 .set_field_access (DerivedBefore , 123 , (FieldAccess .READ ,), "value" )
543+ db0 .commit ()
544+
545+ db0 .close ()
546+ db0 .init (DB0_DIR )
547+ db0 .open ("my-test-prefix" )
548+
549+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-base-enabled-later-base" , protect_fields = True )
550+ @dataclass
551+ class BaseAfter :
552+ base_value : str
553+
554+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-base-enabled-later-derived" )
555+ @dataclass
556+ class DerivedAfter (BaseAfter ):
557+ value : int
558+
559+ obj = db0 .fetch (DerivedAfter , obj_id )
560+ assert get_memo_class_object (obj ).get_type_flags ()["protect_fields" ] is True
561+ db0 .set_field_access (DerivedAfter , 123 , (FieldAccess .READ ,), "value" )
562+
563+
564+ def test_derived_class_stops_inheriting_protect_fields_after_base_reset (db0_fixture ):
565+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-base-reset-base" , protect_fields = True )
566+ @dataclass
567+ class BaseBefore :
568+ base_value : str
569+
570+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-base-reset-derived" )
571+ @dataclass
572+ class DerivedBefore (BaseBefore ):
573+ value : int
574+
575+ obj = DerivedBefore ("base" , 1 )
576+ obj_id = db0 .uuid (obj )
577+ assert get_memo_class_object (obj ).get_type_flags ()["protect_fields" ] is True
578+ db0 .commit ()
579+
580+ db0 .close ()
581+ db0 .init (DB0_DIR )
582+ db0 .open ("my-test-prefix" )
583+
584+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-base-reset-base" , protect_fields = False )
585+ @dataclass
586+ class BaseAfter :
587+ base_value : str
588+
589+ @db0 .memo (id = "dbzero-software/dbzero/tests/protected-base-reset-derived" )
590+ @dataclass
591+ class DerivedAfter (BaseAfter ):
592+ value : int
593+
594+ obj = db0 .fetch (DerivedAfter , obj_id )
595+ db0 .reset_protect_fields (BaseAfter )
596+
597+ assert get_memo_class_object (obj ).get_type_flags ()["protect_fields" ] is False
598+ with pytest .raises (RuntimeError , match = "protected fields" ):
599+ db0 .set_field_access (DerivedAfter , 123 , (FieldAccess .READ ,), "value" )
600+
601+
379602def test_protected_field_getter_returns_missing_value_placeholder (db0_fixture ):
380603 account_id = ContextVar ("protected_placeholder_account_id" )
381604 missing_value = object ()
0 commit comments