familia 1.2.3 → 2.0.0.pre.pre

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 +3 -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 +15 -2
  11. data/Gemfile.lock +61 -61
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +317 -0
  15. data/familia.gemspec +8 -5
  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 -130
  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 +17 -12
  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} +5 -3
  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 -8
  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} +63 -60
  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} +20 -17
  88. data/try/models/datatype_base_try.rb +101 -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 +124 -38
  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
 
@@ -58,7 +62,6 @@ end
58
62
  @symbol_result[:name]
59
63
  #=> "John"
60
64
 
61
- ## Check name as a string
62
65
  @string_result["name"]
63
66
  #=> "John"
64
67
 
@@ -86,6 +89,7 @@ end
86
89
  @test_obj.deserialize_value('just a string')
87
90
  #=> "just a string"
88
91
 
92
+ ## A stringified number is still a stringified number
89
93
  @test_obj.deserialize_value('42')
90
94
  #=> "42"
91
95
 
@@ -93,5 +97,5 @@ end
93
97
  @test_obj.deserialize_value('invalid json')
94
98
  #=> "invalid json"
95
99
 
96
-
100
+ # Clean up
97
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,33 +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
+
9
+ # Error handling: object without proper identifier setup
8
10
  class ::BadIdentifierTest < Familia::Horreum
9
- # Error handling: object without proper identifier setup
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
- @customer_id = 'customer-string-coercion-test' # Test polymorphic string usage for Familia objects
41
+ # Test polymorphic string usage for Familia objects
16
42
  @customer = Customer.new(@customer_id)
17
43
  @customer.name = 'John Doe'
18
44
  @customer.planid = 'premium'
19
45
  @customer.save
46
+ #=> true
20
47
 
21
- @session_id = 'session-string-coercion-test'
22
- @session = Session.new(@session_id)
48
+ ## Session save
49
+ @session = Session.new
23
50
  @session.custid = @customer_id
24
51
  @session.useragent = 'Test Browser'
25
52
  @session.save
53
+ #=> true
26
54
 
27
- @bone = Bone.new # Complex identifier test with array-based identifier
55
+ ## Bone with simple identifier
56
+ @bone = Bone.new
28
57
  @bone.token = 'test_token'
29
58
  @bone.name = 'test_name'
30
-
59
+ @bone.identifier
60
+ #=> 'test_token'
31
61
 
32
62
  ## Basic to_s functionality returns identifier
33
63
  @customer.to_s
@@ -41,14 +71,20 @@ end
41
71
  @customer.to_s == @customer.identifier
42
72
  #=> true
43
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
+
44
85
  ## Session to_s works with generated identifier
45
86
  @session.to_s
46
- #=> @session_id
47
-
48
- ## Method accepting string parameter works with Familia object
49
- def lookup_by_id(id_string)
50
- id_string.to_s.upcase
51
- end
87
+ #=<> @session_id
52
88
 
53
89
  lookup_by_id(@customer)
54
90
  #=> @customer_id.upcase
@@ -62,7 +98,7 @@ lookup_by_id(@customer)
62
98
  ## Array operations work with mixed types
63
99
  @mixed_array = [@customer_id, @customer, @session]
64
100
  @mixed_array.map(&:to_s)
65
- #=> [@customer_id, @customer_id, @session_id]
101
+ #=> [@customer_id, @customer_id, _[2]]
66
102
 
67
103
  ## String comparison works
68
104
  @customer.to_s == @customer_id
@@ -70,75 +106,42 @@ lookup_by_id(@customer)
70
106
 
71
107
  ## Join operations work seamlessly
72
108
  [@customer, 'separator', @session].join(':')
73
- #=> "#{@customer_id}:separator:#{@session_id}"
74
-
75
- ## Case statement works with string matching
76
- def classify_id(obj)
77
- case obj.to_s
78
- when /customer/
79
- 'customer_type'
80
- when /session/
81
- 'session_type'
82
- else
83
- 'unknown_type'
84
- end
85
- end
109
+ #=~> /\A#{@customer_id}:separator:[0-9a-z]+\z/
86
110
 
111
+ ## Classify a customer
87
112
  classify_id(@customer)
88
113
  #=> 'customer_type'
89
114
 
90
- classify_id(@session)
91
- #=> 'session_type'
92
-
93
- ## Polymorphic method accepting both strings and Familia objects
94
- def process_identifier(id_or_object)
95
- # Can handle both string IDs and Familia objects uniformly
96
- processed_id = id_or_object.to_s
97
- "processed:#{processed_id}"
98
- end
99
-
115
+ ## Polymorphic method accepting both strings and Familia objects (string)
100
116
  process_identifier(@customer_id)
101
117
  #=> "processed:#{@customer_id}"
102
118
 
119
+ ## Polymorphic method accepting both strings and Familia objects (familia)
103
120
  process_identifier(@customer)
104
121
  #=> "processed:#{@customer_id}"
105
122
 
106
- ## Redis storage using object as string key
123
+ ## Database storage using object as string key
107
124
  @metadata = Familia::HashKey.new 'metadata'
108
125
  @metadata[@customer] = 'customer_metadata'
109
126
  @metadata[@customer.to_s] # Same key access
110
127
  #=> 'customer_metadata'
111
128
 
112
- ## Cleanup after test
129
+ ## Cleanup after test, 1
113
130
  @metadata.delete!
114
131
  #=> true
115
132
 
133
+ ## Cleanup after test, 2
116
134
  @customer.delete!
117
135
  #=> true
118
136
 
137
+ ## Cleanup after test, 3
119
138
  @session.delete!
120
139
  #=> true
121
140
 
122
141
  ## to_s handles identifier errors gracefully
123
- @bad_obj.to_s.include?('BadIdentifierTest')
124
- #=> true
125
-
126
- ## Array-based identifier works with to_s
127
- @bone.to_s
128
- #=> 'test_token:test_name'
129
-
130
- ## String operations on complex identifier
131
- @bone.to_s.split(':')
132
- #=> ['test_token', 'test_name']
133
-
134
- ## Cleanup a key that does not exist
135
- @bone.delete!
136
- #=> false
137
-
138
- ## Cleanup a key that exists
139
- @bone.save
140
- @bone.delete!
141
- #=> true
142
+ badboi = BadIdentifierTest.new
143
+ badboi.to_s #.include?('BadIdentifierTest')
144
+ #=~> /BadIdentifierTest:0x[0-9a-f]+/
142
145
 
143
146
  ## Performance consideration: to_s caching behavior
144
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