familia 2.0.0.pre24 → 2.0.0.pre25

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.
@@ -0,0 +1,349 @@
1
+ # try/features/relationships/class_level_multi_index_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Tests for class-level multi_index functionality (within: :class)
6
+ # This feature allows multi-value indexes at the class level, similar to how
7
+ # unique_index works without a within: parameter.
8
+ #
9
+ # Example: multi_index :role, :role_index (within: :class is the default)
10
+ # Creates class methods like Customer.find_all_by_role('admin')
11
+
12
+ require_relative '../../support/helpers/test_helpers'
13
+
14
+ # Test class with class-level multi_index
15
+ class ::ClassLevelCustomer < Familia::Horreum
16
+ feature :relationships
17
+ include Familia::Features::Relationships::Indexing
18
+
19
+ identifier_field :custid
20
+ field :custid
21
+ field :name
22
+ field :role
23
+ field :region
24
+
25
+ # Class-level multi_index (default: within: :class)
26
+ multi_index :role, :role_index
27
+
28
+ # Explicit within: :class (same behavior)
29
+ multi_index :region, :region_index, within: :class
30
+
31
+ # Multi_index with query: false
32
+ multi_index :name, :name_index, query: false
33
+ end
34
+
35
+ # Clean up any stale data from previous runs
36
+ %w[admin user superadmin].each do |role|
37
+ ClassLevelCustomer.dbclient.del(ClassLevelCustomer.role_index_for(role).dbkey)
38
+ end
39
+ %w[west east].each do |region|
40
+ ClassLevelCustomer.dbclient.del(ClassLevelCustomer.region_index_for(region).dbkey)
41
+ end
42
+
43
+ # Setup - create test customers with various roles and regions
44
+ @cust1 = ClassLevelCustomer.new(custid: 'cust_001', name: 'Alice', role: 'admin', region: 'west')
45
+ @cust1.save
46
+ @cust2 = ClassLevelCustomer.new(custid: 'cust_002', name: 'Bob', role: 'user', region: 'east')
47
+ @cust2.save
48
+ @cust3 = ClassLevelCustomer.new(custid: 'cust_003', name: 'Charlie', role: 'admin', region: 'west')
49
+ @cust3.save
50
+ @cust4 = ClassLevelCustomer.new(custid: 'cust_004', name: 'Diana', role: 'user', region: 'east')
51
+ @cust4.save
52
+
53
+ # =============================================
54
+ # 1. Class-Level Multi-Index Registration
55
+ # =============================================
56
+
57
+ ## Class-level multi_index relationships are registered
58
+ ClassLevelCustomer.indexing_relationships.length
59
+ #=> 3
60
+
61
+ ## First multi_index relationship has correct configuration
62
+ config = ClassLevelCustomer.indexing_relationships.first
63
+ [config.field, config.index_name, config.cardinality, config.within]
64
+ #=> [:role, :role_index, :multi, :class]
65
+
66
+ ## Second multi_index relationship (explicit within: :class)
67
+ config = ClassLevelCustomer.indexing_relationships[1]
68
+ [config.field, config.index_name, config.cardinality, config.within]
69
+ #=> [:region, :region_index, :multi, :class]
70
+
71
+ ## Third multi_index has query disabled
72
+ config = ClassLevelCustomer.indexing_relationships.last
73
+ [config.field, config.index_name, config.query]
74
+ #=> [:name, :name_index, false]
75
+
76
+ # =============================================
77
+ # 2. Generated Class Methods
78
+ # =============================================
79
+
80
+ ## Factory method is generated on the class
81
+ ClassLevelCustomer.respond_to?(:role_index_for)
82
+ #=> true
83
+
84
+ ## Factory method returns UnsortedSet
85
+ ClassLevelCustomer.role_index_for('admin').class
86
+ #=> Familia::UnsortedSet
87
+
88
+ ## Query method find_all_by_role is generated
89
+ ClassLevelCustomer.respond_to?(:find_all_by_role)
90
+ #=> true
91
+
92
+ ## Query method sample_from_role is generated
93
+ ClassLevelCustomer.respond_to?(:sample_from_role)
94
+ #=> true
95
+
96
+ ## Rebuild method is generated
97
+ ClassLevelCustomer.respond_to?(:rebuild_role_index)
98
+ #=> true
99
+
100
+ ## No query methods when query: false
101
+ ClassLevelCustomer.respond_to?(:find_all_by_name)
102
+ #=> false
103
+
104
+ ## But factory method is still available even with query: false
105
+ ClassLevelCustomer.respond_to?(:name_index_for)
106
+ #=> true
107
+
108
+ # =============================================
109
+ # 3. Generated Instance Methods
110
+ # =============================================
111
+
112
+ ## Add method is generated on instances
113
+ @cust1.respond_to?(:add_to_class_role_index)
114
+ #=> true
115
+
116
+ ## Remove method is generated on instances
117
+ @cust1.respond_to?(:remove_from_class_role_index)
118
+ #=> true
119
+
120
+ ## Update method is generated on instances
121
+ @cust1.respond_to?(:update_in_class_role_index)
122
+ #=> true
123
+
124
+ # =============================================
125
+ # 4. Manual Indexing Operations
126
+ # =============================================
127
+
128
+ ## Add customer to class index manually
129
+ @cust1.add_to_class_role_index
130
+ ClassLevelCustomer.role_index_for('admin').members.include?('cust_001')
131
+ #=> true
132
+
133
+ ## Add multiple customers to index
134
+ @cust2.add_to_class_role_index
135
+ @cust3.add_to_class_role_index
136
+ @cust4.add_to_class_role_index
137
+ ClassLevelCustomer.role_index_for('admin').size
138
+ #=> 2
139
+
140
+ ## Users index has correct members
141
+ ClassLevelCustomer.role_index_for('user').size
142
+ #=> 2
143
+
144
+ # =============================================
145
+ # 5. Query Operations
146
+ # =============================================
147
+
148
+ ## find_all_by_role returns all matching customers
149
+ admins = ClassLevelCustomer.find_all_by_role('admin')
150
+ admins.map(&:custid).sort
151
+ #=> ["cust_001", "cust_003"]
152
+
153
+ ## find_all_by_role returns empty array for non-existent role
154
+ ClassLevelCustomer.find_all_by_role('superadmin')
155
+ #=> []
156
+
157
+ ## sample_from_role returns random customer
158
+ sample = ClassLevelCustomer.sample_from_role('admin', 1)
159
+ ['cust_001', 'cust_003'].include?(sample.first&.custid)
160
+ #=> true
161
+
162
+ ## sample_from_role with count > 1
163
+ samples = ClassLevelCustomer.sample_from_role('user', 2)
164
+ samples.length
165
+ #=> 2
166
+
167
+ # =============================================
168
+ # 6. Update Operations
169
+ # =============================================
170
+
171
+ ## Update method moves customer between indexes
172
+ old_role = @cust1.role
173
+ @cust1.role = 'superadmin'
174
+ @cust1.update_in_class_role_index(old_role)
175
+ ClassLevelCustomer.role_index_for('admin').members.include?('cust_001')
176
+ #=> false
177
+
178
+ ## Customer is now in new index
179
+ ClassLevelCustomer.role_index_for('superadmin').members.include?('cust_001')
180
+ #=> true
181
+
182
+ # =============================================
183
+ # 7. Remove Operations
184
+ # =============================================
185
+
186
+ ## Remove customer from index
187
+ @cust4.remove_from_class_role_index
188
+ ClassLevelCustomer.role_index_for('user').members.include?('cust_004')
189
+ #=> false
190
+
191
+ ## Other customer in same index is unaffected
192
+ ClassLevelCustomer.role_index_for('user').members.include?('cust_002')
193
+ #=> true
194
+
195
+ # =============================================
196
+ # 8. Redis Key Pattern
197
+ # =============================================
198
+
199
+ ## Redis key follows class-level pattern
200
+ index_set = ClassLevelCustomer.role_index_for('admin')
201
+ index_set.dbkey
202
+ #=~ /classlevelcustomer:role_index:admin/
203
+
204
+ ## Different field values have different keys
205
+ key1 = ClassLevelCustomer.role_index_for('admin').dbkey
206
+ key2 = ClassLevelCustomer.role_index_for('user').dbkey
207
+ key1 != key2
208
+ #=> true
209
+
210
+ # =============================================
211
+ # 9. Region Index (explicit within: :class)
212
+ # =============================================
213
+
214
+ ## Verify region field values before indexing
215
+ [@cust1.region, @cust2.region, @cust3.region]
216
+ #=> ["west", "east", "west"]
217
+
218
+ ## Verify the add_to_class_region_index method exists
219
+ @cust1.respond_to?(:add_to_class_region_index)
220
+ #=> true
221
+
222
+ ## Debug: Check what the region_index IndexingRelationship looks like
223
+ region_config = ClassLevelCustomer.indexing_relationships.find { |c| c.index_name == :region_index }
224
+ [region_config.field, region_config.index_name, region_config.cardinality]
225
+ #=> [:region, :region_index, :multi]
226
+
227
+ ## Region index is auto-populated on save (cust1 and cust3 are 'west')
228
+ # Note: Auto-indexing is now enabled for class-level multi_index
229
+ ClassLevelCustomer.region_index_for('west').members.sort
230
+ #=> ["cust_001", "cust_003"]
231
+
232
+ ## East region is also auto-populated (cust2 and cust4 are 'east')
233
+ ClassLevelCustomer.region_index_for('east').members.sort
234
+ #=> ["cust_002", "cust_004"]
235
+
236
+ ## Manual add_to is idempotent (no duplicate entries)
237
+ @cust1.add_to_class_region_index
238
+ ClassLevelCustomer.region_index_for('west').members.sort
239
+ #=> ["cust_001", "cust_003"]
240
+
241
+ ## Verify all region indexes after auto-indexing
242
+ [ClassLevelCustomer.region_index_for('west').members.sort, ClassLevelCustomer.region_index_for('east').members.sort]
243
+ #=> [["cust_001", "cust_003"], ["cust_002", "cust_004"]]
244
+
245
+ ## Query by region works
246
+ west_customers = ClassLevelCustomer.find_all_by_region('west')
247
+ west_customers.map(&:custid).sort
248
+ #=> ["cust_001", "cust_003"]
249
+
250
+ ## East region also works
251
+ east_customers = ClassLevelCustomer.find_all_by_region('east')
252
+ east_customers.map(&:custid).sort
253
+ #=> ["cust_002", "cust_004"]
254
+
255
+ # =============================================
256
+ # 10. Edge Cases and Nil Handling
257
+ # =============================================
258
+
259
+ ## Adding to index with nil field value does nothing (no error)
260
+ @cust_nil = ClassLevelCustomer.new(custid: 'cust_nil', name: 'NilRole', role: nil, region: 'west')
261
+ @cust_nil.save
262
+ result = @cust_nil.add_to_class_role_index
263
+ result.nil?
264
+ #=> true
265
+
266
+ ## Nil role customer is not in any role index
267
+ ClassLevelCustomer.role_index_for('').members.include?('cust_nil')
268
+ #=> false
269
+
270
+ ## Adding to index with empty string field value does nothing
271
+ @cust_empty = ClassLevelCustomer.new(custid: 'cust_empty', name: 'EmptyRole', role: '', region: 'east')
272
+ @cust_empty.save
273
+ result = @cust_empty.add_to_class_role_index
274
+ result.nil?
275
+ #=> true
276
+
277
+ ## Adding to index with whitespace-only field value does nothing
278
+ @cust_whitespace = ClassLevelCustomer.new(custid: 'cust_ws', name: 'WhitespaceRole', role: ' ', region: 'east')
279
+ @cust_whitespace.save
280
+ result = @cust_whitespace.add_to_class_role_index
281
+ result.nil?
282
+ #=> true
283
+
284
+ ## find_all_by_* with nil value returns empty array
285
+ ClassLevelCustomer.find_all_by_role(nil)
286
+ #=> []
287
+
288
+ ## find_all_by_* with empty string returns empty array
289
+ ClassLevelCustomer.find_all_by_role('')
290
+ #=> []
291
+
292
+ ## sample_from_* with nil value returns empty array
293
+ ClassLevelCustomer.sample_from_role(nil, 1)
294
+ #=> []
295
+
296
+ ## sample_from_* with empty string returns empty array
297
+ ClassLevelCustomer.sample_from_role('', 1)
298
+ #=> []
299
+
300
+ ## sample_from_* with count=0 returns empty array
301
+ ClassLevelCustomer.sample_from_role('admin', 0)
302
+ #=> []
303
+
304
+ ## Update with same old and new value does nothing (no-op)
305
+ # First ensure cust3 is in the admin index
306
+ @cust3.role = 'admin'
307
+ @cust3.add_to_class_role_index
308
+ admin_count_before = ClassLevelCustomer.role_index_for('admin').size
309
+ @cust3.update_in_class_role_index('admin') # same value
310
+ admin_count_after = ClassLevelCustomer.role_index_for('admin').size
311
+ admin_count_before == admin_count_after
312
+ #=> true
313
+
314
+ ## Update when field becomes nil removes from old index only
315
+ @cust_update = ClassLevelCustomer.new(custid: 'cust_update', name: 'WillBeNil', role: 'tempuser', region: 'west')
316
+ @cust_update.save
317
+ @cust_update.add_to_class_role_index
318
+ ClassLevelCustomer.role_index_for('tempuser').members.include?('cust_update')
319
+ #=> true
320
+
321
+ ## After setting role to nil and updating, customer is removed from old index
322
+ old_role = @cust_update.role
323
+ @cust_update.role = nil
324
+ @cust_update.update_in_class_role_index(old_role)
325
+ ClassLevelCustomer.role_index_for('tempuser').members.include?('cust_update')
326
+ #=> false
327
+
328
+ ## Update with nil old_value returns early (no-op)
329
+ @cust3.role = 'admin'
330
+ result = @cust3.update_in_class_role_index(nil)
331
+ result.nil?
332
+ #=> true
333
+
334
+ # Teardown
335
+ @cust1.delete!
336
+ @cust2.delete!
337
+ @cust3.delete!
338
+ @cust4.delete!
339
+ @cust_nil&.delete!
340
+ @cust_empty&.delete!
341
+ @cust_whitespace&.delete!
342
+ @cust_update&.delete!
343
+ # Clean up index keys
344
+ ClassLevelCustomer.dbclient.del(ClassLevelCustomer.role_index_for('admin').dbkey)
345
+ ClassLevelCustomer.dbclient.del(ClassLevelCustomer.role_index_for('user').dbkey)
346
+ ClassLevelCustomer.dbclient.del(ClassLevelCustomer.role_index_for('superadmin').dbkey)
347
+ ClassLevelCustomer.dbclient.del(ClassLevelCustomer.role_index_for('tempuser').dbkey)
348
+ ClassLevelCustomer.dbclient.del(ClassLevelCustomer.region_index_for('west').dbkey)
349
+ ClassLevelCustomer.dbclient.del(ClassLevelCustomer.region_index_for('east').dbkey)
@@ -11,7 +11,7 @@ require_relative '../support/helpers/test_helpers'
11
11
  ## Has all datatype relativess
12
12
  registered_types = Familia::DataType.registered_types.keys
13
13
  registered_types.collect(&:to_s).sort
14
- #=> ["counter", "hash", "hashkey", "list", "listkey", "lock", "set", "sorted_set", "string", "stringkey", "unsorted_set", "zset"]
14
+ #=> ["counter", "hash", "hashkey", "json_string", "json_stringkey", "jsonkey", "list", "listkey", "lock", "set", "sorted_set", "string", "stringkey", "unsorted_set", "zset"]
15
15
 
16
16
  ## Familia created class methods for datatype list class
17
17
  Familia::Horreum::DefinitionMethods.public_method_defined? :list?
@@ -65,7 +65,7 @@ rescue StandardError => e
65
65
  end
66
66
  #=> false
67
67
 
68
- ## serialization method configuration methods exist
68
+ ## JSON serialization is hard-coded for consistency
69
69
  begin
70
70
  custom_class = Class.new(Familia::Horreum) do
71
71
  identifier_field :id
@@ -73,8 +73,9 @@ begin
73
73
  field :data
74
74
  end
75
75
 
76
- # Check if these methods exist
77
- custom_class.respond_to?(:dump_method) && custom_class.respond_to?(:load_method)
76
+ # Verify that custom serialization methods have been removed
77
+ # dump_method and load_method are no longer available
78
+ !custom_class.respond_to?(:dump_method) && !custom_class.respond_to?(:load_method)
78
79
  rescue StandardError => e
79
80
  false
80
81
  end