familia 2.0.0.pre23 → 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,393 @@
1
+ # try/features/relationships/class_level_multi_index_rebuild_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Tests for class-level multi_index rebuild functionality
6
+ # This feature allows rebuilding of multi-value indexes at the class level.
7
+ #
8
+ # The rebuild method:
9
+ # - Enumerates all instances via class_sorted_set :instances
10
+ # - Clears existing index sets using SCAN
11
+ # - Rebuilds indexes from current field values
12
+ # - Supports progress callbacks for monitoring
13
+
14
+ require_relative '../../support/helpers/test_helpers'
15
+
16
+ # Test class with class-level multi_index and instances collection
17
+ class ::RebuildCustomer < Familia::Horreum
18
+ feature :relationships
19
+ include Familia::Features::Relationships::Indexing
20
+
21
+ identifier_field :custid
22
+ field :custid
23
+ field :name
24
+ field :tier # premium, standard, free
25
+
26
+ class_sorted_set :instances, reference: true # Required for rebuild
27
+
28
+ multi_index :tier, :tier_index
29
+ end
30
+
31
+ # Test class without instances collection (for prerequisite testing)
32
+ class ::RebuildCustomerNoInstances < Familia::Horreum
33
+ feature :relationships
34
+ include Familia::Features::Relationships::Indexing
35
+
36
+ identifier_field :custid
37
+ field :custid
38
+ field :tier
39
+
40
+ multi_index :tier, :tier_index
41
+ end
42
+
43
+ # Clean up any stale data from previous runs
44
+ %w[premium standard free vip obsolete].each do |tier|
45
+ RebuildCustomer.dbclient.del(RebuildCustomer.tier_index_for(tier).dbkey)
46
+ end
47
+ RebuildCustomer.instances.clear
48
+
49
+ # Setup - create test customers with various tiers
50
+ @cust1 = RebuildCustomer.new(custid: 'rc_001', name: 'Alice', tier: 'premium')
51
+ @cust1.save
52
+ @cust2 = RebuildCustomer.new(custid: 'rc_002', name: 'Bob', tier: 'standard')
53
+ @cust2.save
54
+ @cust3 = RebuildCustomer.new(custid: 'rc_003', name: 'Charlie', tier: 'premium')
55
+ @cust3.save
56
+ @cust4 = RebuildCustomer.new(custid: 'rc_004', name: 'Diana', tier: 'free')
57
+ @cust4.save
58
+ @cust5 = RebuildCustomer.new(custid: 'rc_005', name: 'Eve', tier: 'standard')
59
+ @cust5.save
60
+
61
+ # Register instances
62
+ RebuildCustomer.instances.add(@cust1.identifier)
63
+ RebuildCustomer.instances.add(@cust2.identifier)
64
+ RebuildCustomer.instances.add(@cust3.identifier)
65
+ RebuildCustomer.instances.add(@cust4.identifier)
66
+ RebuildCustomer.instances.add(@cust5.identifier)
67
+
68
+ # Manually add to indexes for initial state
69
+ @cust1.add_to_class_tier_index
70
+ @cust2.add_to_class_tier_index
71
+ @cust3.add_to_class_tier_index
72
+ @cust4.add_to_class_tier_index
73
+ @cust5.add_to_class_tier_index
74
+
75
+ # =============================================
76
+ # 1. Rebuild Method Existence
77
+ # =============================================
78
+
79
+ ## Rebuild method is generated on the class
80
+ RebuildCustomer.respond_to?(:rebuild_tier_index)
81
+ #=> true
82
+
83
+ ## Rebuild method accepts optional batch_size parameter
84
+ RebuildCustomer.method(:rebuild_tier_index).parameters.any? { |type, name| name == :batch_size }
85
+ #=> true
86
+
87
+ # =============================================
88
+ # 2. Basic Rebuild Functionality
89
+ # =============================================
90
+
91
+ ## Verify initial index state before clearing
92
+ RebuildCustomer.tier_index_for('premium').size
93
+ #=> 2
94
+
95
+ ## Clear indexes to simulate corruption or need for rebuild
96
+ %w[premium standard free].each do |tier|
97
+ RebuildCustomer.tier_index_for(tier).clear
98
+ end
99
+ RebuildCustomer.tier_index_for('premium').size
100
+ #=> 0
101
+
102
+ ## Rebuild returns count of processed objects
103
+ count = RebuildCustomer.rebuild_tier_index
104
+ count
105
+ #=> 5
106
+
107
+ ## Premium tier has correct count after rebuild
108
+ RebuildCustomer.tier_index_for('premium').size
109
+ #=> 2
110
+
111
+ ## Standard tier has correct count after rebuild
112
+ RebuildCustomer.tier_index_for('standard').size
113
+ #=> 2
114
+
115
+ ## Free tier has correct count after rebuild
116
+ RebuildCustomer.tier_index_for('free').size
117
+ #=> 1
118
+
119
+ ## Find all by tier works after rebuild
120
+ premiums = RebuildCustomer.find_all_by_tier('premium')
121
+ premiums.map(&:custid).sort
122
+ #=> ["rc_001", "rc_003"]
123
+
124
+ ## All tiers are correctly populated
125
+ standards = RebuildCustomer.find_all_by_tier('standard')
126
+ standards.map(&:custid).sort
127
+ #=> ["rc_002", "rc_005"]
128
+
129
+ ## Free tier query returns correct customer
130
+ frees = RebuildCustomer.find_all_by_tier('free')
131
+ frees.map(&:custid)
132
+ #=> ["rc_004"]
133
+
134
+ # =============================================
135
+ # 3. Rebuild with Progress Callback
136
+ # =============================================
137
+
138
+ ## Clear indexes for progress test
139
+ %w[premium standard free].each do |tier|
140
+ RebuildCustomer.tier_index_for(tier).clear
141
+ end
142
+
143
+ ## Rebuild accepts progress block and store updates in instance variable
144
+ @progress_updates = []
145
+ count = RebuildCustomer.rebuild_tier_index { |progress| @progress_updates << progress.dup }
146
+ count
147
+ #=> 5
148
+
149
+ ## Progress callback receives updates
150
+ @progress_updates.size > 0
151
+ #=> true
152
+
153
+ ## Progress updates include loading phase and store in instance variable
154
+ @loading_updates = @progress_updates.select { |p| p[:phase] == :loading }
155
+ @loading_updates.any?
156
+ #=> true
157
+
158
+ ## Progress updates include clearing phase
159
+ @clearing_updates = @progress_updates.select { |p| p[:phase] == :clearing }
160
+ @clearing_updates.any?
161
+ #=> true
162
+
163
+ ## Progress updates include rebuilding phase and store in instance variable
164
+ @rebuilding_updates = @progress_updates.select { |p| p[:phase] == :rebuilding }
165
+ @rebuilding_updates.any?
166
+ #=> true
167
+
168
+ ## Loading phase includes total count
169
+ @loading_updates.last[:total]
170
+ #=> 5
171
+
172
+ ## Rebuilding phase shows completion
173
+ @rebuilding_updates.last[:current]
174
+ #=> 5
175
+
176
+ ## Rebuilding phase total matches expected
177
+ @rebuilding_updates.last[:total]
178
+ #=> 5
179
+
180
+ # =============================================
181
+ # 4. Rebuild with Empty Instances Collection
182
+ # =============================================
183
+
184
+ ## Class with empty instances has rebuild method
185
+ RebuildCustomerNoInstances.respond_to?(:rebuild_tier_index)
186
+ #=> true
187
+
188
+ ## Class with empty instances returns 0 on rebuild (no objects to process)
189
+ result = RebuildCustomerNoInstances.rebuild_tier_index
190
+ result
191
+ #=> 0
192
+
193
+ # =============================================
194
+ # 5. Rebuild with Multiple Field Values
195
+ # =============================================
196
+
197
+ ## Clear indexes before field value update test
198
+ %w[premium standard free vip].each do |tier|
199
+ RebuildCustomer.tier_index_for(tier).clear
200
+ end
201
+
202
+ ## Update customer tier values to VIP and save
203
+ @cust1.tier = 'vip'
204
+ @cust1.save
205
+ @cust3.tier = 'vip'
206
+ @cust3.save
207
+ true
208
+ #=> true
209
+
210
+ ## Verify the tier change persisted by reloading
211
+ reloaded = RebuildCustomer.find_by_identifier(@cust1.identifier)
212
+ reloaded.tier
213
+ #=> "vip"
214
+
215
+ ## Rebuild reflects updated field values
216
+ count = RebuildCustomer.rebuild_tier_index
217
+ count
218
+ #=> 5
219
+
220
+ ## VIP tier has correct count after tier changes
221
+ RebuildCustomer.tier_index_for('vip').size
222
+ #=> 2
223
+
224
+ ## Premium tier is empty after customers moved to VIP
225
+ RebuildCustomer.tier_index_for('premium').size
226
+ #=> 0
227
+
228
+ ## Find all VIP customers works correctly
229
+ vips = RebuildCustomer.find_all_by_tier('vip')
230
+ vips.map(&:custid).sort
231
+ #=> ["rc_001", "rc_003"]
232
+
233
+ ## Other tiers remain correct after partial update
234
+ RebuildCustomer.tier_index_for('standard').size
235
+ #=> 2
236
+
237
+ ## Free tier remains correct
238
+ RebuildCustomer.tier_index_for('free').size
239
+ #=> 1
240
+
241
+ # =============================================
242
+ # 6. Rebuild with Orphaned Index Cleanup
243
+ # =============================================
244
+
245
+ ## Create orphaned index entry (stale data from a tier that no longer exists)
246
+ RebuildCustomer.tier_index_for('obsolete').add('rc_001')
247
+ RebuildCustomer.tier_index_for('obsolete').size
248
+ #=> 1
249
+
250
+ ## Rebuild cleans up orphaned indexes via SCAN
251
+ RebuildCustomer.rebuild_tier_index
252
+ RebuildCustomer.tier_index_for('obsolete').size
253
+ #=> 0
254
+
255
+ ## Valid indexes still exist after cleanup
256
+ RebuildCustomer.tier_index_for('vip').size
257
+ #=> 2
258
+
259
+ # =============================================
260
+ # 7. Rebuild with nil/empty Field Values
261
+ # =============================================
262
+
263
+ ## Create customer with nil tier
264
+ @cust_nil = RebuildCustomer.new(custid: 'rc_nil', name: 'NilTier', tier: nil)
265
+ @cust_nil.save
266
+ RebuildCustomer.instances.add(@cust_nil.identifier)
267
+ true
268
+ #=> true
269
+
270
+ ## Create customer with empty tier
271
+ @cust_empty = RebuildCustomer.new(custid: 'rc_empty', name: 'EmptyTier', tier: '')
272
+ @cust_empty.save
273
+ RebuildCustomer.instances.add(@cust_empty.identifier)
274
+ true
275
+ #=> true
276
+
277
+ ## Verify instances count is now 7
278
+ RebuildCustomer.instances.size
279
+ #=> 7
280
+
281
+ ## Clear and rebuild
282
+ %w[vip standard free].each do |tier|
283
+ RebuildCustomer.tier_index_for(tier).clear
284
+ end
285
+
286
+ ## Rebuild processes all instances (including nil/empty)
287
+ count = RebuildCustomer.rebuild_tier_index
288
+ count
289
+ #=> 7
290
+
291
+ ## Only valid tiers are indexed (nil/empty skipped)
292
+ total_indexed = RebuildCustomer.tier_index_for('vip').size +
293
+ RebuildCustomer.tier_index_for('standard').size +
294
+ RebuildCustomer.tier_index_for('free').size
295
+ total_indexed
296
+ #=> 5
297
+
298
+ ## Nil tier index is empty
299
+ RebuildCustomer.tier_index_for('').size
300
+ #=> 0
301
+
302
+ # =============================================
303
+ # 8. Rebuild with Stale Instance References
304
+ # =============================================
305
+
306
+ ## Add stale reference to instances collection
307
+ RebuildCustomer.instances.add('stale_customer_id')
308
+ RebuildCustomer.instances.size
309
+ #=> 8
310
+
311
+ ## Rebuild handles stale references gracefully (loads 7 real objects, stale ID filtered out)
312
+ count = RebuildCustomer.rebuild_tier_index
313
+ count
314
+ #=> 7
315
+
316
+ ## Index remains consistent despite stale reference (only 5 valid objects with tiers)
317
+ RebuildCustomer.tier_index_for('vip').size
318
+ #=> 2
319
+
320
+ # =============================================
321
+ # 9. Multiple Consecutive Rebuilds
322
+ # =============================================
323
+
324
+ ## First rebuild (7 loadable objects from 8 instances)
325
+ count1 = RebuildCustomer.rebuild_tier_index
326
+ count1
327
+ #=> 7
328
+
329
+ ## Second rebuild
330
+ count2 = RebuildCustomer.rebuild_tier_index
331
+ count2
332
+ #=> 7
333
+
334
+ ## Third rebuild
335
+ count3 = RebuildCustomer.rebuild_tier_index
336
+ count3
337
+ #=> 7
338
+
339
+ ## Index remains consistent after multiple rebuilds
340
+ RebuildCustomer.tier_index_for('vip').size
341
+ #=> 2
342
+
343
+ ## All expected VIP customers still findable
344
+ vips = RebuildCustomer.find_all_by_tier('vip')
345
+ vips.map(&:custid).sort
346
+ #=> ["rc_001", "rc_003"]
347
+
348
+ # =============================================
349
+ # 10. Rebuild with batch_size Parameter
350
+ # =============================================
351
+
352
+ ## Clear indexes
353
+ %w[vip standard free].each do |tier|
354
+ RebuildCustomer.tier_index_for(tier).clear
355
+ end
356
+
357
+ ## Rebuild with small batch size (7 loadable objects from 8 instances)
358
+ count = RebuildCustomer.rebuild_tier_index(batch_size: 1)
359
+ count
360
+ #=> 7
361
+
362
+ ## Index works correctly with small batch size
363
+ RebuildCustomer.tier_index_for('vip').size
364
+ #=> 2
365
+
366
+ ## Clear and rebuild with large batch size
367
+ %w[vip standard free].each do |tier|
368
+ RebuildCustomer.tier_index_for(tier).clear
369
+ end
370
+
371
+ ## Rebuild with large batch size (7 loadable objects from 8 instances)
372
+ count = RebuildCustomer.rebuild_tier_index(batch_size: 1000)
373
+ count
374
+ #=> 7
375
+
376
+ ## Index works correctly with large batch size
377
+ RebuildCustomer.find_all_by_tier('standard').map(&:custid).sort
378
+ #=> ["rc_002", "rc_005"]
379
+
380
+ # Teardown
381
+ @cust1&.delete!
382
+ @cust2&.delete!
383
+ @cust3&.delete!
384
+ @cust4&.delete!
385
+ @cust5&.delete!
386
+ @cust_nil&.delete!
387
+ @cust_empty&.delete!
388
+
389
+ # Clean up index keys
390
+ %w[premium standard free vip obsolete].each do |tier|
391
+ RebuildCustomer.dbclient.del(RebuildCustomer.tier_index_for(tier).dbkey)
392
+ end
393
+ RebuildCustomer.instances.clear