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.
- checksums.yaml +4 -4
- data/CHANGELOG.rst +71 -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/management.rb +36 -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/edge_cases/find_by_dbkey_race_condition_try.rb +248 -0
- 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 +7 -1
|
@@ -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
|