familia 1.2.1 → 2.0.0.pre2

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +68 -0
  3. data/.github/workflows/docs.yml +64 -0
  4. data/.gitignore +4 -0
  5. data/.pre-commit-config.yaml +3 -1
  6. data/.rubocop.yml +16 -9
  7. data/.rubocop_todo.yml +177 -31
  8. data/.yardopts +9 -0
  9. data/CLAUDE.md +141 -0
  10. data/Gemfile +16 -2
  11. data/Gemfile.lock +97 -36
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +192 -0
  15. data/familia.gemspec +10 -6
  16. data/lib/familia/base.rb +19 -9
  17. data/lib/familia/connection.rb +232 -65
  18. data/lib/familia/core_ext.rb +1 -1
  19. data/lib/familia/datatype/commands.rb +59 -0
  20. data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
  21. data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
  22. data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
  23. data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
  24. data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
  25. data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
  26. data/lib/familia/datatype.rb +243 -0
  27. data/lib/familia/errors.rb +5 -2
  28. data/lib/familia/features/expiration.rb +33 -34
  29. data/lib/familia/features/quantization.rb +9 -3
  30. data/lib/familia/features/safe_dump.rb +2 -3
  31. data/lib/familia/features.rb +2 -2
  32. data/lib/familia/horreum/class_methods.rb +97 -110
  33. data/lib/familia/horreum/commands.rb +46 -51
  34. data/lib/familia/horreum/connection.rb +82 -0
  35. data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
  36. data/lib/familia/horreum/serialization.rb +61 -198
  37. data/lib/familia/horreum/settings.rb +6 -17
  38. data/lib/familia/horreum/utils.rb +11 -10
  39. data/lib/familia/horreum.rb +69 -60
  40. data/lib/familia/logging.rb +12 -12
  41. data/lib/familia/multi_result.rb +72 -0
  42. data/lib/familia/refinements.rb +7 -44
  43. data/lib/familia/settings.rb +11 -11
  44. data/lib/familia/utils.rb +123 -90
  45. data/lib/familia/version.rb +4 -21
  46. data/lib/familia.rb +18 -13
  47. data/lib/middleware/database_middleware.rb +150 -0
  48. data/try/configuration/scenarios_try.rb +65 -0
  49. data/try/core/connection_try.rb +58 -0
  50. data/try/core/errors_try.rb +93 -0
  51. data/try/core/extensions_try.rb +26 -0
  52. data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
  53. data/try/{00_familia_try.rb → core/familia_try.rb} +7 -5
  54. data/try/core/middleware_try.rb +68 -0
  55. data/try/core/refinements_try.rb +39 -0
  56. data/try/core/settings_try.rb +76 -0
  57. data/try/core/tools_try.rb +54 -0
  58. data/try/core/utils_try.rb +189 -0
  59. data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
  60. data/try/datatypes/datatype_base_try.rb +69 -0
  61. data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
  62. data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
  63. data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
  64. data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
  65. data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
  66. data/try/edge_cases/empty_identifiers_try.rb +48 -0
  67. data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -7
  68. data/try/edge_cases/json_serialization_try.rb +85 -0
  69. data/try/edge_cases/race_conditions_try.rb +60 -0
  70. data/try/edge_cases/reserved_keywords_try.rb +59 -0
  71. data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +60 -59
  72. data/try/edge_cases/ttl_side_effects_try.rb +51 -0
  73. data/try/features/expiration_try.rb +86 -0
  74. data/try/features/quantization_try.rb +90 -0
  75. data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
  76. data/try/features/safe_dump_try.rb +137 -0
  77. data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
  78. data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
  79. data/try/horreum/class_methods_try.rb +41 -0
  80. data/try/horreum/commands_try.rb +49 -0
  81. data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
  82. data/try/horreum/relations_try.rb +146 -0
  83. data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
  84. data/try/horreum/settings_try.rb +43 -0
  85. data/try/integration/cross_component_try.rb +46 -0
  86. data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
  87. data/try/{40_customer_try.rb → models/customer_try.rb} +21 -18
  88. data/try/models/datatype_base_try.rb +100 -0
  89. data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
  90. data/try/performance/benchmarks_try.rb +55 -0
  91. data/try/pooling/README.md +20 -0
  92. data/try/pooling/configurable_stress_test_try.rb +435 -0
  93. data/try/pooling/connection_pool_test_try.rb +273 -0
  94. data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  95. data/try/pooling/lib/connection_pool_metrics.rb +372 -0
  96. data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
  97. data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
  98. data/try/pooling/lib/visualize_stress_results.rb +434 -0
  99. data/try/pooling/pool_siege_try.rb +509 -0
  100. data/try/pooling/run_stress_tests_try.rb +482 -0
  101. data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
  102. data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
  103. data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
  104. data/try/prototypes/atomic_saves_v4.rb +105 -0
  105. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
  106. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  107. metadata +143 -46
  108. data/.github/workflows/ruby.yml +0 -71
  109. data/VERSION.yml +0 -4
  110. data/lib/familia/redistype/commands.rb +0 -59
  111. data/lib/familia/redistype.rb +0 -228
  112. data/lib/familia/tools.rb +0 -68
  113. data/lib/redis_middleware.rb +0 -109
  114. data/try/20_redis_type_try.rb +0 -70
  115. data/try/91_json_bug_try.rb +0 -86
@@ -1,7 +1,9 @@
1
- require_relative '../lib/familia'
2
- require_relative './test_helpers'
1
+ # try/datatypes/hash_try.rb
3
2
 
4
- @a = Bone.new 'atoken', 'akey'
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
+
6
+ @a = Bone.new 'atoken'
5
7
 
6
8
  ## Familia::HashKey#has_key? knows when there's no key
7
9
  @a.props.has_key? 'fieldA'
@@ -1,8 +1,10 @@
1
+ # try/datatypes/list_try.rb
1
2
 
2
- require_relative '../lib/familia'
3
- require_relative './test_helpers'
4
3
 
5
- @a = Bone.new 'atoken', 'akey'
4
+ require_relative '../../lib/familia'
5
+ require_relative '../helpers/test_helpers'
6
+
7
+ @a = Bone.new 'atoken'
6
8
 
7
9
  ## Familia::List#push
8
10
  ret = @a.owners.push :value1
@@ -1,8 +1,10 @@
1
+ # try/datatypes/set_try.rb
1
2
 
2
- require_relative '../lib/familia'
3
- require_relative './test_helpers'
4
3
 
5
- @a = Bone.new 'atoken', 'akey'
4
+ require_relative '../../lib/familia'
5
+ require_relative '../helpers/test_helpers'
6
+
7
+ @a = Bone.new 'atoken'
6
8
 
7
9
  ## Familia::Set#add
8
10
  ret = @a.tags.add :a
@@ -1,11 +1,13 @@
1
- require_relative '../lib/familia'
2
- require_relative './test_helpers'
1
+ # try/datatypes/sorted_set_try.rb
3
2
 
4
- # Familia.debug = true
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
5
 
6
- @a = Bone.new 'atoken', 'akey'
6
+
7
+ @a = Bone.new 'atoken'
7
8
 
8
9
  ## Familia::SortedSet#add
10
+ @a = Bone.new 'atoken'
9
11
  @a.metrics.add 2, :metric2
10
12
  @a.metrics.add 4, :metric4
11
13
  @a.metrics.add 0, :metric0
@@ -1,13 +1,13 @@
1
- require_relative '../lib/familia'
2
- require_relative './test_helpers'
1
+ # try/datatypes/string_try.rb
3
2
 
4
- #Familia.apiversion = 'v1'
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
5
 
6
- @a = Bone.new 'atoken2', 'akey'
6
+ @a = Bone.new(token: 'atoken2')
7
7
 
8
- ## Bone#rediskey
9
- @a.rediskey
10
- #=> 'bone:atoken2:akey:object'
8
+ ## Bone#dbkey
9
+ @a.dbkey
10
+ #=> 'bone:atoken2:object'
11
11
 
12
12
  ## Familia::String#value should give default value
13
13
  @a.value.value
@@ -27,7 +27,7 @@ require_relative './test_helpers'
27
27
 
28
28
  ## Familia::String.new
29
29
  @ret = Familia::String.new 'arbitrary:key'
30
- @ret.rediskey
30
+ @ret.dbkey
31
31
  #=> 'arbitrary:key'
32
32
 
33
33
  ## instance set
@@ -0,0 +1,48 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test empty identifier edge cases
4
+ group "Empty Identifier Edge Cases"
5
+
6
+ setup do
7
+ @user_class = Class.new(Familia::Horreum) do
8
+ identifier :email
9
+ field :name
10
+ end
11
+ end
12
+
13
+ try "empty string identifier causes stack overflow" do
14
+ user = @user_class.new(email: "", name: "Test")
15
+
16
+ begin
17
+ user.exists? # This should cause infinite loop
18
+ false
19
+ rescue SystemStackError
20
+ true # Expected stack overflow
21
+ rescue => e
22
+ false # Unexpected error
23
+ end
24
+ end
25
+
26
+ try "nil identifier causes stack overflow" do
27
+ user = @user_class.new(email: nil, name: "Test")
28
+
29
+ begin
30
+ user.exists?
31
+ false
32
+ rescue SystemStackError, Familia::NoIdentifier
33
+ true # Expected error
34
+ end
35
+ end
36
+
37
+ try "validation workaround prevents stack overflow" do
38
+ user = @user_class.new(email: "", name: "Test")
39
+
40
+ # Workaround: validate before operations
41
+ if user.identifier.to_s.empty?
42
+ raise ArgumentError, "Empty identifier"
43
+ end
44
+
45
+ false # Should not reach here
46
+ rescue ArgumentError => e
47
+ e.message.include?("Empty identifier")
48
+ end
@@ -1,20 +1,24 @@
1
- require_relative '../lib/familia'
2
- require_relative './test_helpers'
1
+ # try/edge_cases/hash_symbolization_try.rb
2
+
3
+ # NOTE: These testcases are disabled b/c there's a shared context
4
+ # bug in Tryouts 3.1 that prevents the setup instance vars from
5
+ # being available to the testcases.
6
+
7
+ require_relative '../../lib/familia'
8
+ require_relative '../helpers/test_helpers'
3
9
 
4
10
  Familia.debug = false
5
11
 
6
12
  # Test the updated deserialize_value method
7
13
  class SymbolizeTest < Familia::Horreum
8
- identifier :id
14
+ identifier_field :id
9
15
  field :id
10
16
  field :config
11
17
  end
12
18
 
19
+ @test_hash = { "name" => "John", "age" => 30, "nested" => { "theme" => "dark" } }
13
20
  @test_obj = SymbolizeTest.new
14
21
  @test_obj.id = "symbolize_test_1"
15
-
16
- ## Test with a hash containing string keys
17
- @test_hash = { "name" => "John", "age" => 30, "nested" => { "theme" => "dark" } }
18
22
  @test_obj.config = @test_hash
19
23
  @test_obj.save
20
24
 
@@ -85,6 +89,7 @@ end
85
89
  @test_obj.deserialize_value('just a string')
86
90
  #=> "just a string"
87
91
 
92
+ ## A stringified number is still a stringified number
88
93
  @test_obj.deserialize_value('42')
89
94
  #=> "42"
90
95
 
@@ -92,5 +97,5 @@ end
92
97
  @test_obj.deserialize_value('invalid json')
93
98
  #=> "invalid json"
94
99
 
95
- ## Clean up
100
+ # Clean up
96
101
  @test_obj.destroy!
@@ -0,0 +1,85 @@
1
+ # try/edge_cases/json_serialization_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
+
6
+ Familia.debug = false
7
+
8
+ # Define a simple model with fields that should handle JSON data
9
+ class JsonTest < Familia::Horreum
10
+ identifier_field :id
11
+ field :id
12
+ field :config # This should be able to store Hash objects
13
+ field :tags # This should be able to store Array objects
14
+ field :simple # This should store simple strings as-is
15
+ end
16
+
17
+
18
+ ## Test 1: Store a Hash - should serialize to JSON automatically
19
+ test_obj = JsonTest.new
20
+ test_obj.config = { theme: "dark", notifications: true, settings: { volume: 80 } }
21
+ test_obj.config
22
+ #=:> Hash
23
+
24
+ ## Test 2: Store an Array - should serialize to JSON automatically
25
+ test_obj = JsonTest.new
26
+ test_obj.tags = ["ruby", "valkey", "json", "familia"]
27
+ test_obj.tags
28
+ #=:> Array
29
+
30
+ ## Test 3: Store a simple string - should remain as string
31
+ test_obj = JsonTest.new
32
+ test_obj.simple = "just a string"
33
+ test_obj.simple
34
+ #=:> String
35
+
36
+ ## Save the object - this should call serialize_value and use to_json
37
+ test_obj = JsonTest.new 'a_unque_id'
38
+ test_obj.save
39
+ #=> true
40
+
41
+ ## Verify what's actually stored in Database (raw)
42
+ test_obj = JsonTest.new
43
+ test_obj.id = "json_test_1"
44
+ test_obj.config = { theme: "dark", notifications: true, settings: { volume: 80 } }
45
+ test_obj.simple = "just a string"
46
+ test_obj.tags = ["ruby", "valkey", "json", "familia"]
47
+ test_obj.save
48
+ test_obj.hgetall
49
+ #=> {"id"=>"json_test_1", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"valkey\",\"json\",\"familia\"]", "simple"=>"just a string"}
50
+
51
+ ## Test 4: Hash should be deserialized back to Hash
52
+ test_obj = JsonTest.new 'any_id_will_do'
53
+ puts "Config after refresh:"
54
+ puts test_obj.config
55
+ puts "Config class: "
56
+ [test_obj.config.class, test_obj.config]
57
+ ##=> [Hash, {:theme=>"dark", :notifications=>true, :settings=>{:volume=>80}}]
58
+
59
+ ## Test 5: Array should be deserialized back to Array
60
+ test_obj = JsonTest.new 'any_id_will_do'
61
+ puts "Tags after refresh:"
62
+ puts test_obj.tags.inspect
63
+ puts "Tags class: #{test_obj.tags.class}"
64
+ test_obj.tags.inspect
65
+ test_obj.tags
66
+ ##=> ["ruby", "valkey", "json", "familia"]
67
+
68
+ ## Test 6: Simple string should remain a string (this works correctly)
69
+ test_obj = JsonTest.new 'any_id_will_do'
70
+ puts "Simple after refresh:"
71
+ puts test_obj.simple.inspect
72
+ puts "Simple class: #{test_obj.simple.class}"
73
+ [test_obj.simple.class, test_obj.simple]
74
+ ##=> [String, "just a string"]
75
+
76
+ # Demonstrate the asymmetry:
77
+ test_obj = JsonTest.new 'any_id_will_do'
78
+ puts "\n=== ASYMMETRY DEMONSTRATION ==="
79
+ puts "Before save: config is #{test_obj.config.class}"
80
+ test_obj.config = { example: "data" }
81
+ puts "After assignment: config is #{test_obj.config.class}"
82
+ test_obj.save
83
+ puts "After save: config is still #{test_obj.config.class}"
84
+ test_obj.refresh!
85
+ puts "After refresh: config is now #{test_obj.config.class}!"
@@ -0,0 +1,60 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test connection race conditions
4
+ group "Race Conditions Edge Cases"
5
+
6
+ setup do
7
+ @user_class = Class.new(Familia::Horreum) do
8
+ identifier :email
9
+ field :counter
10
+ end
11
+ end
12
+
13
+ try "concurrent connection access causes race condition" do
14
+ user = @user_class.new(email: "test@example.com", counter: 0)
15
+ user.save
16
+
17
+ threads = []
18
+ results = []
19
+
20
+ # Simulate high concurrency
21
+ 10.times do
22
+ threads << Thread.new do
23
+ begin
24
+ user.incr(:counter)
25
+ results << "success"
26
+ rescue => e
27
+ results << "error: #{e.class.name}"
28
+ end
29
+ end
30
+ end
31
+
32
+ threads.each(&:join)
33
+
34
+ # May show race condition issues
35
+ errors = results.count { |r| r.start_with?("error") }
36
+ errors > 0 # Expects some race condition errors
37
+ ensure
38
+ user&.delete!
39
+ end
40
+
41
+ try "connection pool stress test" do
42
+ users = []
43
+
44
+ # Create multiple users concurrently
45
+ threads = []
46
+ 20.times do |i|
47
+ threads << Thread.new do
48
+ user = @user_class.new(email: "user#{i}@example.com")
49
+ user.save
50
+ users << user
51
+ end
52
+ end
53
+
54
+ threads.each(&:join)
55
+
56
+ # Check for connection issues
57
+ users.length > 0 # Some should succeed despite race conditions
58
+ ensure
59
+ users.each(&:delete!) rescue nil
60
+ end
@@ -0,0 +1,59 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test reserved keyword handling
4
+ group "Reserved Keywords Edge Cases"
5
+
6
+ setup do
7
+ @user_class = Class.new(Familia::Horreum) do
8
+ identifier :email
9
+ # These should fail with reserved keywords
10
+ begin
11
+ field :ttl # Reserved for expiration
12
+ field :db # Reserved for database
13
+ field :redis # Reserved for connection
14
+ rescue => e
15
+ # Expected to fail
16
+ end
17
+
18
+ # Workarounds
19
+ field :secret_ttl
20
+ field :user_db
21
+ field :redis_config
22
+ end
23
+ end
24
+
25
+ try "cannot use ttl as field name" do
26
+ begin
27
+ Class.new(Familia::Horreum) do
28
+ field :ttl
29
+ end
30
+ false # Should not reach here
31
+ rescue => e
32
+ true # Expected error
33
+ end
34
+ end
35
+
36
+ try "workaround with prefixed names works" do
37
+ user = @user_class.new(email: "test@example.com")
38
+ user.secret_ttl = 3600
39
+ user.user_db = 5
40
+ user.redis_config = {host: "localhost"}
41
+ user.save
42
+
43
+ user.secret_ttl == 3600 &&
44
+ user.user_db == 5 &&
45
+ user.redis_config.is_a?(Hash)
46
+ ensure
47
+ user&.delete!
48
+ end
49
+
50
+ try "reserved methods still work normally" do
51
+ user = @user_class.new(email: "test@example.com")
52
+ user.save
53
+
54
+ user.respond_to?(:ttl) &&
55
+ user.respond_to?(:db) &&
56
+ user.respond_to?(:redis)
57
+ ensure
58
+ user&.delete!
59
+ end
@@ -1,35 +1,63 @@
1
- # try/93_string_coercion_try.rb
1
+ # try/edge_cases/string_coercion_try.rb
2
2
 
3
- require_relative '../lib/familia'
4
- require_relative './test_helpers'
3
+ require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false
7
6
 
7
+ @customer_id = 'customer-string-coercion-test'
8
+
8
9
  # Error handling: object without proper identifier setup
9
10
  class ::BadIdentifierTest < Familia::Horreum
10
11
  # No identifier method defined - should cause issues
11
12
  end
12
13
 
14
+ # Case statement works with string matching
15
+ def classify_id(obj)
16
+ case obj.to_s
17
+ when /customer/
18
+ 'customer_type'
19
+ when /session/
20
+ 'session_type'
21
+ else
22
+ 'unknown_type'
23
+ end
24
+ end
25
+
26
+ # Polymorphic method accepting both strings and Familia objects
27
+ def process_identifier(id_or_object)
28
+ # Can handle both string IDs and Familia objects uniformly
29
+ processed_id = id_or_object.to_s
30
+ "processed:#{processed_id}"
31
+ end
32
+
33
+ def lookup_by_id(id_string)
34
+ id_string.to_s.upcase
35
+ end
36
+
37
+ ## Instantiaite a troubled model class
13
38
  @bad_obj = ::BadIdentifierTest.new
39
+ #=:> BadIdentifierTest
14
40
 
15
41
  # Test polymorphic string usage for Familia objects
16
- @customer_id = 'customer-string-coercion-test'
17
42
  @customer = Customer.new(@customer_id)
18
43
  @customer.name = 'John Doe'
19
44
  @customer.planid = 'premium'
20
45
  @customer.save
46
+ #=> true
21
47
 
22
- @session_id = 'session-string-coercion-test'
23
- @session = Session.new(@session_id)
48
+ ## Session save
49
+ @session = Session.new
24
50
  @session.custid = @customer_id
25
51
  @session.useragent = 'Test Browser'
26
52
  @session.save
53
+ #=> true
27
54
 
28
- ## Complex identifier test with array-based identifier
55
+ ## Bone with simple identifier
29
56
  @bone = Bone.new
30
57
  @bone.token = 'test_token'
31
58
  @bone.name = 'test_name'
32
-
59
+ @bone.identifier
60
+ #=> 'test_token'
33
61
 
34
62
  ## Basic to_s functionality returns identifier
35
63
  @customer.to_s
@@ -43,14 +71,20 @@ end
43
71
  @customer.to_s == @customer.identifier
44
72
  #=> true
45
73
 
74
+ ## Session identifier can be set and is not overidden even after saved
75
+ @session_id = 'session-string-coercion-test'
76
+ session = Session.new(@session_id)
77
+ session.identifier
78
+ session
79
+ #==> _.to_s == @session_id
80
+ #==> _.identifier == @session_id
81
+ #==> _.save
82
+ #==> _.identifier == @session_id
83
+ #==> _.to_s == @session_id
84
+
46
85
  ## Session to_s works with generated identifier
47
86
  @session.to_s
48
- #=> @session_id
49
-
50
- ## Method accepting string parameter works with Familia object
51
- def lookup_by_id(id_string)
52
- id_string.to_s.upcase
53
- end
87
+ #=<> @session_id
54
88
 
55
89
  lookup_by_id(@customer)
56
90
  #=> @customer_id.upcase
@@ -64,7 +98,7 @@ lookup_by_id(@customer)
64
98
  ## Array operations work with mixed types
65
99
  @mixed_array = [@customer_id, @customer, @session]
66
100
  @mixed_array.map(&:to_s)
67
- #=> [@customer_id, @customer_id, @session_id]
101
+ #=> [@customer_id, @customer_id, _[2]]
68
102
 
69
103
  ## String comparison works
70
104
  @customer.to_s == @customer_id
@@ -72,75 +106,42 @@ lookup_by_id(@customer)
72
106
 
73
107
  ## Join operations work seamlessly
74
108
  [@customer, 'separator', @session].join(':')
75
- #=> "#{@customer_id}:separator:#{@session_id}"
76
-
77
- ## Case statement works with string matching
78
- def classify_id(obj)
79
- case obj.to_s
80
- when /customer/
81
- 'customer_type'
82
- when /session/
83
- 'session_type'
84
- else
85
- 'unknown_type'
86
- end
87
- end
109
+ #=~> /\A#{@customer_id}:separator:[0-9a-z]+\z/
88
110
 
111
+ ## Classify a customer
89
112
  classify_id(@customer)
90
113
  #=> 'customer_type'
91
114
 
92
- classify_id(@session)
93
- #=> 'session_type'
94
-
95
- ## Polymorphic method accepting both strings and Familia objects
96
- def process_identifier(id_or_object)
97
- # Can handle both string IDs and Familia objects uniformly
98
- processed_id = id_or_object.to_s
99
- "processed:#{processed_id}"
100
- end
101
-
115
+ ## Polymorphic method accepting both strings and Familia objects (string)
102
116
  process_identifier(@customer_id)
103
117
  #=> "processed:#{@customer_id}"
104
118
 
119
+ ## Polymorphic method accepting both strings and Familia objects (familia)
105
120
  process_identifier(@customer)
106
121
  #=> "processed:#{@customer_id}"
107
122
 
108
- ## Redis storage using object as string key
123
+ ## Database storage using object as string key
109
124
  @metadata = Familia::HashKey.new 'metadata'
110
125
  @metadata[@customer] = 'customer_metadata'
111
126
  @metadata[@customer.to_s] # Same key access
112
127
  #=> 'customer_metadata'
113
128
 
114
- ## Cleanup after test
129
+ ## Cleanup after test, 1
115
130
  @metadata.delete!
116
131
  #=> true
117
132
 
133
+ ## Cleanup after test, 2
118
134
  @customer.delete!
119
135
  #=> true
120
136
 
137
+ ## Cleanup after test, 3
121
138
  @session.delete!
122
139
  #=> true
123
140
 
124
141
  ## to_s handles identifier errors gracefully
125
- @bad_obj.to_s.include?('BadIdentifierTest')
126
- #=> true
127
-
128
- ## Array-based identifier works with to_s
129
- @bone.to_s
130
- #=> 'test_token:test_name'
131
-
132
- ## String operations on complex identifier
133
- @bone.to_s.split(':')
134
- #=> ['test_token', 'test_name']
135
-
136
- ## Cleanup a key that does not exist
137
- @bone.delete!
138
- #=> false
139
-
140
- ## Cleanup a key that exists
141
- @bone.save
142
- @bone.delete!
143
- #=> true
142
+ badboi = BadIdentifierTest.new
143
+ badboi.to_s #.include?('BadIdentifierTest')
144
+ #=~> /BadIdentifierTest:0x[0-9a-f]+/
144
145
 
145
146
  ## Performance consideration: to_s caching behavior
146
147
  @customer2 = Customer.new('performance-test')
@@ -0,0 +1,51 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test TTL side effects
4
+ group "TTL Side Effects Edge Cases"
5
+
6
+ setup do
7
+ @session_class = Class.new(Familia::Horreum) do
8
+ identifier :session_id
9
+ field :name
10
+ field :data
11
+ feature :expiration
12
+ ttl 300 # 5 minutes
13
+ end
14
+ end
15
+
16
+ try "field update unintentionally resets TTL" do
17
+ session = @session_class.new(session_id: "test123", name: "Session")
18
+ session.save
19
+
20
+ # Set shorter TTL
21
+ session.expire(60)
22
+ original_ttl = session.realttl
23
+
24
+ # Update field - this may reset TTL unexpectedly
25
+ session.name = "Updated Session"
26
+ session.save
27
+
28
+ new_ttl = session.realttl
29
+
30
+ # TTL should remain short but may have been reset
31
+ new_ttl > original_ttl # Indicates TTL side effect
32
+ ensure
33
+ session&.delete!
34
+ end
35
+
36
+ try "batch update preserves TTL with flag" do
37
+ session = @session_class.new(session_id: "test124")
38
+ session.save
39
+ session.expire(60)
40
+
41
+ original_ttl = session.realttl
42
+
43
+ # Use update_expiration: false to preserve TTL
44
+ session.batch_update({name: "Batch Updated"}, update_expiration: false)
45
+
46
+ new_ttl = session.realttl
47
+
48
+ (original_ttl - new_ttl).abs < 5 # TTL preserved within tolerance
49
+ ensure
50
+ session&.delete!
51
+ end