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,318 @@
1
+ # try/features/relationships/class_level_multi_index_auto_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Tests for class-level multi_index auto-indexing and helper methods
6
+ #
7
+ # This file tests:
8
+ # 1. Auto-indexing on save - objects should automatically be added to class-level indexes
9
+ # 2. Index updates when field values change
10
+ # 3. Helper methods (update_all_indexes, remove_from_all_indexes, current_indexings)
11
+ #
12
+ # Auto-indexing for class-level indexes (within: :class) should work similarly
13
+ # to unique_index (within: nil), calling add_to_class_* methods on save.
14
+
15
+ require_relative '../../support/helpers/test_helpers'
16
+
17
+ # Test class for auto-indexing with class-level multi_index
18
+ class ::AutoIndexCustomer < Familia::Horreum
19
+ feature :relationships
20
+ include Familia::Features::Relationships::Indexing
21
+
22
+ identifier_field :custid
23
+ field :custid
24
+ field :name
25
+ field :status
26
+ field :tier
27
+
28
+ # Class-level multi_index (within: :class is default)
29
+ multi_index :status, :status_index
30
+
31
+ # Explicit within: :class for second index
32
+ multi_index :tier, :tier_index, within: :class
33
+
34
+ # Track instances for rebuild testing
35
+ class_sorted_set :instances, class: self, reference: true
36
+ end
37
+
38
+ # Clean up stale data from previous runs
39
+ %w[active pending inactive premium standard basic].each do |value|
40
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.status_index_for(value).dbkey)
41
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.tier_index_for(value).dbkey)
42
+ end
43
+ # Clean up any stale customer objects
44
+ %w[auto_001 auto_002 auto_003 auto_004 auto_nil auto_empty auto_ws].each do |custid|
45
+ existing = AutoIndexCustomer.find_by_identifier(custid, check_exists: false)
46
+ existing&.delete! if existing&.exists?
47
+ end
48
+
49
+ # =============================================
50
+ # 1. Auto-Indexing on Save Tests
51
+ # =============================================
52
+
53
+ ## New object saved - auto-indexed in status_index
54
+ @auto1 = AutoIndexCustomer.new(custid: 'auto_001', name: 'AutoAlice', status: 'active', tier: 'premium')
55
+ @auto1.save
56
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_001')
57
+ #=> true
58
+
59
+ ## Auto-indexing works for tier_index too (explicit within: :class)
60
+ AutoIndexCustomer.tier_index_for('premium').members.include?('auto_001')
61
+ #=> true
62
+
63
+ ## Second object saved - joins same status index
64
+ @auto2 = AutoIndexCustomer.new(custid: 'auto_002', name: 'AutoBob', status: 'active', tier: 'standard')
65
+ @auto2.save
66
+ AutoIndexCustomer.status_index_for('active').members.sort
67
+ #=> ["auto_001", "auto_002"]
68
+
69
+ ## Different tier index is separate
70
+ AutoIndexCustomer.tier_index_for('standard').members
71
+ #=> ["auto_002"]
72
+
73
+ ## Object with different status creates new index entry
74
+ @auto3 = AutoIndexCustomer.new(custid: 'auto_003', name: 'AutoCharlie', status: 'pending', tier: 'basic')
75
+ @auto3.save
76
+ AutoIndexCustomer.status_index_for('pending').members
77
+ #=> ["auto_003"]
78
+
79
+ ## Active status index unchanged by new pending customer
80
+ AutoIndexCustomer.status_index_for('active').size
81
+ #=> 2
82
+
83
+ # =============================================
84
+ # 2. Query After Auto-Indexing
85
+ # =============================================
86
+
87
+ ## find_all_by_status returns auto-indexed customers
88
+ active_customers = AutoIndexCustomer.find_all_by_status('active')
89
+ active_customers.map(&:custid).sort
90
+ #=> ["auto_001", "auto_002"]
91
+
92
+ ## sample_from_status works with auto-indexed data
93
+ sample = AutoIndexCustomer.sample_from_status('active', 1)
94
+ ['auto_001', 'auto_002'].include?(sample.first&.custid)
95
+ #=> true
96
+
97
+ ## find_all_by_tier returns correct customers
98
+ premium_customers = AutoIndexCustomer.find_all_by_tier('premium')
99
+ premium_customers.map(&:custid)
100
+ #=> ["auto_001"]
101
+
102
+ # =============================================
103
+ # 3. Field Change and Index Update
104
+ # =============================================
105
+
106
+ ## Change status field and save - verify old index is NOT automatically updated
107
+ # Note: auto_update_class_indexes calls add_to_class_*, which is idempotent
108
+ # but does NOT remove from old index. Manual update is needed for field changes.
109
+ @old_status = @auto1.status
110
+ @auto1.status = 'inactive'
111
+ @auto1.save
112
+
113
+ # After save, customer is added to new index
114
+ AutoIndexCustomer.status_index_for('inactive').members.include?('auto_001')
115
+ #=> true
116
+
117
+ ## Old index still contains the entry (save doesn't auto-remove from old)
118
+ # This is expected behavior - save is idempotent add, not update
119
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_001')
120
+ #=> true
121
+
122
+ ## Use update_in_class_status_index to properly move between indexes
123
+ @auto1.update_in_class_status_index(@old_status)
124
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_001')
125
+ #=> false
126
+
127
+ ## Customer now only in inactive index
128
+ AutoIndexCustomer.status_index_for('inactive').members
129
+ #=> ["auto_001"]
130
+
131
+ # =============================================
132
+ # 4. Helper Methods - update_all_indexes
133
+ # =============================================
134
+
135
+ ## update_all_indexes method exists
136
+ @auto2.respond_to?(:update_all_indexes)
137
+ #=> true
138
+
139
+ ## update_all_indexes updates both status and tier indexes
140
+ @old_values = { status: @auto2.status, tier: @auto2.tier }
141
+ @auto2.status = 'pending'
142
+ @auto2.tier = 'premium'
143
+ @auto2.update_all_indexes(@old_values)
144
+
145
+ # Check status index updated
146
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_002')
147
+ #=> false
148
+
149
+ ## New status index contains customer
150
+ AutoIndexCustomer.status_index_for('pending').members.include?('auto_002')
151
+ #=> true
152
+
153
+ ## Old tier index no longer contains customer
154
+ AutoIndexCustomer.tier_index_for('standard').members.include?('auto_002')
155
+ #=> false
156
+
157
+ ## New tier index contains customer
158
+ AutoIndexCustomer.tier_index_for('premium').members.include?('auto_002')
159
+ #=> true
160
+
161
+ # =============================================
162
+ # 5. Helper Methods - remove_from_all_indexes
163
+ # =============================================
164
+
165
+ ## remove_from_all_indexes method exists
166
+ @auto3.respond_to?(:remove_from_all_indexes)
167
+ #=> true
168
+
169
+ ## Verify customer is in indexes before removal
170
+ [
171
+ AutoIndexCustomer.status_index_for('pending').members.include?('auto_003'),
172
+ AutoIndexCustomer.tier_index_for('basic').members.include?('auto_003')
173
+ ]
174
+ #=> [true, true]
175
+
176
+ ## remove_from_all_indexes removes from all class-level indexes
177
+ @auto3.remove_from_all_indexes
178
+ [
179
+ AutoIndexCustomer.status_index_for('pending').members.include?('auto_003'),
180
+ AutoIndexCustomer.tier_index_for('basic').members.include?('auto_003')
181
+ ]
182
+ #=> [false, false]
183
+
184
+ # =============================================
185
+ # 6. Helper Methods - current_indexings
186
+ # =============================================
187
+
188
+ ## current_indexings method exists
189
+ @auto1.respond_to?(:current_indexings)
190
+ #=> true
191
+
192
+ ## Re-add auto1 to indexes for testing current_indexings
193
+ @auto1.add_to_class_status_index
194
+ @auto1.add_to_class_tier_index
195
+ @indexings = @auto1.current_indexings
196
+ @indexings.length
197
+ #=> 2
198
+
199
+ ## current_indexings returns correct info for status_index
200
+ @status_indexing = @indexings.find { |i| i[:index_name] == :status_index }
201
+ [@status_indexing[:field], @status_indexing[:cardinality], @status_indexing[:type]]
202
+ #=> [:status, :multi, "multi_index"]
203
+
204
+ ## current_indexings returns correct info for tier_index
205
+ @tier_indexing = @indexings.find { |i| i[:index_name] == :tier_index }
206
+ [@tier_indexing[:field], @tier_indexing[:cardinality], @tier_indexing[:type]]
207
+ #=> [:tier, :multi, "multi_index"]
208
+
209
+ ## current_indexings shows scope_class for class-level indexes
210
+ @status_indexing[:scope_class]
211
+ #=> "class"
212
+
213
+ # =============================================
214
+ # 7. Helper Methods - indexed_in?
215
+ # =============================================
216
+
217
+ ## indexed_in? method exists
218
+ @auto1.respond_to?(:indexed_in?)
219
+ #=> true
220
+
221
+ ## indexed_in? returns true for indexes where customer is present
222
+ @auto1.indexed_in?(:status_index)
223
+ #=> true
224
+
225
+ ## indexed_in? returns true for tier_index too
226
+ @auto1.indexed_in?(:tier_index)
227
+ #=> true
228
+
229
+ ## indexed_in? returns false for non-existent index
230
+ @auto1.indexed_in?(:nonexistent_index)
231
+ #=> false
232
+
233
+ ## After removal, indexed_in? returns false
234
+ @auto1.remove_from_class_status_index
235
+ @auto1.indexed_in?(:status_index)
236
+ #=> false
237
+
238
+ # =============================================
239
+ # 8. Delete and Index Cleanup
240
+ # =============================================
241
+
242
+ ## Create a new customer for delete testing
243
+ @auto4 = AutoIndexCustomer.new(custid: 'auto_004', name: 'AutoDiana', status: 'active', tier: 'standard')
244
+ @auto4.save
245
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_004')
246
+ #=> true
247
+
248
+ ## Delete does NOT automatically remove from indexes (by design)
249
+ # Applications should call remove_from_all_indexes before delete if needed
250
+ @auto4.delete!
251
+ AutoIndexCustomer.status_index_for('active').members.include?('auto_004')
252
+ #=> true
253
+
254
+ ## Manual cleanup required after delete
255
+ # Note: This tests that stale entries exist until explicitly cleaned
256
+ # The find_by methods handle stale entries gracefully
257
+ stale_customer = AutoIndexCustomer.find_by_identifier('auto_004')
258
+ stale_customer
259
+ #=> nil
260
+
261
+ # =============================================
262
+ # 9. Edge Cases
263
+ # =============================================
264
+
265
+ ## Nil field value - should not be indexed
266
+ @auto_nil = AutoIndexCustomer.new(custid: 'auto_nil', name: 'NilStatus', status: nil, tier: 'basic')
267
+ @auto_nil.save
268
+ # Should not create an entry in the nil index (empty string key)
269
+ AutoIndexCustomer.status_index_for('').members.include?('auto_nil')
270
+ #=> false
271
+
272
+ ## Empty string field value - should not be indexed
273
+ @auto_empty = AutoIndexCustomer.new(custid: 'auto_empty', name: 'EmptyStatus', status: '', tier: 'basic')
274
+ @auto_empty.save
275
+ AutoIndexCustomer.status_index_for('').members.include?('auto_empty')
276
+ #=> false
277
+
278
+ ## Whitespace-only field value - should not be indexed
279
+ @auto_ws = AutoIndexCustomer.new(custid: 'auto_ws', name: 'WSStatus', status: ' ', tier: 'basic')
280
+ @auto_ws.save
281
+ AutoIndexCustomer.status_index_for(' ').members.include?('auto_ws')
282
+ #=> false
283
+
284
+ # =============================================
285
+ # 10. Idempotent Save Behavior
286
+ # =============================================
287
+
288
+ ## Saving same object multiple times doesn't duplicate index entries
289
+ @auto2.save
290
+ @auto2.save
291
+ @auto2.save
292
+ # Should still only have one entry
293
+ AutoIndexCustomer.status_index_for(@auto2.status).members.count { |m| m == 'auto_002' }
294
+ #=> 1
295
+
296
+ ## UnsortedSet inherently prevents duplicates
297
+ AutoIndexCustomer.status_index_for('pending').size
298
+ #=> 1
299
+
300
+ # Teardown
301
+ # Clean up test objects
302
+ [@auto1, @auto2, @auto3, @auto_nil, @auto_empty, @auto_ws].compact.each do |obj|
303
+ obj.delete! if obj.respond_to?(:exists?) && obj.exists?
304
+ end
305
+
306
+ # Clean up index keys
307
+ %w[active pending inactive premium standard basic].each do |value|
308
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.status_index_for(value).dbkey)
309
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.tier_index_for(value).dbkey)
310
+ end
311
+
312
+ # Clean up edge case indexes
313
+ ['', ' '].each do |value|
314
+ AutoIndexCustomer.dbclient.del(AutoIndexCustomer.status_index_for(value).dbkey)
315
+ end
316
+
317
+ # Clean up instances collection
318
+ AutoIndexCustomer.instances.clear if AutoIndexCustomer.respond_to?(:instances)
@@ -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