familia 2.0.0.pre4 → 2.0.0.pre6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +11 -8
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +19 -3
  7. data/README.md +36 -157
  8. data/docs/overview.md +359 -0
  9. data/docs/wiki/API-Reference.md +347 -0
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +600 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +106 -0
  17. data/docs/wiki/Implementation-Guide.md +276 -0
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  20. data/docs/wiki/Security-Model.md +183 -0
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/lib/familia/base.rb +18 -27
  23. data/lib/familia/connection.rb +6 -5
  24. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  25. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  26. data/lib/familia/data_type/types/counter.rb +38 -0
  27. data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
  28. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  31. data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
  32. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  33. data/lib/familia/{datatype.rb → data_type.rb} +12 -14
  34. data/lib/familia/encryption/encrypted_data.rb +137 -0
  35. data/lib/familia/encryption/manager.rb +119 -0
  36. data/lib/familia/encryption/provider.rb +49 -0
  37. data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
  38. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  39. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
  40. data/lib/familia/encryption/registry.rb +50 -0
  41. data/lib/familia/encryption.rb +178 -0
  42. data/lib/familia/encryption_request_cache.rb +68 -0
  43. data/lib/familia/errors.rb +17 -3
  44. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  45. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
  46. data/lib/familia/features/encrypted_fields.rb +28 -0
  47. data/lib/familia/features/expiration.rb +107 -77
  48. data/lib/familia/features/quantization.rb +5 -9
  49. data/lib/familia/features/relatable_objects.rb +2 -4
  50. data/lib/familia/features/safe_dump.rb +14 -17
  51. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  52. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  53. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  54. data/lib/familia/features/transient_fields.rb +47 -0
  55. data/lib/familia/features.rb +40 -24
  56. data/lib/familia/field_type.rb +273 -0
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
  58. data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
  63. data/lib/familia/horreum/subclass/definition.rb +469 -0
  64. data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +30 -22
  67. data/lib/familia/logging.rb +14 -14
  68. data/lib/familia/settings.rb +39 -3
  69. data/lib/familia/utils.rb +45 -0
  70. data/lib/familia/version.rb +1 -1
  71. data/lib/familia.rb +3 -2
  72. data/try/core/base_enhancements_try.rb +115 -0
  73. data/try/core/connection_try.rb +0 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -5
  77. data/try/core/familia_extended_try.rb +3 -4
  78. data/try/core/familia_try.rb +1 -2
  79. data/try/core/persistence_operations_try.rb +297 -0
  80. data/try/core/pools_try.rb +2 -2
  81. data/try/core/secure_identifier_try.rb +0 -1
  82. data/try/core/settings_try.rb +0 -1
  83. data/try/core/utils_try.rb +0 -1
  84. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  85. data/try/data_types/counter_try.rb +93 -0
  86. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  87. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  88. data/try/{datatypes → data_types}/list_try.rb +1 -2
  89. data/try/data_types/lock_try.rb +133 -0
  90. data/try/{datatypes → data_types}/set_try.rb +1 -2
  91. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  92. data/try/{datatypes → data_types}/string_try.rb +1 -2
  93. data/try/debugging/README.md +32 -0
  94. data/try/debugging/cache_behavior_tracer.rb +91 -0
  95. data/try/debugging/debug_aad_process.rb +82 -0
  96. data/try/debugging/debug_concealed_internal.rb +59 -0
  97. data/try/debugging/debug_concealed_reveal.rb +61 -0
  98. data/try/debugging/debug_context_aad.rb +68 -0
  99. data/try/debugging/debug_context_simple.rb +80 -0
  100. data/try/debugging/debug_cross_context.rb +62 -0
  101. data/try/debugging/debug_database_load.rb +64 -0
  102. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  103. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  104. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  105. data/try/debugging/debug_field_decrypt.rb +74 -0
  106. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  107. data/try/debugging/debug_load_path.rb +66 -0
  108. data/try/debugging/debug_method_definition.rb +46 -0
  109. data/try/debugging/debug_method_resolution.rb +41 -0
  110. data/try/debugging/debug_minimal.rb +24 -0
  111. data/try/debugging/debug_provider.rb +68 -0
  112. data/try/debugging/debug_secure_behavior.rb +73 -0
  113. data/try/debugging/debug_string_class.rb +46 -0
  114. data/try/debugging/debug_test.rb +46 -0
  115. data/try/debugging/debug_test_design.rb +80 -0
  116. data/try/debugging/encryption_method_tracer.rb +138 -0
  117. data/try/debugging/provider_diagnostics.rb +110 -0
  118. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  119. data/try/edge_cases/json_serialization_try.rb +0 -1
  120. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  121. data/try/encryption/config_persistence_try.rb +192 -0
  122. data/try/encryption/encryption_core_try.rb +328 -0
  123. data/try/encryption/instance_variable_scope_try.rb +31 -0
  124. data/try/encryption/module_loading_try.rb +28 -0
  125. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  126. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  127. data/try/encryption/roundtrip_validation_try.rb +28 -0
  128. data/try/encryption/secure_memory_handling_try.rb +125 -0
  129. data/try/features/encrypted_fields_core_try.rb +125 -0
  130. data/try/features/encrypted_fields_integration_try.rb +216 -0
  131. data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
  132. data/try/features/encrypted_fields_security_try.rb +377 -0
  133. data/try/features/encryption_fields/aad_protection_try.rb +138 -0
  134. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  135. data/try/features/encryption_fields/context_isolation_try.rb +141 -0
  136. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  137. data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
  138. data/try/features/encryption_fields/fresh_key_try.rb +168 -0
  139. data/try/features/encryption_fields/key_rotation_try.rb +123 -0
  140. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  141. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  142. data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
  143. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  144. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  145. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  146. data/try/features/expiration_try.rb +0 -1
  147. data/try/features/feature_dependencies_try.rb +159 -0
  148. data/try/features/quantization_try.rb +0 -1
  149. data/try/features/real_feature_integration_try.rb +148 -0
  150. data/try/features/relatable_objects_try.rb +0 -1
  151. data/try/features/safe_dump_advanced_try.rb +0 -1
  152. data/try/features/safe_dump_try.rb +0 -1
  153. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  154. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  155. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  156. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  157. data/try/features/transient_fields_core_try.rb +181 -0
  158. data/try/features/transient_fields_integration_try.rb +260 -0
  159. data/try/helpers/test_helpers.rb +67 -0
  160. data/try/horreum/base_try.rb +157 -3
  161. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  162. data/try/horreum/field_categories_try.rb +118 -0
  163. data/try/horreum/field_definition_try.rb +96 -0
  164. data/try/horreum/initialization_try.rb +1 -2
  165. data/try/horreum/relations_try.rb +1 -2
  166. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  167. data/try/horreum/serialization_try.rb +41 -7
  168. data/try/memory/memory_basic_test.rb +73 -0
  169. data/try/memory/memory_detailed_test.rb +121 -0
  170. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  171. data/try/memory/memory_search_for_string.rb +83 -0
  172. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  173. data/try/models/customer_safe_dump_try.rb +1 -2
  174. data/try/models/customer_try.rb +1 -2
  175. data/try/models/datatype_base_try.rb +1 -2
  176. data/try/models/familia_object_try.rb +0 -1
  177. metadata +131 -23
  178. data/lib/familia/horreum/serialization.rb +0 -445
@@ -3,6 +3,6 @@
3
3
  module Familia
4
4
  # Version information for the Familia
5
5
  unless defined?(Familia::VERSION)
6
- VERSION = '2.0.0.pre4'
6
+ VERSION = '2.0.0.pre6'
7
7
  end
8
8
  end
data/lib/familia.rb CHANGED
@@ -73,13 +73,14 @@ module Familia
73
73
  require_relative 'familia/utils'
74
74
 
75
75
  extend SecureIdentifier
76
- extend Logging
77
76
  extend Connection
78
77
  extend Settings
78
+ extend Logging
79
79
  extend Utils
80
80
  end
81
81
 
82
82
  require_relative 'familia/base'
83
83
  require_relative 'familia/features'
84
- require_relative 'familia/datatype'
84
+ require_relative 'familia/data_type'
85
85
  require_relative 'familia/horreum'
86
+ require_relative 'familia/encryption'
@@ -0,0 +1,115 @@
1
+ # try/core/base_enhancements_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+
5
+ Familia.debug = false
6
+
7
+ # Base class provides default UUID generation
8
+ class BaseUuidTest < Familia::Horreum
9
+ identifier_field :id
10
+ field :id
11
+ end
12
+
13
+ # Empty class still has base functionality
14
+ class EmptyBaseTest < Familia::Horreum
15
+ end
16
+
17
+ @base_uuid = BaseUuidTest.new(id: 'uuid_test_1')
18
+
19
+ ## UUID generation creates unique identifiers
20
+ @uuid1 = @base_uuid.uuid
21
+ @uuid1
22
+ #=:> String
23
+
24
+ ## UUID is properly formatted
25
+ @uuid1
26
+ #=~>/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
27
+
28
+ ## UUID is memoized (same value on repeated calls)
29
+ @uuid2 = @base_uuid.uuid
30
+ @uuid1 == @uuid2
31
+ #=> true
32
+
33
+ ## Different instances get different UUIDs
34
+ @base_uuid2 = BaseUuidTest.new(id: 'uuid_test_2')
35
+ @base_uuid.uuid != @base_uuid2.uuid
36
+ #=> true
37
+
38
+ ## Base class provides ID generation
39
+ @generated_id1 = @base_uuid.generate_id
40
+ @generated_id1
41
+ #=:> String
42
+
43
+ ## Generated ID is memoized
44
+ @generated_id2 = @base_uuid.generate_id
45
+ @generated_id1 == @generated_id2
46
+ #=> true
47
+
48
+ ## Base class to_s method returns identifier
49
+ @base_uuid.to_s
50
+ #=> "uuid_test_1"
51
+
52
+ ## Feature registry is accessible
53
+ Familia::Base.features_available
54
+ #=:> Hash
55
+
56
+ ## Feature definitions registry is accessible
57
+ Familia::Base.feature_definitions
58
+ #=:> Hash
59
+
60
+ ## Base class includes proper modules
61
+ BaseUuidTest.ancestors.include?(Familia::Base)
62
+ #=> true
63
+
64
+ ## Feature methods are accessible through the class
65
+ BaseUuidTest.respond_to?(:feature)
66
+ #=> true
67
+
68
+ ## Class instance variables are properly initialized
69
+ BaseUuidTest.instance_variable_get(:@fields)
70
+ #=:> Array
71
+
72
+ ## Field definitions are properly initialized
73
+ BaseUuidTest.instance_variable_get(:@field_types)
74
+ #=:> Hash
75
+
76
+ ## Feature system is properly integrated
77
+ BaseUuidTest.respond_to?(:feature)
78
+ #=> true
79
+
80
+ ## Feature registration methods are available
81
+ Familia::Base.respond_to?(:add_feature)
82
+ #=> true
83
+
84
+ ## Valid identifiers work correctly
85
+ @base_uuid.identifier
86
+ #=> "uuid_test_1"
87
+
88
+ ## Empty class has base methods available
89
+ EmptyBaseTest.ancestors.include?(Familia::Base)
90
+ #=> true
91
+
92
+ ## Empty class can use feature system
93
+ EmptyBaseTest.respond_to?(:feature)
94
+ #=> true
95
+
96
+ ## Test Base module constants are defined
97
+ Familia::Base.features_available
98
+ #=:> Hash
99
+
100
+ ## Dump and load methods are set
101
+ Familia::Base.dump_method
102
+ #=> :to_json
103
+
104
+ ## Load method is set correctly
105
+ Familia::Base.load_method
106
+ #=> :from_json
107
+
108
+ ## Base module provides inspect with class name
109
+ @base_uuid.inspect.include?('BaseUuidTest')
110
+ #=> true
111
+
112
+ @base_uuid.destroy! rescue nil
113
+ @base_uuid2.destroy! rescue nil
114
+ @base_uuid = nil
115
+ @base_uuid2 = nil
@@ -1,6 +1,5 @@
1
1
  # try/core/connection_try.rb
2
2
 
3
- require_relative '../../lib/familia'
4
3
  require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false
@@ -0,0 +1,240 @@
1
+ # try/core/create_method_try.rb
2
+ #
3
+ # Comprehensive test coverage for the create method
4
+ # Tests the correct exception type and error message handling
5
+
6
+ require_relative '../helpers/test_helpers'
7
+
8
+ # Test class for create method behavior
9
+ class CreateTestModel < Familia::Horreum
10
+ identifier_field :id
11
+ field :id
12
+ field :name
13
+ field :value
14
+ end
15
+
16
+ # Clean up any existing test data
17
+ cleanup_keys = []
18
+ begin
19
+ existing_test_keys = Familia.dbclient.keys('createtestmodel:*')
20
+ cleanup_keys.concat(existing_test_keys)
21
+ Familia.dbclient.del(*existing_test_keys) if existing_test_keys.any?
22
+ rescue => e
23
+ # Ignore cleanup errors
24
+ end
25
+
26
+ @test_id_counter = 0
27
+ def next_test_id
28
+ @test_id_counter += 1
29
+ "create-test-#{Time.now.to_i}-#{@test_id_counter}"
30
+ end
31
+
32
+ # =============================================
33
+ # 1. Basic create method functionality
34
+ # =============================================
35
+
36
+ ## create method successfully creates new object
37
+ @test_id = next_test_id
38
+ @created_obj = CreateTestModel.create(id: @test_id, name: 'Created Object', value: 'test_value')
39
+ [@created_obj.class, @created_obj.exists?, @created_obj.name]
40
+ #=> [CreateTestModel, true, 'Created Object']
41
+
42
+ ## create method returns the created object
43
+ @created_obj.is_a?(CreateTestModel)
44
+ #=> true
45
+
46
+ ## create method persists object fields
47
+ @created_obj.refresh!
48
+ [@created_obj.name, @created_obj.value]
49
+ #=> ['Created Object', 'test_value']
50
+
51
+ # =============================================
52
+ # 2. Duplicate creation error handling
53
+ # =============================================
54
+
55
+ ## create method raises RecordExistsError for duplicate
56
+ begin
57
+ CreateTestModel.create(id: @test_id, name: 'Duplicate Attempt')
58
+ false # Should not reach here
59
+ rescue => e
60
+ e.class
61
+ end
62
+ #=> Familia::RecordExistsError
63
+
64
+ ## RecordExistsError includes the dbkey in the message
65
+ begin
66
+ CreateTestModel.create(id: @test_id, name: 'Another Duplicate')
67
+ false # Should not reach here
68
+ rescue Familia::RecordExistsError => e
69
+ expected_dbkey = "createtestmodel:#{@test_id}:object"
70
+ e.message.include?(expected_dbkey)
71
+ end
72
+ #=> true
73
+
74
+ ## RecordExistsError message follows consistent format
75
+ begin
76
+ CreateTestModel.create(id: @test_id, name: 'Yet Another Duplicate')
77
+ false # Should not reach here
78
+ rescue Familia::RecordExistsError => e
79
+ e.message.start_with?('Key already exists:')
80
+ end
81
+ #=> true
82
+
83
+ ## RecordExistsError exposes key property for programmatic access
84
+ @final_test_id = next_test_id
85
+ CreateTestModel.create(id: @final_test_id, name: 'Setup for Key Test')
86
+
87
+ begin
88
+ CreateTestModel.create(id: @final_test_id, name: 'Key Test Duplicate')
89
+ false # Should not reach here
90
+ rescue Familia::RecordExistsError => e
91
+ # Key should be accessible and contain the identifier
92
+ [e.respond_to?(:key), e.key.include?(@final_test_id)]
93
+ end
94
+ #=> [true, true]
95
+
96
+ # =============================================
97
+ # 3. Edge cases and error conditions
98
+ # =============================================
99
+
100
+ ## create with empty identifier raises NoIdentifier error
101
+ CreateTestModel.create(id: '')
102
+ #=!> Familia::NoIdentifier
103
+
104
+ ## create with nil identifier raises NoIdentifier error
105
+ CreateTestModel.create(id: nil)
106
+ #=!> Familia::NoIdentifier
107
+
108
+ ## create with only some fields set
109
+ @partial_id = next_test_id
110
+ @partial_obj = CreateTestModel.create(id: @partial_id, name: 'Partial Object')
111
+ [@partial_obj.exists?, @partial_obj.name, @partial_obj.value]
112
+ #=> [true, 'Partial Object', nil]
113
+
114
+ ## create with no additional fields (only identifier)
115
+ @minimal_id = next_test_id
116
+ @minimal_obj = CreateTestModel.create(id: @minimal_id)
117
+ [@minimal_obj.exists?, @minimal_obj.id]
118
+ #=> [true, @minimal_id]
119
+
120
+ # =============================================
121
+ # 4. Concurrency and transaction behavior
122
+ # =============================================
123
+
124
+ ## create is atomic - no partial state on failure
125
+ @concurrent_id = next_test_id
126
+ @first_obj = CreateTestModel.create(id: @concurrent_id, name: 'First')
127
+
128
+ # Verify first object exists
129
+ first_exists = @first_obj.exists?
130
+
131
+ # Attempt to create duplicate should not affect existing object
132
+ begin
133
+ CreateTestModel.create(id: @concurrent_id, name: 'Concurrent Attempt')
134
+ false # Should not reach here
135
+ rescue Familia::RecordExistsError
136
+ # Original object should be unchanged
137
+ @first_obj.refresh!
138
+ @first_obj.name == 'First'
139
+ end
140
+ #=> true
141
+
142
+ ## create failure doesn't leave partial data
143
+ before_failed_create = Familia.dbclient.keys("createtestmodel:#{@concurrent_id}:*").length
144
+ begin
145
+ CreateTestModel.create(id: @concurrent_id, name: 'Should Fail')
146
+ rescue Familia::RecordExistsError
147
+ # Should not create any additional keys
148
+ after_failed_create = Familia.dbclient.keys("createtestmodel:#{@concurrent_id}:*").length
149
+ after_failed_create == before_failed_create
150
+ end
151
+ #=> true
152
+
153
+ # =============================================
154
+ # 5. Consistency with save_if_not_exists
155
+ # =============================================
156
+
157
+ ## Both create and save_if_not_exists raise same error type for duplicates
158
+ @consistency_id = next_test_id
159
+ @consistency_obj = CreateTestModel.create(id: @consistency_id, name: 'Consistency Test')
160
+
161
+ # Test create raises RecordExistsError
162
+ create_error_class = begin
163
+ CreateTestModel.create(id: @consistency_id, name: 'Create Duplicate')
164
+ nil
165
+ rescue => e
166
+ e.class
167
+ end
168
+
169
+ # Test save_if_not_exists raises RecordExistsError
170
+ sine_error_class = begin
171
+ duplicate_obj = CreateTestModel.new(id: @consistency_id, name: 'SINE Duplicate')
172
+ duplicate_obj.save_if_not_exists
173
+ nil
174
+ rescue => e
175
+ e.class
176
+ end
177
+
178
+ [create_error_class, sine_error_class]
179
+ #=> [Familia::RecordExistsError, Familia::RecordExistsError]
180
+
181
+ ## Both methods have similar error message patterns
182
+ @error_comparison_id = next_test_id
183
+ CreateTestModel.create(id: @error_comparison_id, name: 'Error Comparison')
184
+
185
+ create_error_msg = begin
186
+ CreateTestModel.create(id: @error_comparison_id, name: 'Create Error')
187
+ nil
188
+ rescue => e
189
+ e.message
190
+ end
191
+
192
+ sine_error_msg = begin
193
+ CreateTestModel.new(id: @error_comparison_id, name: 'SINE Error').save_if_not_exists
194
+ nil
195
+ rescue => e
196
+ e.message
197
+ end
198
+
199
+ # Both should reference the same key concept
200
+ [create_error_msg.include?('already exists'), sine_error_msg.include?('already exists')]
201
+ #=> [true, true]
202
+
203
+ # =============================================
204
+ # 6. Integration with different field types
205
+ # =============================================
206
+
207
+ ## create works with complex field values
208
+ @complex_id = next_test_id
209
+ @complex_obj = CreateTestModel.create(
210
+ id: @complex_id,
211
+ name: 'Complex Object',
212
+ value: { nested: 'data', array: [1, 2, 3] }
213
+ )
214
+ [@complex_obj.exists?, @complex_obj.value[:nested]]
215
+ #=> [true, 'data']
216
+
217
+ # =============================================
218
+ # 7. Class vs instance method consistency
219
+ # =============================================
220
+
221
+ ## Class.create and instance.save_if_not_exists have consistent existence checking
222
+ @consistency_check_id = next_test_id
223
+
224
+ # Create via class method
225
+ @class_created = CreateTestModel.create(id: @consistency_check_id, name: 'Class Created')
226
+
227
+ # Both class and instance methods should see the object as existing
228
+ class_sees_exists = CreateTestModel.exists?(@consistency_check_id)
229
+ instance_sees_exists = @class_created.exists?
230
+
231
+ [class_sees_exists, instance_sees_exists]
232
+ #=> [true, true]
233
+
234
+ # =============================================
235
+ # Cleanup
236
+ # =============================================
237
+
238
+ # Clean up all test data
239
+ test_keys = Familia.dbclient.keys('createtestmodel:*')
240
+ Familia.dbclient.del(*test_keys) if test_keys.any?
@@ -0,0 +1,299 @@
1
+ # try/core/database_consistency_try.rb
2
+ #
3
+ # Database consistency verification and edge case testing
4
+ # Complements persistence_operations_try.rb with deeper consistency checks
5
+
6
+ require_relative '../helpers/test_helpers'
7
+
8
+ # Test class with different field types for consistency verification
9
+ class ConsistencyTestModel < Familia::Horreum
10
+ identifier_field :id
11
+ field :id
12
+ field :name
13
+ field :email
14
+ field :active
15
+ field :metadata # For complex data types
16
+ end
17
+
18
+ # Clean up existing test data
19
+ cleanup_keys = []
20
+ begin
21
+ existing_test_keys = Familia.dbclient.keys('consistencytestmodel:*')
22
+ cleanup_keys.concat(existing_test_keys)
23
+ Familia.dbclient.del(*existing_test_keys) if existing_test_keys.any?
24
+ rescue => e
25
+ # Ignore cleanup errors
26
+ end
27
+
28
+ @test_id_counter = 0
29
+ def next_test_id
30
+ @test_id_counter += 1
31
+ "consistency-#{Time.now.to_i}-#{@test_id_counter}"
32
+ end
33
+
34
+ # =============================================
35
+ # 1. Database Consistency Verification
36
+ # =============================================
37
+
38
+ ## Redis key structure follows expected pattern
39
+ @key_test = ConsistencyTestModel.new(id: next_test_id, name: 'Key Test')
40
+ @key_test.save
41
+ dbkey = @key_test.dbkey
42
+ key_parts = dbkey.split(':')
43
+ # Should have pattern: [prefix, identifier, suffix]
44
+ [key_parts.length >= 3, key_parts.include?(@key_test.identifier), key_parts.last]
45
+ #=> [true, true, 'object']
46
+
47
+ ## Field serialization/deserialization roundtrips correctly
48
+ @serial_test = ConsistencyTestModel.new(id: next_test_id)
49
+ # Test different data types
50
+ @serial_test.name = 'Serialization Test'
51
+ @serial_test.active = true
52
+ @serial_test.metadata = { key: 'value', array: [1, 2, 3] }
53
+ @serial_test.save
54
+
55
+ # Refresh and verify data integrity
56
+ @serial_test.refresh!
57
+ [@serial_test.name, @serial_test.active, @serial_test.metadata]
58
+ #=> ['Serialization Test', 'true', {:key=>'value', :array=>[1, 2, 3]}]
59
+
60
+ ## Hash field count matches object field count
61
+ expected_fields = @serial_test.class.persistent_fields.length
62
+ redis_field_count = Familia.dbclient.hlen(@serial_test.dbkey)
63
+ actual_object_fields = @serial_test.to_h.keys.length
64
+ # All should match (redis may have fewer due to nil exclusion)
65
+ [expected_fields >= redis_field_count, redis_field_count, actual_object_fields]
66
+ #=> [true, 5, 5]
67
+
68
+ ## Memory vs persistence state consistency after save
69
+ @consistency_obj = ConsistencyTestModel.new(id: next_test_id, name: 'Memory Test', email: 'test@example.com')
70
+ @consistency_obj.save
71
+
72
+ # Get memory state
73
+ memory_name = @consistency_obj.name
74
+ memory_email = @consistency_obj.email
75
+
76
+ # Get persistence state
77
+ redis_name = Familia.dbclient.hget(@consistency_obj.dbkey, 'name')
78
+ redis_email = Familia.dbclient.hget(@consistency_obj.dbkey, 'email')
79
+
80
+ [memory_name == redis_name, memory_email == redis_email]
81
+ #=> [true, true]
82
+
83
+ # =============================================
84
+ # 2. Concurrent Modification Detection
85
+ # =============================================
86
+
87
+ ## Multiple objects with same identifier maintain consistency
88
+ obj1 = ConsistencyTestModel.new(id: next_test_id, name: 'Object 1')
89
+ obj1.save
90
+ obj1_id = obj1.identifier
91
+
92
+ # Create second object with same ID (simulating concurrent access)
93
+ obj2 = ConsistencyTestModel.new(id: obj1_id, name: 'Object 2')
94
+ obj2.save # This overwrites obj1's data
95
+
96
+ # Both objects should now see the updated data when refreshed
97
+ obj1.refresh!
98
+ obj2.refresh!
99
+ [obj1.name == obj2.name, obj1.name]
100
+ #=> [true, 'Object 2']
101
+
102
+ ## exists? consistency under concurrent modifications
103
+ @concurrent_mod = ConsistencyTestModel.new(id: next_test_id, name: 'Concurrent')
104
+ @concurrent_mod.save
105
+ before_modify = @concurrent_mod.exists?
106
+
107
+ # Simulate external modification
108
+ Familia.dbclient.hset(@concurrent_mod.dbkey, 'name', 'Modified Externally')
109
+ after_modify = @concurrent_mod.exists?
110
+
111
+ # exists? should still return true regardless of field changes
112
+ [before_modify, after_modify]
113
+ #=> [true, true]
114
+
115
+ # =============================================
116
+ # 3. Edge Cases and Error Conditions
117
+ # =============================================
118
+
119
+ ## Corrupted data handling (malformed JSON in complex fields)
120
+ @corrupt_test = ConsistencyTestModel.new(id: next_test_id)
121
+ @corrupt_test.save
122
+
123
+ # Manually insert malformed JSON
124
+ Familia.dbclient.hset(@corrupt_test.dbkey, 'metadata', '{"invalid": json}')
125
+
126
+ # Object should handle corrupted data gracefully
127
+ begin
128
+ @corrupt_test.refresh!
129
+ # metadata should be returned as string since JSON parsing failed
130
+ @corrupt_test.metadata.class
131
+ rescue => e
132
+ "Error: #{e.class}"
133
+ end
134
+ #=> String
135
+
136
+ ## Empty hash object edge case (critical for check_size parameter)
137
+ @empty_hash = ConsistencyTestModel.new(id: next_test_id)
138
+ # Save creates the hash with identifier
139
+ @empty_hash.save
140
+
141
+ # Manually remove all fields to create an empty hash
142
+ # First add a temp field then remove it, which creates empty hash in some Redis versions
143
+ Familia.dbclient.hset(@empty_hash.dbkey, 'temp_field', 'temp_value')
144
+ Familia.dbclient.hdel(@empty_hash.dbkey, 'temp_field')
145
+ # Now remove all remaining fields to create truly empty hash
146
+ all_fields = Familia.dbclient.hkeys(@empty_hash.dbkey)
147
+ Familia.dbclient.hdel(@empty_hash.dbkey, *all_fields) if all_fields.any?
148
+
149
+ # exists? behavior with empty hash
150
+ key_exists_raw = Familia.dbclient.exists(@empty_hash.dbkey) > 0
151
+ hash_length = Familia.dbclient.hlen(@empty_hash.dbkey)
152
+ obj_exists_with_check = @empty_hash.exists?(check_size: true)
153
+ obj_exists_without_check = @empty_hash.exists?(check_size: false)
154
+
155
+ [key_exists_raw, hash_length, obj_exists_without_check, obj_exists_with_check]
156
+ #=> [false, 0, false, false]
157
+
158
+ ## Transaction isolation verification
159
+ @tx_test = ConsistencyTestModel.new(id: next_test_id, name: 'Transaction Test')
160
+ @tx_test.save
161
+
162
+ # Verify transaction doesn't interfere with exists? calls
163
+ result = @tx_test.transaction do |conn|
164
+ # During transaction, exists? should still work
165
+ exists_in_tx = @tx_test.exists?
166
+ conn.hset(@tx_test.dbkey, 'active', 'true')
167
+ exists_in_tx
168
+ end
169
+
170
+ exists_after_tx = @tx_test.exists?
171
+ [result, exists_after_tx]
172
+ #=> [[0], true]
173
+
174
+ # =============================================
175
+ # 4. Performance Consistency
176
+ # =============================================
177
+
178
+ ## exists? performance is consistent regardless of object size
179
+ @small_obj = ConsistencyTestModel.new(id: next_test_id, name: 'Small')
180
+ @small_obj.save
181
+
182
+ @large_obj = ConsistencyTestModel.new(id: next_test_id)
183
+ @large_obj.name = 'Large Object'
184
+ @large_obj.email = 'large@example.com'
185
+ @large_obj.metadata = { large_data: 'x' * 1000 }
186
+ @large_obj.save
187
+
188
+ # exists? should work equally fast for both
189
+ small_exists = @small_obj.exists?
190
+ large_exists = @large_obj.exists?
191
+
192
+ [small_exists, large_exists]
193
+ #=> [true, true]
194
+
195
+ ## Batch operations maintain consistency
196
+ @batch_obj = ConsistencyTestModel.new(id: next_test_id, name: 'Original Batch')
197
+ @batch_obj.save
198
+
199
+ # Batch update multiple fields
200
+ batch_result = @batch_obj.batch_update(
201
+ name: 'Updated Batch',
202
+ email: 'batch@example.com',
203
+ active: true
204
+ )
205
+
206
+ # Verify exists? still works correctly after batch operations
207
+ exists_after_batch = @batch_obj.exists?
208
+ [@batch_obj.name, batch_result.successful?, exists_after_batch]
209
+ #=> ['Updated Batch', true, true]
210
+
211
+ # =============================================
212
+ # 5. Integration Consistency with Features
213
+ # =============================================
214
+
215
+ ## Transient fields don't affect exists? behavior
216
+ class TransientConsistencyTest < Familia::Horreum
217
+ identifier_field :id
218
+ field :id
219
+ field :name
220
+ transient_field :temp_value
221
+ end
222
+
223
+ @transient_obj = TransientConsistencyTest.new(id: next_test_id, name: 'Transient Test')
224
+ @transient_obj.temp_value = 'This should not persist'
225
+ @transient_obj.save
226
+
227
+ # exists? should work normally despite transient fields
228
+ exists_with_transient = @transient_obj.exists?
229
+
230
+ @transient_obj.refresh!
231
+ # Transient field should be nil after refresh, but exists? should still work
232
+ transient_nil = @transient_obj.temp_value.nil?
233
+ exists_after_refresh = @transient_obj.exists?
234
+
235
+ [exists_with_transient, transient_nil, exists_after_refresh]
236
+ #=> [true, true, true]
237
+
238
+ # =============================================
239
+ # 6. Database Command Consistency
240
+ # =============================================
241
+
242
+ ## save/exists?/destroy lifecycle is consistent
243
+ @lifecycle_obj = ConsistencyTestModel.new(id: next_test_id, name: 'Lifecycle Test')
244
+
245
+ # Initial state
246
+ initial_exists = @lifecycle_obj.exists?
247
+
248
+ # After save
249
+ @lifecycle_obj.save
250
+ saved_exists = @lifecycle_obj.exists?
251
+
252
+ # After modification
253
+ @lifecycle_obj.name = 'Modified Lifecycle'
254
+ @lifecycle_obj.save
255
+ modified_exists = @lifecycle_obj.exists?
256
+
257
+ # After destroy
258
+ @lifecycle_obj.destroy!
259
+ destroyed_exists = @lifecycle_obj.exists?
260
+
261
+ [initial_exists, saved_exists, modified_exists, destroyed_exists]
262
+ #=> [false, true, true, false]
263
+
264
+ ## Field removal doesn't break exists?
265
+ @field_removal = ConsistencyTestModel.new(id: next_test_id, name: 'Field Removal')
266
+ @field_removal.save
267
+
268
+ # Remove a field manually
269
+ Familia.dbclient.hdel(@field_removal.dbkey, 'name')
270
+
271
+ # exists? should still work
272
+ exists_after_field_removal = @field_removal.exists?
273
+ remaining_fields = Familia.dbclient.hlen(@field_removal.dbkey)
274
+
275
+ [exists_after_field_removal, remaining_fields > 0]
276
+ #=> [true, true]
277
+
278
+ ## Class vs instance exists? always consistent
279
+ @class_instance_test = ConsistencyTestModel.new(id: next_test_id, name: 'Class Instance Test')
280
+ @class_instance_test.save
281
+
282
+ # Multiple checks should always be consistent
283
+ results = 5.times.map do
284
+ class_result = ConsistencyTestModel.exists?(@class_instance_test.identifier)
285
+ instance_result = @class_instance_test.exists?
286
+ class_result == instance_result
287
+ end
288
+
289
+ results.all?
290
+ #=> true
291
+
292
+ # =============================================
293
+ # Cleanup
294
+ # =============================================
295
+
296
+ # Clean up all test data
297
+ test_keys = Familia.dbclient.keys('consistencytestmodel:*')
298
+ test_keys.concat(Familia.dbclient.keys('transientconsistencytest:*'))
299
+ Familia.dbclient.del(*test_keys) if test_keys.any?