familia 2.0.0.pre17 → 2.0.0.pre19
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/CHANGELOG.rst +118 -6
- data/CLAUDE.md +43 -11
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +52 -0
- data/bin/irb +1 -1
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/core-field-system.md +48 -26
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +2 -1
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
- data/lib/familia/data_type/serialization.rb +9 -5
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +130 -11
- data/lib/familia/horreum/definition.rb +8 -38
- data/lib/familia/horreum/management.rb +38 -27
- data/lib/familia/horreum/persistence.rb +191 -67
- data/lib/familia/horreum/serialization.rb +94 -73
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +41 -18
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +268 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +5 -5
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +2 -2
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +34 -5
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +5 -5
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +23 -23
- data/try/integration/cross_component_try.rb +1 -1
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +2 -2
- data/try/{core → integration}/persistence_operations_try.rb +163 -11
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +2 -2
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/{horreum → unit/horreum}/base_try.rb +4 -4
- data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +147 -126
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -0,0 +1,247 @@
|
|
1
|
+
# DataType Transaction Support Tryouts
|
2
|
+
#
|
3
|
+
# Tests transaction support for DataType objects, covering both parent-owned
|
4
|
+
# DataTypes (delegating to parent) and standalone DataTypes (managing their
|
5
|
+
# own connections). Validates atomic operations, connection context handling,
|
6
|
+
# and integration with the transaction mode system.
|
7
|
+
|
8
|
+
require_relative '../../support/helpers/test_helpers'
|
9
|
+
|
10
|
+
# Setup - Create test model with various DataType fields
|
11
|
+
class TransactionTestUser < Familia::Horreum
|
12
|
+
logical_database 2
|
13
|
+
identifier_field :userid
|
14
|
+
field :userid
|
15
|
+
field :name
|
16
|
+
field :email
|
17
|
+
|
18
|
+
# Instance-level DataTypes
|
19
|
+
sorted_set :scores
|
20
|
+
hashkey :profile
|
21
|
+
set :tags
|
22
|
+
list :activity
|
23
|
+
counter :visits
|
24
|
+
string :bio
|
25
|
+
end
|
26
|
+
|
27
|
+
@user = TransactionTestUser.new(userid: 'txn_user_001')
|
28
|
+
@user.name = 'Transaction Tester'
|
29
|
+
@user.save
|
30
|
+
|
31
|
+
## Parent-owned SortedSet can execute transaction
|
32
|
+
result = @user.scores.transaction do |conn|
|
33
|
+
conn.zadd(@user.scores.dbkey, 100, 'level1')
|
34
|
+
conn.zadd(@user.scores.dbkey, 200, 'level2')
|
35
|
+
conn.zadd(@user.scores.dbkey, 300, 'level3')
|
36
|
+
end
|
37
|
+
[result.is_a?(MultiResult), @user.scores.members.sort]
|
38
|
+
#=> [true, ["level1", "level2", "level3"]]
|
39
|
+
|
40
|
+
## Parent-owned HashKey can execute transaction
|
41
|
+
result = @user.profile.transaction do |conn|
|
42
|
+
conn.hset(@user.profile.dbkey, 'city', 'San Francisco')
|
43
|
+
conn.hset(@user.profile.dbkey, 'country', 'USA')
|
44
|
+
conn.hget(@user.profile.dbkey, 'city')
|
45
|
+
end
|
46
|
+
[result.is_a?(MultiResult), result.results.last, @user.profile['country']]
|
47
|
+
#=> [true, "San Francisco", "USA"]
|
48
|
+
|
49
|
+
## Parent-owned UnsortedSet can execute transaction
|
50
|
+
result = @user.tags.transaction do |conn|
|
51
|
+
conn.sadd(@user.tags.dbkey, 'ruby')
|
52
|
+
conn.sadd(@user.tags.dbkey, 'redis')
|
53
|
+
conn.scard(@user.tags.dbkey)
|
54
|
+
end
|
55
|
+
[result.is_a?(MultiResult), result.results.last, @user.tags.members.sort]
|
56
|
+
#=> [true, 2, ["redis", "ruby"]]
|
57
|
+
|
58
|
+
## Parent-owned List can execute transaction
|
59
|
+
result = @user.activity.transaction do |conn|
|
60
|
+
conn.rpush(@user.activity.dbkey, 'login')
|
61
|
+
conn.rpush(@user.activity.dbkey, 'view_profile')
|
62
|
+
conn.rpush(@user.activity.dbkey, 'logout')
|
63
|
+
conn.llen(@user.activity.dbkey)
|
64
|
+
end
|
65
|
+
[result.is_a?(MultiResult), result.results.last, @user.activity.members]
|
66
|
+
#=> [true, 3, ["login", "view_profile", "logout"]]
|
67
|
+
|
68
|
+
## Parent-owned Counter can execute transaction
|
69
|
+
result = @user.visits.transaction do |conn|
|
70
|
+
conn.set(@user.visits.dbkey, 0)
|
71
|
+
conn.incr(@user.visits.dbkey)
|
72
|
+
conn.incr(@user.visits.dbkey)
|
73
|
+
conn.get(@user.visits.dbkey)
|
74
|
+
end
|
75
|
+
[result.is_a?(MultiResult), result.results.last.to_i, @user.visits.value]
|
76
|
+
#=> [true, 2, 2]
|
77
|
+
|
78
|
+
## Parent-owned StringKey can execute transaction
|
79
|
+
result = @user.bio.transaction do |conn|
|
80
|
+
conn.set(@user.bio.dbkey, 'Ruby developer')
|
81
|
+
conn.append(@user.bio.dbkey, ' and Redis enthusiast')
|
82
|
+
conn.get(@user.bio.dbkey)
|
83
|
+
end
|
84
|
+
[result.is_a?(MultiResult), result.results.last, @user.bio.value]
|
85
|
+
#=> [true, "Ruby developer and Redis enthusiast", "Ruby developer and Redis enthusiast"]
|
86
|
+
|
87
|
+
## Standalone SortedSet can execute transaction
|
88
|
+
leaderboard = Familia::SortedSet.new('game:leaderboard')
|
89
|
+
leaderboard.delete!
|
90
|
+
result = leaderboard.transaction do |conn|
|
91
|
+
conn.zadd(leaderboard.dbkey, 500, 'player1')
|
92
|
+
conn.zadd(leaderboard.dbkey, 600, 'player2')
|
93
|
+
conn.zadd(leaderboard.dbkey, 450, 'player3')
|
94
|
+
conn.zcard(leaderboard.dbkey)
|
95
|
+
end
|
96
|
+
[result.is_a?(MultiResult), result.results.last, leaderboard.members.size]
|
97
|
+
#=> [true, 3, 3]
|
98
|
+
|
99
|
+
## Standalone HashKey can execute transaction
|
100
|
+
cache = Familia::HashKey.new('app:cache')
|
101
|
+
cache.delete!
|
102
|
+
result = cache.transaction do |conn|
|
103
|
+
conn.hset(cache.dbkey, 'key1', 'value1')
|
104
|
+
conn.hset(cache.dbkey, 'key2', 'value2')
|
105
|
+
conn.hkeys(cache.dbkey)
|
106
|
+
end
|
107
|
+
[result.is_a?(MultiResult), result.results.last.sort, cache.keys.sort]
|
108
|
+
#=> [true, ["key1", "key2"], ["key1", "key2"]]
|
109
|
+
|
110
|
+
## Standalone UnsortedSet can execute transaction
|
111
|
+
global_tags = Familia::UnsortedSet.new('app:tags')
|
112
|
+
global_tags.delete!
|
113
|
+
result = global_tags.transaction do |conn|
|
114
|
+
conn.sadd(global_tags.dbkey, 'tag1')
|
115
|
+
conn.sadd(global_tags.dbkey, 'tag2')
|
116
|
+
conn.smembers(global_tags.dbkey)
|
117
|
+
end
|
118
|
+
[result.is_a?(MultiResult), result.results.last.sort, global_tags.members.sort]
|
119
|
+
#=> [true, ["tag1", "tag2"], ["tag1", "tag2"]]
|
120
|
+
|
121
|
+
## Standalone StringKey can execute transaction
|
122
|
+
session_data = Familia::StringKey.new('session:abc123')
|
123
|
+
session_data.delete!
|
124
|
+
result = session_data.transaction do |conn|
|
125
|
+
conn.set(session_data.dbkey, '{"user_id": 123}')
|
126
|
+
conn.expire(session_data.dbkey, 3600)
|
127
|
+
conn.get(session_data.dbkey)
|
128
|
+
end
|
129
|
+
[result.is_a?(MultiResult), result.results.last, session_data.value]
|
130
|
+
#=> [true, "{\"user_id\": 123}", "{\"user_id\": 123}"]
|
131
|
+
|
132
|
+
## Transaction with logical_database option works
|
133
|
+
custom_cache = Familia::HashKey.new('custom:cache', logical_database: 3)
|
134
|
+
custom_cache.delete!
|
135
|
+
result = custom_cache.transaction do |conn|
|
136
|
+
conn.hset(custom_cache.dbkey, 'setting', 'enabled')
|
137
|
+
conn.hget(custom_cache.dbkey, 'setting')
|
138
|
+
end
|
139
|
+
[result.is_a?(MultiResult), result.results.last]
|
140
|
+
#=> [true, "enabled"]
|
141
|
+
|
142
|
+
## Transaction provides correct connection object type
|
143
|
+
conn_class = nil
|
144
|
+
@user.scores.transaction do |conn|
|
145
|
+
conn_class = conn.class.name
|
146
|
+
end
|
147
|
+
conn_class
|
148
|
+
#=> "Redis::MultiConnection"
|
149
|
+
|
150
|
+
## Transaction with direct_access works correctly
|
151
|
+
result = @user.profile.transaction do |trans_conn|
|
152
|
+
trans_conn.hset(@user.profile.dbkey, 'status', 'active')
|
153
|
+
|
154
|
+
# direct_access should use the same transaction connection
|
155
|
+
@user.profile.direct_access do |conn, key|
|
156
|
+
conn.object_id == trans_conn.object_id &&
|
157
|
+
conn.hset(key, 'verified', 'true')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
[@user.profile['status'], @user.profile['verified']]
|
161
|
+
#=> ["active", "true"]
|
162
|
+
|
163
|
+
## Transaction atomicity - all commands succeed or none
|
164
|
+
test_zset = Familia::SortedSet.new('atomic:test')
|
165
|
+
test_zset.delete!
|
166
|
+
test_zset.add('initial', 1)
|
167
|
+
|
168
|
+
begin
|
169
|
+
test_zset.transaction do |conn|
|
170
|
+
conn.zadd(test_zset.dbkey, 100, 'member1')
|
171
|
+
conn.zadd(test_zset.dbkey, 200, 'member2')
|
172
|
+
raise 'Intentional error to test rollback'
|
173
|
+
end
|
174
|
+
rescue => e
|
175
|
+
# Transaction should have rolled back
|
176
|
+
test_zset.members
|
177
|
+
end
|
178
|
+
#=> ["initial"]
|
179
|
+
|
180
|
+
## Nested transactions with parent-owned DataTypes work
|
181
|
+
outer_result = @user.scores.transaction do |outer_conn|
|
182
|
+
outer_conn.zadd(@user.scores.dbkey, 999, 'outer_member')
|
183
|
+
|
184
|
+
inner_result = @user.tags.transaction do |inner_conn|
|
185
|
+
inner_conn.sadd(@user.tags.dbkey, 'nested_tag')
|
186
|
+
end
|
187
|
+
|
188
|
+
inner_result.is_a?(MultiResult)
|
189
|
+
end
|
190
|
+
[outer_result.is_a?(MultiResult), @user.tags.member?('nested_tag')]
|
191
|
+
#=> [true, true]
|
192
|
+
|
193
|
+
## Transaction respects transaction modes (permissive)
|
194
|
+
begin
|
195
|
+
original_mode = Familia.transaction_mode
|
196
|
+
Familia.configure { |config| config.transaction_mode = :permissive }
|
197
|
+
|
198
|
+
# Force a cached connection to trigger fallback
|
199
|
+
@user.class.instance_variable_set(:@dbclient, Familia.create_dbclient)
|
200
|
+
|
201
|
+
result = @user.scores.transaction do |conn|
|
202
|
+
# Should be IndividualCommandProxy in fallback mode
|
203
|
+
conn.class == Familia::Connection::IndividualCommandProxy &&
|
204
|
+
conn.zadd(@user.scores.dbkey, 888, 'fallback_test')
|
205
|
+
end
|
206
|
+
|
207
|
+
result.is_a?(MultiResult)
|
208
|
+
ensure
|
209
|
+
@user.class.remove_instance_variable(:@dbclient)
|
210
|
+
Familia.configure { |config| config.transaction_mode = original_mode }
|
211
|
+
end
|
212
|
+
#=> true
|
213
|
+
|
214
|
+
## Transaction with empty block returns empty MultiResult
|
215
|
+
result = @user.scores.transaction { |conn| }
|
216
|
+
[result.is_a?(MultiResult), result.results.empty?]
|
217
|
+
#=> [true, true]
|
218
|
+
|
219
|
+
## Transaction connection uses parent's logical_database
|
220
|
+
# TransactionTestUser has logical_database 2
|
221
|
+
# Parent-owned DataType delegates to parent, verify via class setting
|
222
|
+
@user.scores.delete!
|
223
|
+
@user.scores.transaction do |conn|
|
224
|
+
conn.zadd(@user.scores.dbkey, 1, 'test_member')
|
225
|
+
end
|
226
|
+
TransactionTestUser.logical_database
|
227
|
+
#=> 2
|
228
|
+
|
229
|
+
## Multiple DataType types in single transaction
|
230
|
+
result = @user.scores.transaction do |conn|
|
231
|
+
# Can operate on different DataTypes using same connection
|
232
|
+
conn.zadd(@user.scores.dbkey, 777, 'multi_test')
|
233
|
+
conn.hset(@user.profile.dbkey, 'multi', 'yes')
|
234
|
+
conn.sadd(@user.tags.dbkey, 'multi_tag')
|
235
|
+
conn.rpush(@user.activity.dbkey, 'multi_action')
|
236
|
+
end
|
237
|
+
[
|
238
|
+
result.is_a?(MultiResult),
|
239
|
+
@user.scores.member?('multi_test'),
|
240
|
+
@user.profile['multi'],
|
241
|
+
@user.tags.member?('multi_tag'),
|
242
|
+
@user.activity.members.include?('multi_action')
|
243
|
+
]
|
244
|
+
#=> [true, true, "yes", true, true]
|
245
|
+
|
246
|
+
# Cleanup
|
247
|
+
@user.destroy!
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# Database consistency verification and edge case testing
|
4
4
|
# Complements persistence_operations_try.rb with deeper consistency checks
|
5
5
|
|
6
|
-
require_relative '../helpers/test_helpers'
|
6
|
+
require_relative '../support/helpers/test_helpers'
|
7
7
|
|
8
8
|
# Test class with different field types for consistency verification
|
9
9
|
class ConsistencyTestModel < Familia::Horreum
|
@@ -55,15 +55,18 @@ key_parts = dbkey.split(':')
|
|
55
55
|
# Refresh and verify data integrity
|
56
56
|
@serial_test.refresh!
|
57
57
|
[@serial_test.name, @serial_test.active, @serial_test.metadata]
|
58
|
-
#=> [
|
58
|
+
#=> ["Serialization Test", true, {"key"=>"value", "array"=>[1, 2, 3]}]
|
59
59
|
|
60
60
|
## Hash field count matches object field count
|
61
|
+
@serial_test = ConsistencyTestModel.new(id: next_test_id)
|
62
|
+
@serial_test.save
|
61
63
|
expected_fields = @serial_test.class.persistent_fields.length
|
62
64
|
redis_field_count = Familia.dbclient.hlen(@serial_test.dbkey)
|
63
65
|
actual_object_fields = @serial_test.to_h.keys.length
|
64
|
-
#
|
65
|
-
|
66
|
-
|
66
|
+
# The JSON Serializer stores all fields (including nil as "null")
|
67
|
+
# Expected fields (5) >= redis count (5) >= to_h count (5, even though email is nil)
|
68
|
+
[expected_fields, redis_field_count, actual_object_fields]
|
69
|
+
#=> [5, 5, 5]
|
67
70
|
|
68
71
|
## Memory vs persistence state consistency after save
|
69
72
|
@consistency_obj = ConsistencyTestModel.new(id: next_test_id, name: 'Memory Test', email: 'test@example.com')
|
@@ -73,9 +76,9 @@ actual_object_fields = @serial_test.to_h.keys.length
|
|
73
76
|
memory_name = @consistency_obj.name
|
74
77
|
memory_email = @consistency_obj.email
|
75
78
|
|
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
|
+
# Get persistence state (deserialize from JSON storage)
|
80
|
+
redis_name = @consistency_obj.deserialize_value(Familia.dbclient.hget(@consistency_obj.dbkey, 'name'))
|
81
|
+
redis_email = @consistency_obj.deserialize_value(Familia.dbclient.hget(@consistency_obj.dbkey, 'email'))
|
79
82
|
|
80
83
|
[memory_name == redis_name, memory_email == redis_email]
|
81
84
|
#=> [true, true]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/models/customer_safedump_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
# Setup
|
6
6
|
@now = Familia.now.to_i
|
@@ -39,7 +39,11 @@ require_relative '../helpers/test_helpers'
|
|
39
39
|
|
40
40
|
## Safe dump includes correct updated timestamp
|
41
41
|
@safe_dump[:updated]
|
42
|
-
|
42
|
+
#=:> Float
|
43
|
+
|
44
|
+
## Safe dump includes correct updated timestamp
|
45
|
+
@safe_dump[:updated].to_i
|
46
|
+
#=> @now.to_i
|
43
47
|
|
44
48
|
## Safe dump includes correct secrets_created count
|
45
49
|
@customer.secrets_created.increment
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/models/familia_object_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -71,7 +71,7 @@ Customer.all_customers.size
|
|
71
71
|
|
72
72
|
## Familia class clear
|
73
73
|
Customer.all_customers.delete!
|
74
|
-
#=>
|
74
|
+
#=> 1
|
75
75
|
|
76
76
|
## Familia class replace 1 of 4
|
77
77
|
Customer.message.value = 'msg1'
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# Comprehensive test coverage for core persistence methods: exists?, save, save_if_not_exists, create
|
4
4
|
# This test addresses gaps that allowed the exists? bug to go undetected
|
5
5
|
|
6
|
-
require_relative '../helpers/test_helpers'
|
6
|
+
require_relative '../support/helpers/test_helpers'
|
7
7
|
|
8
8
|
# Use a simple test class to isolate persistence behavior
|
9
9
|
class PersistenceTestModel < Familia::Horreum
|
@@ -13,6 +13,27 @@ class PersistenceTestModel < Familia::Horreum
|
|
13
13
|
field :value
|
14
14
|
end
|
15
15
|
|
16
|
+
# Create model with expiration feature for save_fields testing
|
17
|
+
class ExpirationPersistenceTest < Familia::Horreum
|
18
|
+
feature :expiration
|
19
|
+
identifier_field :id
|
20
|
+
field :id
|
21
|
+
field :name
|
22
|
+
field :email
|
23
|
+
field :status
|
24
|
+
field :metadata
|
25
|
+
|
26
|
+
default_expiration 3600 # 1 hour
|
27
|
+
end
|
28
|
+
|
29
|
+
# Simple model without expiration feature
|
30
|
+
class SimpleModel < Familia::Horreum
|
31
|
+
identifier_field :id
|
32
|
+
field :id
|
33
|
+
field :name
|
34
|
+
field :value
|
35
|
+
end
|
36
|
+
|
16
37
|
# Clean up any existing test data
|
17
38
|
cleanup_keys = []
|
18
39
|
begin
|
@@ -111,9 +132,9 @@ result = @sine_new.save_if_not_exists
|
|
111
132
|
[result, @sine_new.exists?]
|
112
133
|
#=> [true, true]
|
113
134
|
|
114
|
-
## save_if_not_exists raises error for existing object
|
135
|
+
## save_if_not_exists! raises error for existing object
|
115
136
|
@sine_duplicate = PersistenceTestModel.new(id: @sine_new.identifier, name: 'Duplicate')
|
116
|
-
@sine_duplicate.save_if_not_exists
|
137
|
+
@sine_duplicate.save_if_not_exists!
|
117
138
|
#=!> Familia::RecordExistsError
|
118
139
|
|
119
140
|
## save_if_not_exists with update_expiration: false
|
@@ -128,14 +149,10 @@ original_name = 'Original Name'
|
|
128
149
|
@sine_fail_test.save_if_not_exists
|
129
150
|
# Now create duplicate and verify state doesn't change on failure
|
130
151
|
@sine_fail_duplicate = PersistenceTestModel.new(id: @sine_fail_test.identifier, name: 'Changed Name')
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
# State should be unchanged
|
136
|
-
@sine_fail_duplicate.name == 'Changed Name'
|
137
|
-
end
|
138
|
-
#=> true
|
152
|
+
result = @sine_fail_duplicate.save_if_not_exists
|
153
|
+
# save_if_not_exists returns false on failure, state should be unchanged
|
154
|
+
[result == false, @sine_fail_duplicate.name == 'Changed Name']
|
155
|
+
#=> [true, true]
|
139
156
|
|
140
157
|
# =============================================
|
141
158
|
# 4. create Method Coverage (MISSING from current tests)
|
@@ -291,7 +308,142 @@ actual_key = @key_obj.dbkey
|
|
291
308
|
# Cleanup
|
292
309
|
# =============================================
|
293
310
|
|
311
|
+
# =============================================
|
312
|
+
# 8. save_fields Method Coverage
|
313
|
+
# =============================================
|
314
|
+
|
315
|
+
## save_fields basic functionality with specified fields
|
316
|
+
@save_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Original Name', email: 'test@example.com', status: 'active')
|
317
|
+
@save_fields_obj.save
|
318
|
+
# Modify fields locally
|
319
|
+
@save_fields_obj.name = 'Updated Name'
|
320
|
+
@save_fields_obj.status = 'inactive'
|
321
|
+
@save_fields_obj.metadata = { updated: true }
|
322
|
+
# Save only specific fields
|
323
|
+
result = @save_fields_obj.save_fields(:name, :metadata)
|
324
|
+
[result.class == ExpirationPersistenceTest, @save_fields_obj.exists?]
|
325
|
+
#=> [true, true]
|
326
|
+
|
327
|
+
## Verify only specified fields were saved
|
328
|
+
@save_fields_obj.refresh!
|
329
|
+
[@save_fields_obj.name, @save_fields_obj.status, @save_fields_obj.metadata]
|
330
|
+
#=> ['Updated Name', 'active', { 'updated' => true }]
|
331
|
+
|
332
|
+
## save_fields with update_expiration: true (default)
|
333
|
+
@exp_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Expiration Test')
|
334
|
+
@exp_obj.save
|
335
|
+
original_ttl = @exp_obj.ttl
|
336
|
+
# Wait a moment to ensure TTL decreases
|
337
|
+
sleep 0.1
|
338
|
+
@exp_obj.name = 'Updated with TTL'
|
339
|
+
@exp_obj.save_fields(:name) # Should update expiration by default
|
340
|
+
new_ttl = @exp_obj.ttl
|
341
|
+
# TTL should be refreshed (closer to default_expiration)
|
342
|
+
# Allow for small timing variations
|
343
|
+
new_ttl >= (ExpirationPersistenceTest.default_expiration - 10)
|
344
|
+
#=> true
|
345
|
+
|
346
|
+
## save_fields with update_expiration: false
|
347
|
+
@no_exp_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'No Exp Update')
|
348
|
+
@no_exp_obj.save
|
349
|
+
# Wait briefly and get TTL
|
350
|
+
sleep 0.1
|
351
|
+
original_ttl = @no_exp_obj.ttl
|
352
|
+
@no_exp_obj.name = 'Updated without TTL'
|
353
|
+
@no_exp_obj.save_fields(:name, update_expiration: false)
|
354
|
+
new_ttl = @no_exp_obj.ttl
|
355
|
+
# TTL should be approximately the same (slightly less due to time passing)
|
356
|
+
(new_ttl - original_ttl).abs < 2
|
357
|
+
#=> true
|
358
|
+
|
359
|
+
## save_fields with multiple fields
|
360
|
+
@multi_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Multi', email: 'multi@test.com')
|
361
|
+
@multi_fields_obj.save
|
362
|
+
@multi_fields_obj.name = 'Multi Updated'
|
363
|
+
@multi_fields_obj.email = 'updated@test.com'
|
364
|
+
@multi_fields_obj.status = 'new_status'
|
365
|
+
result = @multi_fields_obj.save_fields(:name, :email, :status)
|
366
|
+
@multi_fields_obj.refresh!
|
367
|
+
[@multi_fields_obj.name, @multi_fields_obj.email, @multi_fields_obj.status]
|
368
|
+
#=> ['Multi Updated', 'updated@test.com', 'new_status']
|
369
|
+
|
370
|
+
## save_fields with string field names
|
371
|
+
@string_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'String Fields')
|
372
|
+
@string_fields_obj.save
|
373
|
+
@string_fields_obj.name = 'Updated via String'
|
374
|
+
result = @string_fields_obj.save_fields('name') # String instead of symbol
|
375
|
+
@string_fields_obj.refresh!
|
376
|
+
@string_fields_obj.name
|
377
|
+
#=> 'Updated via String'
|
378
|
+
|
379
|
+
## save_fields error handling - empty fields
|
380
|
+
@empty_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Empty Test')
|
381
|
+
@empty_fields_obj.save
|
382
|
+
@empty_fields_obj.save_fields()
|
383
|
+
#=!> ArgumentError
|
384
|
+
|
385
|
+
## save_fields error handling - unknown field
|
386
|
+
@unknown_field_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Unknown Field')
|
387
|
+
@unknown_field_obj.save
|
388
|
+
@unknown_field_obj.save_fields(:nonexistent_field)
|
389
|
+
#=!> ArgumentError
|
390
|
+
|
391
|
+
## save_fields with nil values
|
392
|
+
@nil_values_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Nil Values', status: 'initial')
|
393
|
+
@nil_values_obj.save
|
394
|
+
@nil_values_obj.status = nil
|
395
|
+
@nil_values_obj.save_fields(:status)
|
396
|
+
@nil_values_obj.refresh!
|
397
|
+
@nil_values_obj.status
|
398
|
+
#=> nil
|
399
|
+
|
400
|
+
## save_fields with complex data types (Hash, Array)
|
401
|
+
@complex_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Complex')
|
402
|
+
@complex_obj.save
|
403
|
+
@complex_obj.metadata = {
|
404
|
+
tags: ['ruby', 'redis'],
|
405
|
+
config: { timeout: 30, retries: 3 },
|
406
|
+
enabled: true
|
407
|
+
}
|
408
|
+
@complex_obj.save_fields(:metadata)
|
409
|
+
@complex_obj.refresh!
|
410
|
+
expected_metadata = {
|
411
|
+
'tags' => ['ruby', 'redis'],
|
412
|
+
'config' => { 'timeout' => 30, 'retries' => 3 },
|
413
|
+
'enabled' => true
|
414
|
+
}
|
415
|
+
@complex_obj.metadata == expected_metadata
|
416
|
+
#=> true
|
417
|
+
|
418
|
+
## save_fields transactional behavior
|
419
|
+
@transaction_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Transaction Test')
|
420
|
+
@transaction_obj.save
|
421
|
+
@transaction_obj.name = 'Updated in Transaction'
|
422
|
+
@transaction_obj.email = 'transaction@test.com'
|
423
|
+
# All fields should be saved atomically
|
424
|
+
@transaction_obj.save_fields(:name, :email)
|
425
|
+
@transaction_obj.refresh!
|
426
|
+
[@transaction_obj.name, @transaction_obj.email]
|
427
|
+
#=> ['Updated in Transaction', 'transaction@test.com']
|
428
|
+
|
429
|
+
## save_fields performance with model without expiration feature
|
430
|
+
|
431
|
+
@simple_obj = SimpleModel.new(id: next_test_id, name: 'Simple', value: 'test')
|
432
|
+
@simple_obj.save
|
433
|
+
@simple_obj.name = 'Simple Updated'
|
434
|
+
# Should work without expiration feature (update_expiration param ignored)
|
435
|
+
result = @simple_obj.save_fields(:name, update_expiration: true)
|
436
|
+
@simple_obj.refresh!
|
437
|
+
@simple_obj.name
|
438
|
+
#=> 'Simple Updated'
|
439
|
+
|
440
|
+
# =============================================
|
441
|
+
# Cleanup
|
442
|
+
# =============================================
|
443
|
+
|
294
444
|
# Clean up test data
|
295
445
|
test_keys = Familia.dbclient.keys('persistencetestmodel:*')
|
296
446
|
test_keys.concat(Familia.dbclient.keys('encryptedpersistencetest:*')) if defined?(EncryptedPersistenceTest)
|
447
|
+
test_keys.concat(Familia.dbclient.keys('expirationpersistencetest:*'))
|
448
|
+
test_keys.concat(Familia.dbclient.keys('simplemodel:*'))
|
297
449
|
Familia.dbclient.del(*test_keys) if test_keys.any?
|