familia 2.0.0.pre21 → 2.0.0.pre22

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.talismanrc +5 -1
  3. data/CHANGELOG.rst +43 -0
  4. data/Gemfile.lock +1 -1
  5. data/lib/familia/connection/operation_core.rb +1 -2
  6. data/lib/familia/connection/pipelined_core.rb +1 -3
  7. data/lib/familia/connection/transaction_core.rb +1 -2
  8. data/lib/familia/data_type/serialization.rb +76 -51
  9. data/lib/familia/data_type/types/sorted_set.rb +5 -10
  10. data/lib/familia/data_type/types/stringkey.rb +22 -0
  11. data/lib/familia/features/external_identifier.rb +29 -0
  12. data/lib/familia/features/object_identifier.rb +47 -0
  13. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +15 -15
  14. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +8 -0
  15. data/lib/familia/horreum/database_commands.rb +6 -1
  16. data/lib/familia/horreum/management.rb +141 -10
  17. data/lib/familia/horreum/persistence.rb +3 -0
  18. data/lib/familia/identifier_extractor.rb +1 -1
  19. data/lib/familia/version.rb +1 -1
  20. data/lib/multi_result.rb +59 -31
  21. data/try/features/count_any_edge_cases_try.rb +486 -0
  22. data/try/features/count_any_methods_try.rb +197 -0
  23. data/try/features/external_identifier/external_identifier_try.rb +134 -0
  24. data/try/features/object_identifier/object_identifier_try.rb +138 -0
  25. data/try/features/relationships/indexing_rebuild_try.rb +6 -0
  26. data/try/integration/data_types/datatype_pipelines_try.rb +5 -3
  27. data/try/integration/data_types/datatype_transactions_try.rb +13 -7
  28. data/try/integration/models/customer_try.rb +3 -3
  29. data/try/unit/data_types/boolean_try.rb +35 -22
  30. data/try/unit/data_types/hash_try.rb +2 -2
  31. data/try/unit/data_types/serialization_try.rb +386 -0
  32. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +2 -1
  33. metadata +4 -7
  34. data/changelog.d/20251105_flexible_external_identifier_format.rst +0 -66
  35. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +0 -44
  36. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +0 -20
  37. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +0 -91
  38. data/changelog.d/20251107_optimized_redis_exists_checks.rst +0 -94
  39. data/changelog.d/20251108_frozen_string_literal_pragma.rst +0 -44
@@ -0,0 +1,486 @@
1
+ require_relative '../support/helpers/test_helpers'
2
+
3
+ # Test class for count/any edge case testing - synchronization issues
4
+ class EdgeCaseCustomer < Familia::Horreum
5
+ identifier_field :custid
6
+ field :custid
7
+ field :name
8
+ end
9
+
10
+ # Setup - clear any existing data
11
+ EdgeCaseCustomer.instances.clear
12
+ # Clear all keys matching the pattern
13
+ EdgeCaseCustomer.dbclient.scan_each(match: EdgeCaseCustomer.dbkey('*')) do |key|
14
+ EdgeCaseCustomer.dbclient.del(key)
15
+ end
16
+
17
+ # =============================================================================
18
+ # Edge Case 1: Phantom Instances (stale additions)
19
+ # Identifier exists in instances sorted set but object key doesn't exist
20
+ # =============================================================================
21
+
22
+ ## EDGE: Phantom instance - count shows stale entry
23
+ # Manually add identifier to instances without creating object
24
+ EdgeCaseCustomer.instances.add('phantom1', Time.now.to_i)
25
+ EdgeCaseCustomer.count
26
+ #=> 1
27
+
28
+ ## EDGE: Phantom instance - keys_count shows authoritative count (0)
29
+ EdgeCaseCustomer.keys_count
30
+ #=> 0
31
+
32
+ ## EDGE: Phantom instance - scan_count shows authoritative count (0)
33
+ EdgeCaseCustomer.scan_count
34
+ #=> 0
35
+
36
+ ## EDGE: Phantom instance - count! shows authoritative count (0)
37
+ EdgeCaseCustomer.count!
38
+ #=> 0
39
+
40
+ ## EDGE: Phantom instance - any? returns true (stale)
41
+ EdgeCaseCustomer.any?
42
+ #=> true
43
+
44
+ ## EDGE: Phantom instance - keys_any? returns false (authoritative)
45
+ EdgeCaseCustomer.keys_any?
46
+ #=> false
47
+
48
+ ## EDGE: Phantom instance - scan_any? returns false (authoritative)
49
+ EdgeCaseCustomer.scan_any?
50
+ #=> false
51
+
52
+ ## EDGE: Phantom instance - any! returns false (authoritative)
53
+ EdgeCaseCustomer.any!
54
+ #=> false
55
+
56
+ ## EDGE: Phantom instance cleanup - clear instances
57
+ EdgeCaseCustomer.instances.clear
58
+ #=*> Integer
59
+
60
+ # =============================================================================
61
+ # Edge Case 2: Orphaned Objects
62
+ # Object exists in Redis but identifier NOT in instances sorted set
63
+ # =============================================================================
64
+
65
+ ## EDGE: Orphaned object - create and remove from instances manually
66
+ @orphan = EdgeCaseCustomer.create!(custid: 'orphan1', name: 'Orphan')
67
+ EdgeCaseCustomer.instances.remove('orphan1')
68
+ # After removal, count should be 0 but object still exists
69
+ EdgeCaseCustomer.count
70
+ #=> 0
71
+
72
+ ## EDGE: Orphaned object - keys_count finds the orphan
73
+ EdgeCaseCustomer.keys_count
74
+ #=> 1
75
+
76
+ ## EDGE: Orphaned object - scan_count finds the orphan
77
+ EdgeCaseCustomer.scan_count
78
+ #=> 1
79
+
80
+ ## EDGE: Orphaned object - count! finds the orphan
81
+ EdgeCaseCustomer.count!
82
+ #=> 1
83
+
84
+ ## EDGE: Orphaned object - any? returns false (no instances entry)
85
+ # Since instances is empty, any? should return false
86
+ EdgeCaseCustomer.any?
87
+ #=> false
88
+
89
+ ## EDGE: Orphaned object - keys_any? returns true (object exists)
90
+ EdgeCaseCustomer.keys_any?
91
+ #=> true
92
+
93
+ ## EDGE: Orphaned object - scan_any? returns true (object exists)
94
+ EdgeCaseCustomer.scan_any?
95
+ #=> true
96
+
97
+ ## EDGE: Orphaned object - any! returns true (object exists)
98
+ EdgeCaseCustomer.any!
99
+ #=> true
100
+
101
+ ## EDGE: Orphaned object cleanup - destroy and clear
102
+ @orphan.destroy!
103
+ EdgeCaseCustomer.instances.clear
104
+ EdgeCaseCustomer.all.each(&:destroy!)
105
+ #=*> Array
106
+
107
+ # =============================================================================
108
+ # Edge Case 3: Duplicate Instance Entries
109
+ # Same identifier added multiple times to instances (shouldn't happen but test it)
110
+ # =============================================================================
111
+
112
+ ## EDGE: Duplicate entries - create fresh customer
113
+ @dup = EdgeCaseCustomer.create!(custid: 'dup1', name: 'Duplicate')
114
+ # Manually add the same identifier again with different score
115
+ # ZADD should just update the score, not create duplicate
116
+ EdgeCaseCustomer.instances.add('dup1', Time.now.to_i + 1000)
117
+ EdgeCaseCustomer.count
118
+ #=> 1
119
+
120
+ ## EDGE: Duplicate entries - keys_count also shows 1
121
+ EdgeCaseCustomer.keys_count
122
+ #=> 1
123
+
124
+ ## EDGE: Duplicate entries - scan_count also shows 1
125
+ EdgeCaseCustomer.scan_count
126
+ #=> 1
127
+
128
+ ## EDGE: Duplicate entries cleanup - destroy and clear
129
+ @dup.destroy!
130
+ EdgeCaseCustomer.instances.clear
131
+ EdgeCaseCustomer.all.each(&:destroy!)
132
+ #=*> Array
133
+
134
+ # =============================================================================
135
+ # Edge Case 4: Empty Identifier
136
+ # What happens with empty string identifier?
137
+ # =============================================================================
138
+
139
+ ## EDGE: Empty identifier - instances can store empty string
140
+ EdgeCaseCustomer.instances.add('', Time.now.to_i)
141
+ EdgeCaseCustomer.count
142
+ #=> 1
143
+
144
+ ## EDGE: Empty identifier - keys_count returns 0 (no matching keys)
145
+ # Empty identifier doesn't create valid keys
146
+ EdgeCaseCustomer.keys_count
147
+ #=> 0
148
+
149
+ ## EDGE: Empty identifier - scan_count returns 0 (no matching keys)
150
+ EdgeCaseCustomer.scan_count
151
+ #=> 0
152
+
153
+ ## EDGE: Empty identifier cleanup - clear all
154
+ EdgeCaseCustomer.instances.clear
155
+ EdgeCaseCustomer.all.each(&:destroy!)
156
+ #=*> Array
157
+
158
+ # =============================================================================
159
+ # Edge Case 5: Mixed State - Multiple Desync Scenarios
160
+ # Combination of phantoms, orphans, and valid objects
161
+ # =============================================================================
162
+
163
+ ## EDGE: Mixed state - setup complex scenario
164
+ @valid1 = EdgeCaseCustomer.create!(custid: 'valid1', name: 'Valid 1')
165
+ @valid2 = EdgeCaseCustomer.create!(custid: 'valid2', name: 'Valid 2')
166
+ # Create phantom (in instances but no object)
167
+ EdgeCaseCustomer.instances.add('phantom2', Time.now.to_i)
168
+ # Create orphan (object but not in instances)
169
+ @orphan2 = EdgeCaseCustomer.create!(custid: 'orphan2', name: 'Orphan 2')
170
+ EdgeCaseCustomer.instances.remove('orphan2')
171
+ # instances has: valid1, valid2, phantom2 = 3
172
+ EdgeCaseCustomer.count
173
+ #=> 3
174
+
175
+ ## EDGE: Mixed state - keys_count shows authoritative count
176
+ # Actual objects exist: valid1, valid2, orphan2 = 3
177
+ EdgeCaseCustomer.keys_count
178
+ #=> 3
179
+
180
+ ## EDGE: Mixed state - scan_count shows authoritative count
181
+ EdgeCaseCustomer.scan_count
182
+ #=> 3
183
+
184
+ ## EDGE: Mixed state - count! shows authoritative count
185
+ EdgeCaseCustomer.count!
186
+ #=> 3
187
+
188
+ ## EDGE: Mixed state - both counts match in this case
189
+ # By coincidence, both are 3 (different reasons though)
190
+ EdgeCaseCustomer.keys_count == EdgeCaseCustomer.count
191
+ #=> true
192
+
193
+ ## EDGE: Mixed state cleanup - clear all
194
+ EdgeCaseCustomer.instances.clear
195
+ EdgeCaseCustomer.all.each(&:destroy!)
196
+ #=*> Array
197
+
198
+ # =============================================================================
199
+ # Edge Case 6: Special Characters in Identifiers
200
+ # Unicode, special chars, patterns that might break filters
201
+ # =============================================================================
202
+
203
+ ## EDGE: Special chars - asterisk in identifier
204
+ @special1 = EdgeCaseCustomer.create!(custid: 'user*123', name: 'Special')
205
+ EdgeCaseCustomer.count
206
+ #=> 1
207
+
208
+ ## EDGE: Special chars - keys_count finds it
209
+ EdgeCaseCustomer.keys_count
210
+ #=> 1
211
+
212
+ ## EDGE: Special chars - scan_count finds it
213
+ EdgeCaseCustomer.scan_count
214
+ #=> 1
215
+
216
+ ## EDGE: Special chars - filter with asterisk pattern
217
+ # This tests if the literal asterisk in custid affects pattern matching
218
+ EdgeCaseCustomer.keys_count('user*')
219
+ #=> 1
220
+
221
+ ## EDGE: Special chars - scan filter with asterisk pattern
222
+ EdgeCaseCustomer.scan_count('user*')
223
+ #=> 1
224
+
225
+ ## EDGE: Special chars cleanup - destroy special1
226
+ @special1.destroy!
227
+ #=*> Integer
228
+
229
+ ## EDGE: Special chars - question mark in identifier
230
+ @special2 = EdgeCaseCustomer.create!(custid: 'user?456', name: 'Special2')
231
+ EdgeCaseCustomer.keys_count('user?*')
232
+ #=> 1
233
+
234
+ ## EDGE: Special chars - scan filter with question mark pattern
235
+ EdgeCaseCustomer.scan_count('user?*')
236
+ #=> 1
237
+
238
+ ## EDGE: Special chars cleanup - destroy special2
239
+ @special2.destroy!
240
+ #=*> Integer
241
+
242
+ ## EDGE: Special chars - brackets in identifier
243
+ @special3 = EdgeCaseCustomer.create!(custid: 'user[test]', name: 'Special3')
244
+ EdgeCaseCustomer.keys_count('user*')
245
+ #=> 1
246
+
247
+ ## EDGE: Special chars - scan finds bracketed identifier
248
+ EdgeCaseCustomer.scan_count('user*')
249
+ #=> 1
250
+
251
+ ## EDGE: Special chars cleanup - destroy special3 and clear
252
+ @special3.destroy!
253
+ EdgeCaseCustomer.instances.clear
254
+ #=*> Integer
255
+
256
+ # =============================================================================
257
+ # Edge Case 7: Score Manipulation
258
+ # Instances sorted set uses scores (timestamps), test score edge cases
259
+ # =============================================================================
260
+
261
+ ## EDGE: Score manipulation - zero score
262
+ EdgeCaseCustomer.instances.add('score_test1', 0)
263
+ EdgeCaseCustomer.count
264
+ #=> 1
265
+
266
+ ## EDGE: Score manipulation - negative score
267
+ EdgeCaseCustomer.instances.add('score_test2', -12345)
268
+ EdgeCaseCustomer.count
269
+ #=> 2
270
+
271
+ ## EDGE: Score manipulation - very large score
272
+ EdgeCaseCustomer.instances.add('score_test3', 9999999999999)
273
+ EdgeCaseCustomer.count
274
+ #=> 3
275
+
276
+ ## EDGE: Score manipulation - scores don't affect count
277
+ # Regardless of score values, count should be accurate
278
+ EdgeCaseCustomer.count
279
+ #=> 3
280
+
281
+ ## EDGE: Score manipulation - keys_count ignores scores (checks actual keys)
282
+ EdgeCaseCustomer.keys_count
283
+ #=> 0
284
+
285
+ ## EDGE: Score manipulation - scan_count ignores scores
286
+ EdgeCaseCustomer.scan_count
287
+ #=> 0
288
+
289
+ ## EDGE: Score manipulation cleanup - clear instances
290
+ EdgeCaseCustomer.instances.clear
291
+ #=*> Integer
292
+
293
+ # =============================================================================
294
+ # Edge Case 8: Large Dataset - Scan Cursor Behavior
295
+ # Test that scan_count properly iterates through large datasets
296
+ # =============================================================================
297
+
298
+ ## EDGE: Large dataset - create 100 instances
299
+ 100.times do |i|
300
+ EdgeCaseCustomer.create!(custid: "large#{i}", name: "Large #{i}")
301
+ end
302
+ EdgeCaseCustomer.count
303
+ #=> 100
304
+
305
+ ## EDGE: Large dataset - keys_count matches (blocks but works)
306
+ EdgeCaseCustomer.keys_count
307
+ #=> 100
308
+
309
+ ## EDGE: Large dataset - scan_count matches (non-blocking, cursor iteration)
310
+ EdgeCaseCustomer.scan_count
311
+ #=> 100
312
+
313
+ ## EDGE: Large dataset - count! matches
314
+ EdgeCaseCustomer.count!
315
+ #=> 100
316
+
317
+ ## EDGE: Large dataset - scan_any? returns true efficiently
318
+ EdgeCaseCustomer.scan_any?
319
+ #=> true
320
+
321
+ ## EDGE: Large dataset - keys_any? returns true
322
+ EdgeCaseCustomer.keys_any?
323
+ #=> true
324
+
325
+ ## EDGE: Large dataset - filter scan works with many results
326
+ EdgeCaseCustomer.scan_count('large1*')
327
+ #=> 11
328
+
329
+ ## EDGE: Large dataset - filter keys works with many results
330
+ EdgeCaseCustomer.keys_count('large1*')
331
+ #=> 11
332
+
333
+ ## EDGE: Large dataset cleanup - clear all
334
+ EdgeCaseCustomer.instances.clear
335
+ EdgeCaseCustomer.all.each(&:destroy!)
336
+ #=*> Array
337
+
338
+ # =============================================================================
339
+ # Edge Case 9: Manual Redis Operations Breaking Consistency
340
+ # Direct Redis commands that bypass Familia's tracking
341
+ # =============================================================================
342
+
343
+ ## EDGE: Manual ops - RENAME breaks dbkey pattern
344
+ @manual1 = EdgeCaseCustomer.create!(custid: 'manual1', name: 'Manual')
345
+ original_key = @manual1.dbkey
346
+ EdgeCaseCustomer.dbclient.rename(original_key, 'some:random:key')
347
+ EdgeCaseCustomer.count
348
+ #=> 1
349
+
350
+ ## EDGE: Manual ops - keys_count doesn't find renamed key
351
+ EdgeCaseCustomer.keys_count
352
+ #=> 0
353
+
354
+ ## EDGE: Manual ops - scan_count doesn't find renamed key
355
+ EdgeCaseCustomer.scan_count
356
+ #=> 0
357
+
358
+ ## EDGE: Manual ops - instances still has stale entry
359
+ EdgeCaseCustomer.any?
360
+ #=> true
361
+
362
+ ## EDGE: Manual ops - but keys_any? correctly returns false
363
+ EdgeCaseCustomer.keys_any?
364
+ #=> false
365
+
366
+ ## EDGE: Manual ops - and scan_any? correctly returns false
367
+ EdgeCaseCustomer.scan_any?
368
+ #=> false
369
+
370
+ ## EDGE: Manual ops cleanup - delete renamed key and clear instances
371
+ EdgeCaseCustomer.dbclient.del('some:random:key')
372
+ EdgeCaseCustomer.instances.clear
373
+ #=*> Integer
374
+
375
+ # =============================================================================
376
+ # Edge Case 10: Partial Transaction Failures
377
+ # Simulate scenarios where object exists but instances tracking failed
378
+ # =============================================================================
379
+
380
+ ## EDGE: Partial failure - manually create object without instances entry
381
+ @direct_key = EdgeCaseCustomer.new(custid: 'partial1', name: 'Partial').dbkey
382
+ EdgeCaseCustomer.dbclient.hset(@direct_key, 'custid', 'partial1')
383
+ EdgeCaseCustomer.dbclient.hset(@direct_key, 'name', 'Partial')
384
+ EdgeCaseCustomer.count
385
+ #=> 0
386
+
387
+ ## EDGE: Partial failure - keys_count finds the manually created object
388
+ EdgeCaseCustomer.keys_count
389
+ #=> 1
390
+
391
+ ## EDGE: Partial failure - scan_count finds it too
392
+ EdgeCaseCustomer.scan_count
393
+ #=> 1
394
+
395
+ ## EDGE: Partial failure - count! finds it
396
+ EdgeCaseCustomer.count!
397
+ #=> 1
398
+
399
+ ## EDGE: Partial failure - any? returns false (not in instances)
400
+ EdgeCaseCustomer.any?
401
+ #=> false
402
+
403
+ ## EDGE: Partial failure - keys_any? returns true (key exists)
404
+ EdgeCaseCustomer.keys_any?
405
+ #=> true
406
+
407
+ ## EDGE: Partial failure - scan_any? returns true (key exists)
408
+ EdgeCaseCustomer.scan_any?
409
+ #=> true
410
+
411
+ ## EDGE: Partial failure cleanup - delete direct key and clear
412
+ EdgeCaseCustomer.dbclient.del(@direct_key)
413
+ EdgeCaseCustomer.instances.clear
414
+ #=*> Integer
415
+
416
+ # =============================================================================
417
+ # Edge Case 11: Filter Pattern Edge Cases
418
+ # Test filter behavior with complex patterns and edge cases
419
+ # =============================================================================
420
+
421
+ ## EDGE: Filter patterns - create first filter test object
422
+ @filter1 = EdgeCaseCustomer.create!(custid: 'filter1', name: 'Filter1')
423
+ @filter1.custid
424
+ #=> 'filter1'
425
+
426
+ ## EDGE: Filter patterns - single character wildcard
427
+ EdgeCaseCustomer.keys_count('?')
428
+ #=> 0
429
+
430
+ ## EDGE: Filter patterns - create second filter test object
431
+ @filter2 = EdgeCaseCustomer.create!(custid: 'filter2', name: 'Filter2')
432
+ @filter2.custid
433
+ #=> 'filter2'
434
+
435
+ ## EDGE: Filter patterns - multiple wildcards count
436
+ EdgeCaseCustomer.keys_count('*')
437
+ #=*> Integer
438
+
439
+ ## EDGE: Filter patterns - scan with wildcard matches all
440
+ EdgeCaseCustomer.scan_count('*')
441
+ #=*> Integer
442
+
443
+ ## EDGE: Filter patterns - range pattern
444
+ EdgeCaseCustomer.keys_count('filter[12]')
445
+ #=> 2
446
+
447
+ ## EDGE: Filter patterns - scan with range pattern
448
+ EdgeCaseCustomer.scan_count('filter[12]')
449
+ #=> 2
450
+
451
+ ## EDGE: Empty identifier cleanup - clear all
452
+ EdgeCaseCustomer.instances.clear
453
+ EdgeCaseCustomer.all.each(&:destroy!)
454
+ #=*> Array
455
+
456
+ # =============================================================================
457
+ # Edge Case 12: Boundary Conditions
458
+ # Test edge cases around zero, one, and boundary values
459
+ # =============================================================================
460
+
461
+ ## EDGE: Boundary - scan_any? short-circuits on first match
462
+ # Create one object and verify scan_any? doesn't iterate unnecessarily
463
+ @boundary1 = EdgeCaseCustomer.create!(custid: 'boundary1', name: 'Boundary')
464
+ EdgeCaseCustomer.scan_any?
465
+ #=> true
466
+
467
+ ## EDGE: Boundary - scan_any? with filter short-circuits
468
+ EdgeCaseCustomer.scan_any?('bound*')
469
+ #=> true
470
+
471
+ ## EDGE: Boundary - scan_any? returns false when no matches
472
+ EdgeCaseCustomer.scan_any?('nonexistent*')
473
+ #=> false
474
+
475
+ ## EDGE: Boundary - keys_any? with non-matching filter
476
+ EdgeCaseCustomer.keys_any?('nonexistent*')
477
+ #=> false
478
+
479
+ ## EDGE: Boundary cleanup - destroy and clear
480
+ @boundary1.destroy!
481
+ EdgeCaseCustomer.instances.clear
482
+ #=*> Integer
483
+
484
+ ## Final cleanup - clear instances
485
+ EdgeCaseCustomer.instances.clear
486
+ #=*> Integer
@@ -0,0 +1,197 @@
1
+ require_relative '../support/helpers/test_helpers'
2
+
3
+ # Test class for count/any method testing
4
+ class CountTestCustomer < Familia::Horreum
5
+ identifier_field :custid
6
+ field :custid
7
+ field :name
8
+ end
9
+
10
+ # Setup - clear any existing data
11
+ CountTestCustomer.instances.clear
12
+ CountTestCustomer.all.each(&:destroy!)
13
+
14
+ ## count returns 0 when no instances exist
15
+ CountTestCustomer.count
16
+ #=> 0
17
+
18
+ ## keys_count returns 0 when no instances exist (KEYS command)
19
+ CountTestCustomer.keys_count
20
+ #=> 0
21
+
22
+ ## scan_count returns 0 when no instances exist (SCAN command)
23
+ CountTestCustomer.scan_count
24
+ #=> 0
25
+
26
+ ## count! returns 0 when no instances exist (alias to scan_count)
27
+ CountTestCustomer.count!
28
+ #=> 0
29
+
30
+ ## any? returns false when no instances exist
31
+ CountTestCustomer.any?
32
+ #=> false
33
+
34
+ ## keys_any? returns false when no instances exist (KEYS command)
35
+ CountTestCustomer.keys_any?
36
+ #=> false
37
+
38
+ ## scan_any? returns false when no instances exist (SCAN command)
39
+ CountTestCustomer.scan_any?
40
+ #=> false
41
+
42
+ ## any! returns false when no instances exist (alias to scan_any?)
43
+ CountTestCustomer.any!
44
+ #=> false
45
+
46
+ ## count returns 1 after creating an instance
47
+ @cust1 = CountTestCustomer.create!(custid: 'alice', name: 'Alice')
48
+ CountTestCustomer.count
49
+ #=> 1
50
+
51
+ ## keys_count returns 1 after creating an instance
52
+ CountTestCustomer.keys_count
53
+ #=> 1
54
+
55
+ ## scan_count returns 1 after creating an instance
56
+ CountTestCustomer.scan_count
57
+ #=> 1
58
+
59
+ ## count! returns 1 after creating an instance
60
+ CountTestCustomer.count!
61
+ #=> 1
62
+
63
+ ## any? returns true after creating an instance
64
+ CountTestCustomer.any?
65
+ #=> true
66
+
67
+ ## keys_any? returns true after creating an instance
68
+ CountTestCustomer.keys_any?
69
+ #=> true
70
+
71
+ ## scan_any? returns true after creating an instance
72
+ CountTestCustomer.scan_any?
73
+ #=> true
74
+
75
+ ## any! returns true after creating an instance
76
+ CountTestCustomer.any!
77
+ #=> true
78
+
79
+ ## count increases after creating another instance
80
+ @cust2 = CountTestCustomer.create!(custid: 'bob', name: 'Bob')
81
+ CountTestCustomer.count
82
+ #=> 2
83
+
84
+ ## keys_count also shows 2 instances
85
+ CountTestCustomer.keys_count
86
+ #=> 2
87
+
88
+ ## scan_count also shows 2 instances
89
+ CountTestCustomer.scan_count
90
+ #=> 2
91
+
92
+ ## count! also shows 2 instances
93
+ CountTestCustomer.count!
94
+ #=> 2
95
+
96
+ ## keys_count with filter matches specific patterns
97
+ @cust3 = CountTestCustomer.create!(custid: 'alice2', name: 'Alice2')
98
+ CountTestCustomer.keys_count('alice*')
99
+ #=> 2
100
+
101
+ ## scan_count with filter matches specific patterns
102
+ CountTestCustomer.scan_count('alice*')
103
+ #=> 2
104
+
105
+ ## count! with filter matches specific patterns
106
+ CountTestCustomer.count!('alice*')
107
+ #=> 2
108
+
109
+ ## keys_any? with filter detects matching patterns
110
+ CountTestCustomer.keys_any?('alice*')
111
+ #=> true
112
+
113
+ ## scan_any? with filter detects matching patterns
114
+ CountTestCustomer.scan_any?('alice*')
115
+ #=> true
116
+
117
+ ## any! with filter detects matching patterns
118
+ CountTestCustomer.any!('alice*')
119
+ #=> true
120
+
121
+ ## keys_any? with filter returns false for non-matching patterns
122
+ CountTestCustomer.keys_any?('nonexistent*')
123
+ #=> false
124
+
125
+ ## scan_any? with filter returns false for non-matching patterns
126
+ CountTestCustomer.scan_any?('nonexistent*')
127
+ #=> false
128
+
129
+ ## any! with filter returns false for non-matching patterns
130
+ CountTestCustomer.any!('nonexistent*')
131
+ #=> false
132
+
133
+ ## count reflects deletion when object is destroyed via Familia
134
+ @cust1.destroy!
135
+ CountTestCustomer.count
136
+ #=> 2
137
+
138
+ ## keys_count also reflects the deletion
139
+ CountTestCustomer.keys_count
140
+ #=> 2
141
+
142
+ ## scan_count also reflects the deletion
143
+ CountTestCustomer.scan_count
144
+ #=> 2
145
+
146
+ ## count! also reflects the deletion
147
+ CountTestCustomer.count!
148
+ #=> 2
149
+
150
+ ## count shows stale data when instance deleted outside Familia
151
+ # Delete directly from Redis without going through Familia
152
+ CountTestCustomer.dbclient.del(@cust2.dbkey)
153
+ CountTestCustomer.count
154
+ #=> 2
155
+
156
+ ## keys_count shows authoritative data after direct deletion
157
+ CountTestCustomer.keys_count
158
+ #=> 1
159
+
160
+ ## scan_count shows authoritative data after direct deletion
161
+ CountTestCustomer.scan_count
162
+ #=> 1
163
+
164
+ ## count! shows authoritative data after direct deletion
165
+ CountTestCustomer.count!
166
+ #=> 1
167
+
168
+ ## any? may return true even when all objects deleted outside Familia
169
+ # Instance tracking still has entries
170
+ CountTestCustomer.any?
171
+ #=> true
172
+
173
+ ## keys_any? returns false when no actual keys exist
174
+ CountTestCustomer.dbclient.del(@cust3.dbkey)
175
+ CountTestCustomer.keys_any?
176
+ #=> false
177
+
178
+ ## scan_any? returns false when no actual keys exist
179
+ CountTestCustomer.scan_any?
180
+ #=> false
181
+
182
+ ## any! returns false when no actual keys exist
183
+ CountTestCustomer.any!
184
+ #=> false
185
+
186
+ ## size alias works correctly (aliases to fast count method)
187
+ @cust4 = CountTestCustomer.create!(custid: 'charlie', name: 'Charlie')
188
+ CountTestCustomer.size == CountTestCustomer.count
189
+ #=> true
190
+
191
+ ## length alias works correctly (aliases to fast count method)
192
+ CountTestCustomer.length == CountTestCustomer.count
193
+ #=> true
194
+
195
+ # Cleanup
196
+ CountTestCustomer.instances.clear
197
+ CountTestCustomer.all.each(&:destroy!)