familia 2.0.0.pre16 → 2.0.0.pre17

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/{code-smellage.yml → code-smells.yml} +3 -63
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +6 -0
  6. data/CHANGELOG.rst +22 -0
  7. data/CLAUDE.md +38 -0
  8. data/Gemfile.lock +1 -1
  9. data/docs/archive/FAMILIA_TECHNICAL.md +1 -1
  10. data/docs/overview.md +2 -2
  11. data/docs/reference/api-technical.md +1 -1
  12. data/examples/encrypted_fields.rb +1 -1
  13. data/examples/safe_dump.rb +1 -1
  14. data/lib/familia/base.rb +6 -4
  15. data/lib/familia/data_type/class_methods.rb +63 -0
  16. data/lib/familia/data_type/connection.rb +83 -0
  17. data/lib/familia/data_type/settings.rb +96 -0
  18. data/lib/familia/data_type/types/hashkey.rb +2 -1
  19. data/lib/familia/data_type/types/sorted_set.rb +113 -10
  20. data/lib/familia/data_type/types/stringkey.rb +0 -4
  21. data/lib/familia/data_type.rb +6 -193
  22. data/lib/familia/features/encrypted_fields.rb +5 -2
  23. data/lib/familia/features/external_identifier.rb +49 -8
  24. data/lib/familia/features/object_identifier.rb +84 -12
  25. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +6 -1
  26. data/lib/familia/features/relationships/indexing.rb +7 -1
  27. data/lib/familia/features/relationships/participation/participant_methods.rb +6 -2
  28. data/lib/familia/features/transient_fields.rb +7 -2
  29. data/lib/familia/features.rb +6 -1
  30. data/lib/familia/field_type.rb +0 -18
  31. data/lib/familia/horreum/{core/connection.rb → connection.rb} +21 -0
  32. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +109 -32
  33. data/lib/familia/horreum/{subclass/management.rb → management.rb} +1 -3
  34. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +72 -169
  35. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +22 -2
  36. data/lib/familia/horreum/serialization.rb +172 -0
  37. data/lib/familia/horreum.rb +29 -8
  38. data/lib/familia/version.rb +1 -1
  39. data/try/configuration/scenarios_try.rb +1 -1
  40. data/try/core/connection_try.rb +4 -4
  41. data/try/core/database_consistency_try.rb +1 -0
  42. data/try/core/errors_try.rb +3 -3
  43. data/try/core/familia_try.rb +1 -1
  44. data/try/core/isolated_dbclient_try.rb +2 -2
  45. data/try/core/tools_try.rb +2 -2
  46. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  47. data/try/features/field_groups_try.rb +244 -0
  48. data/try/features/relationships/indexing_try.rb +10 -0
  49. data/try/features/transient_fields/refresh_reset_try.rb +2 -0
  50. data/try/helpers/test_helpers.rb +3 -4
  51. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  52. data/try/horreum/commands_try.rb +2 -0
  53. data/try/horreum/defensive_initialization_try.rb +86 -0
  54. data/try/horreum/destroy_related_fields_cleanup_try.rb +2 -0
  55. data/try/horreum/settings_try.rb +2 -0
  56. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  57. data/try/models/customer_try.rb +5 -5
  58. data/try/valkey.conf +26 -0
  59. metadata +19 -11
  60. data/lib/familia/horreum/core.rb +0 -21
  61. /data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +0 -0
  62. /data/lib/familia/horreum/{shared/settings.rb → settings.rb} +0 -0
  63. /data/lib/familia/horreum/{core/utils.rb → utils.rb} +0 -0
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ # try/features/field_groups_try.rb
4
+
5
+ require_relative '../../lib/familia'
6
+
7
+ # Define test classes in setup section
8
+ class BasicUser < Familia::Horreum
9
+ field_group :personal_info do
10
+ field :name
11
+ field :email
12
+ end
13
+ end
14
+
15
+ class MultiGroupUser < Familia::Horreum
16
+ field_group :personal do
17
+ field :name
18
+ field :email
19
+ end
20
+
21
+ field_group :metadata do
22
+ field :created_at
23
+ field :updated_at
24
+ end
25
+ end
26
+
27
+ class EmptyGroupModel < Familia::Horreum
28
+ field_group :placeholder
29
+ end
30
+
31
+ class TransientModel < Familia::Horreum
32
+ feature :transient_fields
33
+ transient_field :api_key
34
+ transient_field :session_token
35
+ end
36
+
37
+ class EncryptedModel < Familia::Horreum
38
+ feature :encrypted_fields
39
+ encrypted_field :password
40
+ encrypted_field :credit_card
41
+ end
42
+
43
+ class MixedGroupsModel < Familia::Horreum
44
+ feature :transient_fields
45
+ transient_field :temp_data
46
+
47
+ field_group :custom do
48
+ field :custom_field
49
+ end
50
+
51
+ feature :encrypted_fields
52
+ encrypted_field :secret_key
53
+ end
54
+
55
+ class FieldsOutsideGroups < Familia::Horreum
56
+ field :standalone_field
57
+
58
+ field_group :grouped do
59
+ field :grouped_field
60
+ end
61
+ end
62
+
63
+ class NoSuchGroup < Familia::Horreum
64
+ field_group :existing do
65
+ field :name
66
+ end
67
+ end
68
+
69
+ class ParentModel < Familia::Horreum
70
+ field_group :base_fields do
71
+ field :id
72
+ end
73
+ end
74
+
75
+ class ChildModel < ParentModel
76
+ field_group :child_fields do
77
+ field :name
78
+ end
79
+ end
80
+
81
+ # Create instances for testing
82
+ @user = MultiGroupUser.new(name: 'Alice', email: 'alice@example.com', created_at: Time.now.to_i)
83
+ @user2 = BasicUser.new(name: 'Bob', email: 'bob@example.com')
84
+
85
+ ## Manual field groups - basic access via hash
86
+ BasicUser.instance_variable_get(:@field_groups)[:personal_info]
87
+ #=> [:name, :email]
88
+
89
+ ## Multiple groups - access personal group via hash
90
+ MultiGroupUser.instance_variable_get(:@field_groups)[:personal]
91
+ #=> [:name, :email]
92
+
93
+ ## Multiple groups - access metadata group via hash
94
+ MultiGroupUser.instance_variable_get(:@field_groups)[:metadata]
95
+ #=> [:created_at, :updated_at]
96
+
97
+ ## Multiple groups - list all field groups (returns hash)
98
+ MultiGroupUser.field_groups.keys.sort
99
+ #=> [:metadata, :personal]
100
+
101
+ ## Field groups - fields defined inside groups are tracked
102
+ user = MultiGroupUser.new(name: 'Alice', email: 'alice@example.com', created_at: Time.now.to_i)
103
+
104
+ ## Grouped fields - access name field
105
+ @user.name
106
+ #=> 'Alice'
107
+
108
+ ## Grouped fields - access email field
109
+ @user.email
110
+ #=> 'alice@example.com'
111
+
112
+ ## Empty group - access via hash
113
+ EmptyGroupModel.instance_variable_get(:@field_groups)[:placeholder]
114
+ #=> []
115
+
116
+ ## Empty group - list field groups (returns hash)
117
+ EmptyGroupModel.field_groups
118
+ #=> {placeholder: []}
119
+
120
+ ## Empty group - list field group keys
121
+ EmptyGroupModel.field_groups.keys
122
+ #=> [:placeholder]
123
+
124
+ ## Transient feature - access via backward compatible method
125
+ TransientModel.transient_fields
126
+ #=> [:api_key, :session_token]
127
+
128
+ ## Transient feature - access via field_groups hash
129
+ TransientModel.instance_variable_get(:@field_groups)[:transient_fields]
130
+ #=> [:api_key, :session_token]
131
+
132
+ ## Transient feature - field_groups returns hash with content
133
+ TransientModel.field_groups
134
+ #=> {transient_fields: [:api_key, :session_token]}
135
+
136
+ ## Transient feature - list field group keys
137
+ TransientModel.field_groups.keys
138
+ #=> [:transient_fields]
139
+
140
+ ## Encrypted feature - access via backward compatible method
141
+ EncryptedModel.encrypted_fields
142
+ #=> [:password, :credit_card]
143
+
144
+ ## Encrypted feature - access via field_groups hash
145
+ EncryptedModel.instance_variable_get(:@field_groups)[:encrypted_fields]
146
+ #=> [:password, :credit_card]
147
+
148
+ ## Encrypted feature - field_groups returns hash with content
149
+ EncryptedModel.field_groups
150
+ #=> {encrypted_fields: [:password, :credit_card]}
151
+
152
+ ## Encrypted feature - list field group keys
153
+ EncryptedModel.field_groups.keys
154
+ #=> [:encrypted_fields]
155
+
156
+ ## Mixed groups - list all field group keys
157
+ MixedGroupsModel.field_groups.keys.sort
158
+ #=> [:custom, :encrypted_fields, :transient_fields]
159
+
160
+ ## Mixed groups - access custom group via hash
161
+ MixedGroupsModel.instance_variable_get(:@field_groups)[:custom]
162
+ #=> [:custom_field]
163
+
164
+ ## Mixed groups - access transient_fields via backward compatible method
165
+ MixedGroupsModel.transient_fields
166
+ #=> [:temp_data]
167
+
168
+ ## Mixed groups - access encrypted_fields via backward compatible method
169
+ MixedGroupsModel.encrypted_fields
170
+ #=> [:secret_key]
171
+
172
+ ## Error: nested field groups
173
+ class NestedGroupsModel < Familia::Horreum
174
+ field_group :outer do
175
+ field_group :inner do
176
+ field :bad
177
+ end
178
+ end
179
+ end
180
+ #=!> Familia::Problem
181
+
182
+ ## Exception during field_group block resets @current_field_group
183
+ class ErrorDuringGroup < Familia::Horreum
184
+ begin
185
+ field_group :broken do
186
+ field :first_field
187
+ raise StandardError, "Simulated error"
188
+ field :unreachable_field
189
+ end
190
+ rescue StandardError
191
+ # Swallow the error for testing
192
+ end
193
+
194
+ # Field defined after the error should not be in :broken group
195
+ field :after_error
196
+ end
197
+
198
+ ErrorDuringGroup
199
+ #=> ErrorDuringGroup
200
+
201
+ ## Exception handling - broken group has only first_field
202
+ ErrorDuringGroup.instance_variable_get(:@field_groups)[:broken]
203
+ #=> [:first_field]
204
+
205
+ ## Exception handling - after_error field is not in broken group
206
+ ErrorDuringGroup.instance_variable_get(:@field_groups)[:broken].include?(:after_error)
207
+ #=> false
208
+
209
+ ## Exception handling - after_error is in fields list
210
+ ErrorDuringGroup.fields.include?(:after_error)
211
+ #=> true
212
+
213
+ ## Exception handling - current_field_group was reset to nil
214
+ ErrorDuringGroup.instance_variable_get(:@current_field_group)
215
+ #=> nil
216
+
217
+
218
+ ## Fields outside - access grouped field group via hash
219
+ FieldsOutsideGroups.instance_variable_get(:@field_groups)[:grouped]
220
+ #=> [:grouped_field]
221
+
222
+ ## Fields outside - all fields include both grouped and standalone
223
+ FieldsOutsideGroups.fields
224
+ #=> [:standalone_field, :grouped_field]
225
+
226
+ ## Accessing non-existent field group returns nil
227
+ NoSuchGroup.instance_variable_get(:@field_groups)[:nonexistent]
228
+ #=> nil
229
+
230
+ ## Inheritance - parent class has its own field groups
231
+ ParentModel.field_groups
232
+ #=> {base_fields: [:id]}
233
+
234
+ ## Inheritance - child class has its own field groups
235
+ ChildModel.field_groups
236
+ #=> {child_fields: [:name]}
237
+
238
+ ## Normal field access - get name value
239
+ @user2.name
240
+ #=> 'Bob'
241
+
242
+ ## Normal field access - get email value
243
+ @user2.email
244
+ #=> 'bob@example.com'
@@ -152,6 +152,11 @@ found_users.map(&:user_id).sort
152
152
  TestUser.find_all_by_email([]).length
153
153
  #=> 0
154
154
 
155
+ ## Single value (non-array) is accepted by find_all_by method
156
+ found_users = TestUser.find_all_by_email('bob@example.com')
157
+ found_users.map(&:user_id)
158
+ #=> ["user_002"]
159
+
155
160
  ## Update index entry with old value removal
156
161
  old_email = @user1.email
157
162
  @user1.email = 'alice.new@example.com'
@@ -236,6 +241,11 @@ found_emps = @company.find_all_by_badge_number(badges)
236
241
  found_emps.map(&:emp_id).sort
237
242
  #=> ["emp_001", "emp_002"]
238
243
 
244
+ ## Single value (non-array) accepted for instance-scoped find_all_by
245
+ found_emps = @company.find_all_by_badge_number('BADGE002')
246
+ found_emps.map(&:emp_id)
247
+ #=> ["emp_002"]
248
+
239
249
  ## Update badge index entry
240
250
  old_badge = @emp1.badge_number
241
251
  @emp1.badge_number = 'BADGE001_NEW'
@@ -8,6 +8,8 @@ Familia.debug = false
8
8
  Familia.dbclient.flushdb
9
9
 
10
10
  class SecretService < Familia::Horreum
11
+ feature :transient_fields
12
+
11
13
  identifier_field :name
12
14
 
13
15
  field :name
@@ -10,6 +10,7 @@ require_relative '../../lib/familia'
10
10
 
11
11
  Familia.enable_database_logging = true
12
12
  Familia.enable_database_counter = true
13
+ Familia.uri = 'redis://127.0.0.1:2525'
13
14
 
14
15
  class Bone < Familia::Horreum
15
16
  using Familia::Refinements::TimeLiterals
@@ -44,12 +45,10 @@ class Customer < Familia::Horreum
44
45
 
45
46
  using Familia::Refinements::TimeLiterals
46
47
 
47
- logical_database 15 # Use something other than the default DB
48
+ logical_database 3 # Use something other than the default DB
48
49
  default_expiration 5.years
49
50
 
50
51
  feature :safe_dump
51
- # feature :expiration
52
- # feature :api_version
53
52
 
54
53
  # Use new SafeDump DSL instead of @safe_dump_fields
55
54
  safe_dump_field :custid
@@ -108,7 +107,7 @@ end
108
107
  class Session < Familia::Horreum
109
108
  using Familia::Refinements::TimeLiterals
110
109
 
111
- logical_database 14 # don't use Onetime's default DB
110
+ logical_database 2 # a non-default database
112
111
  default_expiration 180.minutes
113
112
 
114
113
  identifier_field :sessid
@@ -0,0 +1,212 @@
1
+ # try/horreum/auto_indexing_on_save_try.rb
2
+
3
+ #
4
+ # Auto-indexing on save functionality tests
5
+ # Tests automatic index population when Familia::Horreum objects are saved
6
+ #
7
+
8
+ require_relative '../helpers/test_helpers'
9
+
10
+ # Test classes for auto-indexing functionality
11
+ class ::AutoIndexUser < Familia::Horreum
12
+ feature :relationships
13
+
14
+ identifier_field :user_id
15
+ field :user_id
16
+ field :email
17
+ field :username
18
+ field :department
19
+
20
+ # Class-level unique indexes (should auto-populate on save)
21
+ unique_index :email, :email_index
22
+ unique_index :username, :username_index
23
+ end
24
+
25
+ class ::AutoIndexCompany < Familia::Horreum
26
+ feature :relationships
27
+
28
+ identifier_field :company_id
29
+ field :company_id
30
+ field :name
31
+ end
32
+
33
+ class ::AutoIndexEmployee < Familia::Horreum
34
+ feature :relationships
35
+
36
+ identifier_field :emp_id
37
+ field :emp_id
38
+ field :badge_number
39
+ field :department
40
+
41
+ # Instance-scoped indexes (should NOT auto-populate - require parent context)
42
+ unique_index :badge_number, :badge_index, within: AutoIndexCompany
43
+ multi_index :department, :dept_index, within: AutoIndexCompany
44
+ end
45
+
46
+ # Setup
47
+ @user_id = "user_#{rand(1000000)}"
48
+ @user = AutoIndexUser.new(user_id: @user_id, email: 'test@example.com', username: 'testuser', department: 'engineering')
49
+
50
+ @company_id = "comp_#{rand(1000000)}"
51
+ @company = AutoIndexCompany.new(company_id: @company_id, name: 'Test Corp')
52
+
53
+ @emp_id = "emp_#{rand(1000000)}"
54
+ @employee = AutoIndexEmployee.new(emp_id: @emp_id, badge_number: 'BADGE123', department: 'sales')
55
+
56
+ # =============================================
57
+ # 1. Class-Level Unique Index Auto-Population
58
+ # =============================================
59
+
60
+ ## Unique index is empty before save
61
+ AutoIndexUser.email_index.has_key?('test@example.com')
62
+ #=> false
63
+
64
+ ## Save automatically populates unique index
65
+ @user.save
66
+ AutoIndexUser.email_index.has_key?('test@example.com')
67
+ #=> true
68
+
69
+ ## Auto-populated index maps to correct identifier
70
+ AutoIndexUser.email_index.get('test@example.com')
71
+ #=> @user_id
72
+
73
+ ## Finder method works after auto-indexing
74
+ found = AutoIndexUser.find_by_email('test@example.com')
75
+ found&.user_id
76
+ #=> @user_id
77
+
78
+ ## Multiple unique indexes auto-populate on same save
79
+ AutoIndexUser.username_index.get('testuser')
80
+ #=> @user_id
81
+
82
+ ## Subsequent saves maintain index (idempotent)
83
+ @user.save
84
+ AutoIndexUser.email_index.get('test@example.com')
85
+ #=> @user_id
86
+
87
+ ## Changing indexed field and saving adds new entry (old entry remains unless manually removed)
88
+ # Note: Auto-indexing is idempotent addition only - updates require manual update_in_class_* calls
89
+ @user.email = 'newemail@example.com'
90
+ @user.save
91
+ # New email is indexed, but old email remains (expected behavior - use update_in_class_* for proper updates)
92
+ [AutoIndexUser.email_index.has_key?('test@example.com'), AutoIndexUser.email_index.get('newemail@example.com') == @user_id]
93
+ #=> [true, true]
94
+
95
+ # =============================================
96
+ # 2. Instance-Scoped Indexes (Manual Only)
97
+ # =============================================
98
+
99
+ ## Instance-scoped indexes do NOT auto-populate on save
100
+ @employee.save
101
+ @company.badge_index.has_key?('BADGE123')
102
+ #=> false
103
+
104
+ ## Instance-scoped indexes remain manual (require parent context)
105
+ @employee.add_to_auto_index_company_badge_index(@company)
106
+ @company.badge_index.has_key?('BADGE123')
107
+ #=> true
108
+
109
+ # =============================================
110
+ # 3. Edge Cases and Error Handling
111
+ # =============================================
112
+
113
+ ## Nil field values handled gracefully
114
+ @user_nil_id = "user_nil_#{rand(1000000)}"
115
+ @user_nil = AutoIndexUser.new(user_id: @user_nil_id, email: nil, username: nil, department: nil)
116
+ @user_nil.save
117
+ AutoIndexUser.email_index.has_key?('')
118
+ #=> false
119
+
120
+ ## Empty string field values handled gracefully
121
+ @user_empty_id = "user_empty_#{rand(1000000)}"
122
+ @user_empty = AutoIndexUser.new(user_id: @user_empty_id, email: '', username: '', department: '')
123
+ @user_empty.save
124
+ # Empty strings are indexed (they're valid string values, just empty)
125
+ AutoIndexUser.email_index.has_key?('')
126
+ #=> true
127
+
128
+ ## Auto-indexing works with create method
129
+ @user2_id = "user_#{rand(1000000)}"
130
+ @user2 = AutoIndexUser.create(user_id: @user2_id, email: 'create@example.com', username: 'createuser', department: 'marketing')
131
+ AutoIndexUser.find_by_email('create@example.com')&.user_id
132
+ #=> @user2_id
133
+
134
+ ## Auto-indexing idempotent with multiple saves
135
+ @user2.save
136
+ @user2.save
137
+ @user2.save
138
+ AutoIndexUser.email_index.get('create@example.com')
139
+ #=> @user2_id
140
+
141
+ ## Field update followed by save adds new entry (use update_in_class_* for proper updates)
142
+ old_email = @user2.email
143
+ @user2.email = 'updated@example.com'
144
+ @user2.save
145
+ # Both old and new emails are indexed (auto-indexing doesn't remove old values)
146
+ # For proper updates that remove old values, use: @user2.update_in_class_email_index(old_email)
147
+ [AutoIndexUser.email_index.has_key?(old_email), AutoIndexUser.email_index.get('updated@example.com') == @user2_id]
148
+ #=> [true, true]
149
+
150
+ # =============================================
151
+ # 4. Integration with Other Features
152
+ # =============================================
153
+
154
+ ## Auto-indexing works with transient fields
155
+ class ::AutoIndexWithTransient < Familia::Horreum
156
+ feature :transient_fields
157
+ feature :relationships
158
+
159
+ identifier_field :id
160
+ field :id
161
+ field :email
162
+ transient_field :temp_value
163
+
164
+ unique_index :email, :email_index
165
+ end
166
+
167
+ @transient_id = "trans_#{rand(1000000)}"
168
+ @transient_obj = AutoIndexWithTransient.new(id: @transient_id, email: 'transient@example.com', temp_value: 'ignored')
169
+ @transient_obj.save
170
+ AutoIndexWithTransient.find_by_email('transient@example.com')&.id
171
+ #=> @transient_id
172
+
173
+ ## Auto-indexing works regardless of other features
174
+ # Just verify that the feature system doesn't interfere
175
+ @transient_obj.class.respond_to?(:indexing_relationships)
176
+ #=> true
177
+
178
+ # =============================================
179
+ # 5. Performance and Behavior Verification
180
+ # =============================================
181
+
182
+ ## Auto-indexing has negligible overhead (no existence checks)
183
+ # This test verifies the design: we use idempotent commands (HSET, SADD)
184
+ # rather than checking if the index exists before updating
185
+ @user4_id = "user_#{rand(1000000)}"
186
+ @user4 = AutoIndexUser.new(user_id: @user4_id, email: 'perf@example.com', username: 'perfuser', department: 'ops')
187
+
188
+ # Save multiple times - all should succeed with same result
189
+ @user4.save
190
+ @user4.save
191
+ @user4.save
192
+
193
+ AutoIndexUser.email_index.get('perf@example.com')
194
+ #=> @user4_id
195
+
196
+ ## Auto-indexing only processes class-level indexes
197
+ # Verify no errors when instance-scoped indexes present
198
+ @employee2_id = "emp_#{rand(1000000)}"
199
+ @employee2 = AutoIndexEmployee.new(emp_id: @employee2_id, badge_number: 'BADGE456', department: 'engineering')
200
+ @employee2.save # Should not error, just skip instance-scoped indexes
201
+ @employee2.emp_id
202
+ #=> @employee2_id
203
+
204
+ # Teardown - clean up test objects
205
+ [@user, @user2, @user4, @user_nil, @user_empty, @company, @employee, @employee2, @transient_obj].each do |obj|
206
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
207
+ end
208
+
209
+ # Clean up class-level indexes
210
+ [AutoIndexUser.email_index, AutoIndexUser.username_index].each do |index|
211
+ index.delete! if index.respond_to?(:delete!) && index.respond_to?(:exists?) && index.exists?
212
+ end
@@ -1,3 +1,5 @@
1
+ # try/horreum/commands_try.rb
2
+
1
3
  # Test Horreum Valkey/Redis commands
2
4
 
3
5
  require_relative '../helpers/test_helpers'
@@ -0,0 +1,86 @@
1
+ # try/horreum/defensive_initialization_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+
5
+ # Test defensive initialization behavior
6
+ class User < Familia::Horreum
7
+ field :email
8
+ list :sessions
9
+ zset :metrics
10
+
11
+ def initialize(email = nil)
12
+ # This is the common mistake - overriding initialize without calling super
13
+ @email = email
14
+ # Missing: super() or initialize_relatives
15
+ end
16
+ end
17
+
18
+ class SafeUser < Familia::Horreum
19
+ field :email
20
+ list :sessions
21
+ zset :metrics
22
+
23
+ def init
24
+ # This is the correct way - using the init hook
25
+ # Fields are already set by initialize, no need to override
26
+ end
27
+ end
28
+
29
+ # Setup instances for testing
30
+ @user = User.new("test@example.com")
31
+ @safe_user = SafeUser.new
32
+ @safe_user.email = "safe@example.com"
33
+
34
+ ## Test that accessing relationships after bad initialize triggers lazy initialization
35
+ @user.email
36
+ #=> "test@example.com"
37
+
38
+ ## Test that sessions works with lazy initialization
39
+ @user.sessions.class
40
+ #=> Familia::ListKey
41
+
42
+ ## Test that metrics also works with lazy initialization
43
+ @user.metrics.class
44
+ #=> Familia::SortedSet
45
+
46
+ ## Test that safe user works normally
47
+ @safe_user.email
48
+ #=> "safe@example.com"
49
+
50
+ ## Test that safe user sessions work
51
+ @safe_user.sessions.class
52
+ #=> Familia::ListKey
53
+
54
+ ## Test that relatives_initialized flag prevents double initialization
55
+ @user.singleton_class.instance_variable_get(:@relatives_initialized)
56
+ #=> true
57
+
58
+ ## Test that manual initialize_relatives call is no-op
59
+ @user.initialize_relatives
60
+ @user.sessions.class
61
+ #=> Familia::ListKey
62
+
63
+ ## Test that the original problem is now fixed - bad override still works
64
+ class BadUser < Familia::Horreum
65
+ field :email
66
+ list :sessions
67
+
68
+ def initialize(email)
69
+ # Bad: overriding initialize without calling super
70
+ @email = email
71
+ # Missing: super() or initialize_relatives
72
+ end
73
+ end
74
+
75
+ @bad_user = BadUser.new("bad@example.com")
76
+ @bad_user.email
77
+ #=> "bad@example.com"
78
+
79
+ ## Test that relationships work despite bad initialize (lazy initialization kicks in)
80
+ @bad_user.sessions.class
81
+ #=> Familia::ListKey
82
+
83
+ ## Test that the bad user can actually use the relationships
84
+ @bad_user.sessions.add("session_123")
85
+ @bad_user.sessions.size > 0
86
+ #=> true
@@ -1,3 +1,5 @@
1
+ # try/horreum/destroy_related_fields_cleanup_try.rb
2
+
1
3
  # Horreum destroy! Related Fields Cleanup Tryouts
2
4
  #
3
5
  # Tests that when a Horreum instance is destroyed, all its related fields
@@ -1,3 +1,5 @@
1
+ # try/horreum/settings_try.rb
2
+
1
3
  # Test Horreum settings
2
4
 
3
5
  require_relative '../helpers/test_helpers'
@@ -59,7 +59,7 @@ docker exec $CONTAINER_ID bash -c '
59
59
  # $
60
60
  # $ docker run --rm -d -p 3000:3000 \
61
61
  # -e SECRET=$SECRET \
62
- # -e REDIS_URL=redis://host.docker.internal:6379/0 \
62
+ # -e REDIS_URL=redis://host.docker.internal:2525/0 \
63
63
  # ghcr.io/onetimesecret/devtimesecret-lite:latest
64
64
  #
65
65
  # abcd1234
@@ -103,15 +103,15 @@ exists = Customer.exists?('test@example.com')
103
103
 
104
104
  ## Customer.logical_database returns the correct database number
105
105
  Customer.logical_database
106
- #=> 15
106
+ #=> 3
107
107
 
108
108
  ## Customer.logical_database returns the correct database number
109
109
  @customer.logical_database
110
- #=> 15
110
+ #=> 3
111
111
 
112
112
  ## @customer.dbclient.connection returns the correct database URI
113
113
  @customer.dbclient.connection
114
- #=> {:host=>"127.0.0.1", :port=>6379, :db=>15, :id=>"redis://127.0.0.1:6379/15", :location=>"127.0.0.1:6379"}
114
+ #=> {:host=>"127.0.0.1", :port=>2525, :db=>3, :id=>"redis://127.0.0.1:2525/3", :location=>"127.0.0.1:2525"}
115
115
 
116
116
  ## @customer.dbclient.uri returns the correct database URI
117
117
  @customer.secrets_created.logical_database
@@ -119,7 +119,7 @@ Customer.logical_database
119
119
 
120
120
  ## @customer.dbclient.uri returns the correct database URI
121
121
  @customer.secrets_created.dbclient.connection
122
- #=> {:host=>"127.0.0.1", :port=>6379, :db=>15, :id=>"redis://127.0.0.1:6379/15", :location=>"127.0.0.1:6379"}
122
+ #=> {:host=>"127.0.0.1", :port=>2525, :db=>3, :id=>"redis://127.0.0.1:2525/3", :location=>"127.0.0.1:2525"}
123
123
 
124
124
  ## Customer.url is nil by default
125
125
  Customer.uri
@@ -131,7 +131,7 @@ Customer.instances.logical_database
131
131
 
132
132
  ## Customer.logical_database returns the correct database number
133
133
  Customer.instances.uri.to_s
134
- #=> 'redis://127.0.0.1/15'
134
+ #=> 'redis://127.0.0.1/3'
135
135
 
136
136
  # Teardown
137
137
  Customer.instances.delete!
data/try/valkey.conf ADDED
@@ -0,0 +1,26 @@
1
+ # try/test-valkey.conf
2
+
3
+ # Familia - Tryouts Valkey Config
4
+ # 2025-10-01
5
+ #
6
+ # Usage:
7
+ #
8
+ # $ valkey-server try/valkey.conf
9
+ #
10
+
11
+ dir ./data
12
+
13
+ enable-debug-command yes
14
+
15
+ #requirepass CHANGEME
16
+
17
+ bind 127.0.0.1
18
+ port 2525
19
+ databases 10
20
+
21
+ timeout 4
22
+ daemonize no
23
+ loglevel notice
24
+
25
+ # Disable RDB persistence for tests DB
26
+ save ""