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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +11 -8
- data/Gemfile +5 -1
- data/Gemfile.lock +19 -3
- data/README.md +36 -157
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +347 -0
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +600 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +106 -0
- data/docs/wiki/Implementation-Guide.md +276 -0
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +183 -0
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +12 -14
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +119 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +273 -0
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
- data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
- data/lib/familia/horreum/subclass/definition.rb +469 -0
- data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +30 -22
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +3 -2
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -5
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +1 -2
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/data_types/counter_try.rb +93 -0
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/data_types/lock_try.rb +133 -0
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +125 -0
- data/try/features/encrypted_fields_integration_try.rb +216 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
- data/try/features/encrypted_fields_security_try.rb +377 -0
- data/try/features/encryption_fields/aad_protection_try.rb +138 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +141 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
- data/try/features/encryption_fields/fresh_key_try.rb +168 -0
- data/try/features/encryption_fields/key_rotation_try.rb +123 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +67 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +1 -2
- data/try/horreum/relations_try.rb +1 -2
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +41 -7
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +1 -2
- data/try/models/customer_try.rb +1 -2
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +131 -23
- data/lib/familia/horreum/serialization.rb +0 -445
data/lib/familia/version.rb
CHANGED
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/
|
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
|
data/try/core/connection_try.rb
CHANGED
@@ -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?
|