familia 2.0.0.pre5 → 2.0.0.pre7

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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -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 +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -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 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. data/try/features/relatable_objects_try.rb +0 -220
@@ -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?
@@ -72,21 +72,42 @@ end
72
72
  begin
73
73
  raise Familia::KeyNotFoundError.new('test:key')
74
74
  rescue Familia::KeyNotFoundError => e
75
- e.message.include?('Key not found in Redis')
75
+ e.message.include?('Key not found')
76
76
  end
77
77
  #=> true
78
78
 
79
79
  ## KeyNotFoundError has custom message again
80
80
  raise Familia::KeyNotFoundError.new('test:key')
81
- #=!> error.message.include?("Key not found in Redis")
81
+ #=!> error.message.include?("Key not found")
82
82
  #=!> error.class == Familia::KeyNotFoundError
83
83
 
84
+ ## RecordExistsError stores key
85
+ begin
86
+ raise Familia::RecordExistsError.new('existing:key')
87
+ rescue Familia::RecordExistsError => e
88
+ e.key
89
+ end
90
+ #=> "existing:key"
91
+
92
+ ## RecordExistsError has custom message
93
+ begin
94
+ raise Familia::RecordExistsError.new('existing:key')
95
+ rescue Familia::RecordExistsError => e
96
+ e.message.include?('Key already exists')
97
+ end
98
+ #=> true
99
+
100
+ ## RecordExistsError inherits from NonUniqueKey
101
+ Familia::RecordExistsError.superclass
102
+ #=> Familia::NonUniqueKey
103
+
84
104
  ## All error classes inherit from Problem
85
105
  [
86
106
  Familia::NoIdentifier,
87
107
  Familia::NonUniqueKey,
88
108
  Familia::HighRiskFactor,
89
109
  Familia::NotConnected,
90
- Familia::KeyNotFoundError
91
- ].all? { |klass| klass.superclass == Familia::Problem }
110
+ Familia::KeyNotFoundError,
111
+ Familia::RecordExistsError
112
+ ].all? { |klass| klass.superclass == Familia::Problem || klass.superclass.superclass == Familia::Problem }
92
113
  ##=> true
@@ -4,7 +4,7 @@ require_relative '../helpers/test_helpers'
4
4
 
5
5
  ## Check for help class
6
6
  Bone.related_fields.keys # consistent b/c hashes are ordered
7
- #=> [:owners, :tags, :metrics, :props, :value]
7
+ #=> [:owners, :tags, :metrics, :props, :value, :counter, :lock]
8
8
 
9
9
  ## Familia has a uri
10
10
  Familia.uri