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.
- checksums.yaml +4 -4
- data/CHANGELOG.rst +37 -0
- data/CLAUDE.md +1 -1
- data/Gemfile.lock +1 -1
- data/docs/guides/feature-relationships-indexing.md +104 -9
- data/docs/guides/feature-relationships-methods.md +37 -5
- data/docs/overview.md +9 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/data_type/serialization.rb +8 -9
- data/lib/familia/data_type/settings.rb +0 -8
- data/lib/familia/data_type/types/json_stringkey.rb +155 -0
- data/lib/familia/data_type.rb +5 -4
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +281 -15
- data/lib/familia/features/relationships/indexing.rb +57 -27
- data/lib/familia/features/safe_dump.rb +0 -3
- data/lib/familia/horreum/persistence.rb +4 -1
- data/lib/familia/horreum/settings.rb +2 -10
- data/lib/familia/horreum.rb +1 -2
- data/lib/familia/version.rb +1 -1
- data/try/features/relationships/class_level_multi_index_auto_try.rb +318 -0
- data/try/features/relationships/class_level_multi_index_rebuild_try.rb +393 -0
- data/try/features/relationships/class_level_multi_index_try.rb +349 -0
- data/try/integration/familia_extended_try.rb +1 -1
- data/try/integration/scenarios_try.rb +4 -3
- data/try/unit/data_types/json_stringkey_try.rb +431 -0
- data/try/unit/horreum/settings_try.rb +0 -11
- metadata +6 -1
|
@@ -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
|