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.
- checksums.yaml +4 -4
- data/.talismanrc +5 -1
- data/CHANGELOG.rst +43 -0
- data/Gemfile.lock +1 -1
- data/lib/familia/connection/operation_core.rb +1 -2
- data/lib/familia/connection/pipelined_core.rb +1 -3
- data/lib/familia/connection/transaction_core.rb +1 -2
- data/lib/familia/data_type/serialization.rb +76 -51
- data/lib/familia/data_type/types/sorted_set.rb +5 -10
- data/lib/familia/data_type/types/stringkey.rb +22 -0
- data/lib/familia/features/external_identifier.rb +29 -0
- data/lib/familia/features/object_identifier.rb +47 -0
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +15 -15
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +8 -0
- data/lib/familia/horreum/database_commands.rb +6 -1
- data/lib/familia/horreum/management.rb +141 -10
- data/lib/familia/horreum/persistence.rb +3 -0
- data/lib/familia/identifier_extractor.rb +1 -1
- data/lib/familia/version.rb +1 -1
- data/lib/multi_result.rb +59 -31
- data/try/features/count_any_edge_cases_try.rb +486 -0
- data/try/features/count_any_methods_try.rb +197 -0
- data/try/features/external_identifier/external_identifier_try.rb +134 -0
- data/try/features/object_identifier/object_identifier_try.rb +138 -0
- data/try/features/relationships/indexing_rebuild_try.rb +6 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +5 -3
- data/try/integration/data_types/datatype_transactions_try.rb +13 -7
- data/try/integration/models/customer_try.rb +3 -3
- data/try/unit/data_types/boolean_try.rb +35 -22
- data/try/unit/data_types/hash_try.rb +2 -2
- data/try/unit/data_types/serialization_try.rb +386 -0
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +2 -1
- metadata +4 -7
- data/changelog.d/20251105_flexible_external_identifier_format.rst +0 -66
- data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +0 -44
- data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +0 -20
- data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +0 -91
- data/changelog.d/20251107_optimized_redis_exists_checks.rst +0 -94
- 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!)
|