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
@@ -387,3 +387,137 @@ found_no_prefix&.id
387
387
  findable_custom.destroy! rescue nil
388
388
  findable_hyphen.destroy! rescue nil
389
389
  findable_no_prefix.destroy! rescue nil
390
+
391
+ # ========================================
392
+ # extid? Method Test Coverage
393
+ # ========================================
394
+
395
+ ## extid? returns false for nil
396
+ ExternalIdTest.extid?(nil)
397
+ #=> false
398
+
399
+ ## extid? returns false for empty string
400
+ ExternalIdTest.extid?('')
401
+ #=> false
402
+
403
+ ## extid? returns false for whitespace-only string
404
+ ExternalIdTest.extid?(' ')
405
+ #=> false
406
+
407
+ ## extid? returns true for valid extid with default format
408
+ # Valid format: ext_ prefix + exactly 25 alphanumeric characters
409
+ ExternalIdTest.extid?('ext_0123456789abcdefghijklmno')
410
+ #=> true
411
+
412
+ ## extid? returns true for valid extid with uppercase characters
413
+ # The pattern allows [0-9a-zA-Z] for the ID part
414
+ ExternalIdTest.extid?('ext_0123456789ABCDEFGHIJKLMNO')
415
+ #=> true
416
+
417
+ ## extid? returns true for valid extid with mixed case
418
+ ExternalIdTest.extid?('ext_0123456789AbCdEfGhIjKlMnO')
419
+ #=> true
420
+
421
+ ## extid? returns true for generated extid
422
+ obj = ExternalIdTest.new
423
+ ExternalIdTest.extid?(obj.extid)
424
+ #=> true
425
+
426
+ ## extid? returns false for wrong prefix
427
+ ExternalIdTest.extid?('xxx_0123456789abcdefghijklmno')
428
+ #=> false
429
+
430
+ ## extid? returns false for missing prefix
431
+ ExternalIdTest.extid?('0123456789abcdefghijklmno')
432
+ #=> false
433
+
434
+ ## extid? accepts ID part with 24 chars (within 20-32 range)
435
+ ExternalIdTest.extid?('ext_0123456789abcdefghijklmn')
436
+ #=> true
437
+
438
+ ## extid? accepts ID part with 26 chars (within 20-32 range)
439
+ ExternalIdTest.extid?('ext_0123456789abcdefghijklmnop')
440
+ #=> true
441
+
442
+ ## extid? returns false for ID part too short (19 chars, below minimum)
443
+ ExternalIdTest.extid?('ext_0123456789abcdefghi')
444
+ #=> false
445
+
446
+ ## extid? returns false for ID part too long (33 chars, above maximum)
447
+ ExternalIdTest.extid?('ext_0123456789abcdefghijklmnopqrstuvw')
448
+ #=> false
449
+
450
+ ## extid? returns false for invalid characters in ID (underscore)
451
+ ExternalIdTest.extid?('ext_0123456789abcdefghij_lmno')
452
+ #=> false
453
+
454
+ ## extid? returns false for invalid characters in ID (hyphen)
455
+ ExternalIdTest.extid?('ext_0123456789abcdefghij-lmno')
456
+ #=> false
457
+
458
+ ## extid? returns false for invalid characters in ID (special chars)
459
+ ExternalIdTest.extid?('ext_0123456789abcdefghij!@#$%')
460
+ #=> false
461
+
462
+ ## extid? with custom prefix format returns true for valid extid
463
+ CustomPrefixTest.extid?('cust_0123456789abcdefghijklmno')
464
+ #=> true
465
+
466
+ ## extid? with custom prefix format returns false for default prefix
467
+ CustomPrefixTest.extid?('ext_0123456789abcdefghijklmno')
468
+ #=> false
469
+
470
+ ## extid? with custom prefix format returns true for generated extid
471
+ custom_obj = CustomPrefixTest.new
472
+ CustomPrefixTest.extid?(custom_obj.extid)
473
+ #=> true
474
+
475
+ ## extid? with hyphen format returns true for valid extid
476
+ CustomFormatTest.extid?('ext-0123456789abcdefghijklmno')
477
+ #=> true
478
+
479
+ ## extid? with hyphen format returns false for underscore format
480
+ CustomFormatTest.extid?('ext_0123456789abcdefghijklmno')
481
+ #=> false
482
+
483
+ ## extid? with hyphen format returns true for generated extid
484
+ hyphen_obj = CustomFormatTest.new
485
+ CustomFormatTest.extid?(hyphen_obj.extid)
486
+ #=> true
487
+
488
+ ## extid? with no-prefix format returns true for valid extid
489
+ NoPrefixFormatTest.extid?('api/0123456789abcdefghijklmno')
490
+ #=> true
491
+
492
+ ## extid? with no-prefix format returns false for default format
493
+ NoPrefixFormatTest.extid?('ext_0123456789abcdefghijklmno')
494
+ #=> false
495
+
496
+ ## extid? with no-prefix format returns true for generated extid
497
+ no_prefix_obj = NoPrefixFormatTest.new
498
+ NoPrefixFormatTest.extid?(no_prefix_obj.extid)
499
+ #=> true
500
+
501
+ ## extid? returns false for partial match at start
502
+ ExternalIdTest.extid?('ext_0123456789abcdefghijklmno_extra')
503
+ #=> false
504
+
505
+ ## extid? returns false for partial match with leading chars
506
+ ExternalIdTest.extid?('prefix_ext_0123456789abcdefghijklmno')
507
+ #=> false
508
+
509
+ ## extid? returns false for spaces in ID
510
+ ExternalIdTest.extid?('ext_0123456789 bcdefghijklmno')
511
+ #=> false
512
+
513
+ ## extid? returns false for just the prefix
514
+ ExternalIdTest.extid?('ext_')
515
+ #=> false
516
+
517
+ ## extid? with Symbol input (symbols support =~ matching)
518
+ ExternalIdTest.extid?(:ext_0123456789abcdefghijklmno)
519
+ #=> true
520
+
521
+ ## extid? with Symbol input returns false for invalid format
522
+ ExternalIdTest.extid?(:invalid_symbol)
523
+ #=> false
@@ -201,3 +201,141 @@ race_obj.save # Save so find_by_objid can locate
201
201
  found = BasicObjectTest.find_by_objid(generated_objid)
202
202
  found && found.id == "race_test_123"
203
203
  #=> true
204
+
205
+ # ========================================
206
+ # objid? Method Test Coverage
207
+ # ========================================
208
+
209
+ ## objid? returns false for nil
210
+ BasicObjectTest.objid?(nil)
211
+ #=> false
212
+
213
+ ## objid? returns false for empty string
214
+ BasicObjectTest.objid?('')
215
+ #=> false
216
+
217
+ ## objid? returns false for whitespace-only string
218
+ BasicObjectTest.objid?(' ')
219
+ #=> false
220
+
221
+ ## objid? returns true for valid UUID v7 format
222
+ # UUID v7 format: xxxxxxxx-xxxx-7xxx-xxxx-xxxxxxxxxxxx (version char is '7')
223
+ BasicObjectTest.objid?('01234567-89ab-7def-89ab-0123456789ab')
224
+ #=> true
225
+
226
+ ## objid? returns true for generated UUID v7 objid
227
+ obj = BasicObjectTest.new
228
+ BasicObjectTest.objid?(obj.objid)
229
+ #=> true
230
+
231
+ ## objid? returns false for UUID v4 in UUID v7 class
232
+ # Version char is '4' instead of '7'
233
+ BasicObjectTest.objid?('01234567-89ab-4def-89ab-0123456789ab')
234
+ #=> false
235
+
236
+ ## objid? returns false for wrong version char
237
+ BasicObjectTest.objid?('01234567-89ab-5def-89ab-0123456789ab')
238
+ #=> false
239
+
240
+ ## objid? returns false for missing hyphens
241
+ BasicObjectTest.objid?('0123456789ab7def89ab0123456789ab')
242
+ #=> false
243
+
244
+ ## objid? returns false for wrong length (too short)
245
+ BasicObjectTest.objid?('01234567-89ab-7def-89ab-012345678')
246
+ #=> false
247
+
248
+ ## objid? returns false for wrong length (too long)
249
+ BasicObjectTest.objid?('01234567-89ab-7def-89ab-0123456789abcd')
250
+ #=> false
251
+
252
+ ## objid? returns false for hyphens in wrong positions
253
+ BasicObjectTest.objid?('0123456-789ab-7def-89ab-0123456789ab')
254
+ #=> false
255
+
256
+ ## objid? returns false for UUID with non-hex characters
257
+ # Validates that all characters are valid hexadecimal digits
258
+ BasicObjectTest.objid?('gggggggg-gggg-7ggg-gggg-gggggggggggg')
259
+ #=> false
260
+
261
+ ## objid? with UUID v4 generator returns true for valid v4 format
262
+ UuidV4Test.objid?('01234567-89ab-4def-89ab-0123456789ab')
263
+ #=> true
264
+
265
+ ## objid? with UUID v4 generator returns true for generated objid
266
+ v4_obj = UuidV4Test.new
267
+ UuidV4Test.objid?(v4_obj.objid)
268
+ #=> true
269
+
270
+ ## objid? with UUID v4 generator returns false for v7 format
271
+ UuidV4Test.objid?('01234567-89ab-7def-89ab-0123456789ab')
272
+ #=> false
273
+
274
+ ## objid? with hex generator returns true for valid hex string
275
+ HexTest.objid?('0123456789abcdef')
276
+ #=> true
277
+
278
+ ## objid? with hex generator returns true for generated hex objid
279
+ hex_obj = HexTest.new
280
+ HexTest.objid?(hex_obj.objid)
281
+ #=> true
282
+
283
+ ## objid? with hex generator returns true for uppercase hex
284
+ HexTest.objid?('0123456789ABCDEF')
285
+ #=> true
286
+
287
+ ## objid? with hex generator returns true for mixed case hex
288
+ HexTest.objid?('0123456789AbCdEf')
289
+ #=> true
290
+
291
+ ## objid? with hex generator returns false for non-hex chars
292
+ HexTest.objid?('0123456789ghijkl')
293
+ #=> false
294
+
295
+ ## objid? with hex generator returns false for UUID format
296
+ HexTest.objid?('01234567-89ab-7def-89ab-0123456789ab')
297
+ #=> false
298
+
299
+ ## objid? with hex generator returns true for short hex
300
+ HexTest.objid?('abc')
301
+ #=> true
302
+
303
+ ## objid? with hex generator returns true for long hex
304
+ HexTest.objid?('0123456789abcdef0123456789abcdef')
305
+ #=> true
306
+
307
+ ## objid? with custom proc generator returns false (unsupported)
308
+ CustomProcTest.objid?('custom_12345678')
309
+ #=> false
310
+
311
+ ## objid? with custom proc generator returns false for any input
312
+ CustomProcTest.objid?('anything')
313
+ #=> false
314
+
315
+ ## objid? UUID validation checks position 8 for hyphen
316
+ BasicObjectTest.objid?('01234567X89ab-7def-89ab-0123456789ab')
317
+ #=> false
318
+
319
+ ## objid? UUID validation checks position 13 for hyphen
320
+ BasicObjectTest.objid?('01234567-89abX7def-89ab-0123456789ab')
321
+ #=> false
322
+
323
+ ## objid? UUID validation checks position 18 for hyphen
324
+ BasicObjectTest.objid?('01234567-89ab-7defX89ab-0123456789ab')
325
+ #=> false
326
+
327
+ ## objid? UUID validation checks position 23 for hyphen
328
+ BasicObjectTest.objid?('01234567-89ab-7def-89abX0123456789ab')
329
+ #=> false
330
+
331
+ ## objid? returns false for partial UUID match
332
+ BasicObjectTest.objid?('01234567-89ab-7def')
333
+ #=> false
334
+
335
+ ## objid? with Symbol input for UUID v7 (symbols support string comparison)
336
+ BasicObjectTest.objid?(:'01234567-89ab-7def-89ab-0123456789ab')
337
+ #=> true
338
+
339
+ ## objid? with Symbol input returns false for invalid format
340
+ BasicObjectTest.objid?(:invalid_objid)
341
+ #=> false
@@ -553,6 +553,9 @@ found3 = @company.find_by_badge_number("BADGE003")
553
553
  # The architecture prevents this via factory pattern, but guard provides explicit protection
554
554
  begin
555
555
  # Simulate calling rebuild_via_participation with multi-index cardinality
556
+ # Note: dept_index is a multi-index, so we need to use dept_index_for to get a DataType instance
557
+ # But for this test, we just need any HashKey-like object to pass for serialization
558
+ index_hashkey = @company.badge_index # Use a valid HashKey index
556
559
  Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
557
560
  @company,
558
561
  RebuildTestEmployee,
@@ -560,6 +563,7 @@ begin
560
563
  :add_to_rebuild_test_company_dept_index,
561
564
  @company.employees,
562
565
  :multi, # Wrong cardinality!
566
+ index_hashkey, # Added required parameter
563
567
  batch_size: 100
564
568
  )
565
569
  "should have raised"
@@ -571,6 +575,7 @@ end
571
575
  ## Guard accepts correct cardinality (:unique)
572
576
  begin
573
577
  index_config = RebuildTestEmployee.indexing_relationships.find { |r| r.index_name == :badge_index }
578
+ index_hashkey = @company.badge_index # Get the index HashKey for serialization
574
579
  Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
575
580
  @company,
576
581
  RebuildTestEmployee,
@@ -578,6 +583,7 @@ begin
578
583
  :add_to_rebuild_test_company_badge_index,
579
584
  @company.employees,
580
585
  :unique, # Correct cardinality
586
+ index_hashkey, # Added required parameter
581
587
  batch_size: 100
582
588
  )
583
589
  "no error"
@@ -81,10 +81,12 @@ result = @user.scores.pipelined { |pipe| }
81
81
  #=> [true, true]
82
82
 
83
83
  ## Multiple DataType operations in single pipeline
84
+ # Note: Raw Redis commands bypass Familia's JSON serialization.
85
+ # Use serialize_value for values that will be looked up via Familia methods.
84
86
  result = @user.scores.pipelined do |pipe|
85
- pipe.zadd(@user.scores.dbkey, 500, 'multi')
86
- pipe.hset(@user.profile.dbkey, 'multi', 'pipeline')
87
- pipe.sadd(@user.tags.dbkey, 'multi_tag')
87
+ pipe.zadd(@user.scores.dbkey, 500, @user.scores.serialize_value('multi'))
88
+ pipe.hset(@user.profile.dbkey, 'multi', @user.profile.serialize_value('pipeline'))
89
+ pipe.sadd(@user.tags.dbkey, @user.tags.serialize_value('multi_tag'))
88
90
  end
89
91
  [
90
92
  result.is_a?(MultiResult),
@@ -152,6 +152,8 @@ conn_class
152
152
  #=> "Redis::MultiConnection"
153
153
 
154
154
  ## Transaction with direct_access works correctly
155
+ # Note: direct_access bypasses serialize_value, so raw 'true' string
156
+ # gets parsed as JSON boolean true on retrieval (Issue #190 behavior)
155
157
  result = @user.profile.transaction do |trans_conn|
156
158
  trans_conn.hset(@user.profile.dbkey, 'status', 'active')
157
159
 
@@ -162,7 +164,7 @@ result = @user.profile.transaction do |trans_conn|
162
164
  end
163
165
  end
164
166
  [@user.profile['status'], @user.profile['verified']]
165
- #=> ["active", "true"]
167
+ #=> ["active", true]
166
168
 
167
169
  ## Transaction atomicity - all commands succeed or none
168
170
  test_zset = Familia::SortedSet.new('atomic:test')
@@ -182,11 +184,13 @@ end
182
184
  #=> ["initial"]
183
185
 
184
186
  ## Nested transactions with parent-owned DataTypes work
187
+ # Note: Raw Redis commands bypass Familia's JSON serialization.
188
+ # Use serialize_value or check raw members for consistency.
185
189
  outer_result = @user.scores.transaction do |outer_conn|
186
- outer_conn.zadd(@user.scores.dbkey, 999, 'outer_member')
190
+ outer_conn.zadd(@user.scores.dbkey, 999, @user.scores.serialize_value('outer_member'))
187
191
 
188
192
  inner_result = @user.tags.transaction do |inner_conn|
189
- inner_conn.sadd(@user.tags.dbkey, 'nested_tag')
193
+ inner_conn.sadd(@user.tags.dbkey, @user.tags.serialize_value('nested_tag'))
190
194
  end
191
195
 
192
196
  inner_result.is_a?(MultiResult)
@@ -231,12 +235,14 @@ TransactionTestUser.logical_database
231
235
  #=> 2
232
236
 
233
237
  ## Multiple DataType types in single transaction
238
+ # Note: Raw Redis commands bypass Familia's JSON serialization.
239
+ # Use serialize_value for values that will be looked up via Familia methods.
234
240
  result = @user.scores.transaction do |conn|
235
241
  # Can operate on different DataTypes using same connection
236
- conn.zadd(@user.scores.dbkey, 777, 'multi_test')
237
- conn.hset(@user.profile.dbkey, 'multi', 'yes')
238
- conn.sadd(@user.tags.dbkey, 'multi_tag')
239
- conn.rpush(@user.activity.dbkey, 'multi_action')
242
+ conn.zadd(@user.scores.dbkey, 777, @user.scores.serialize_value('multi_test'))
243
+ conn.hset(@user.profile.dbkey, 'multi', @user.profile.serialize_value('yes'))
244
+ conn.sadd(@user.tags.dbkey, @user.tags.serialize_value('multi_tag'))
245
+ conn.rpush(@user.activity.dbkey, @user.activity.serialize_value('multi_action'))
240
246
  end
241
247
  [
242
248
  result.is_a?(MultiResult),
@@ -73,7 +73,8 @@ Customer.find_by_id(ident).planid
73
73
  #=> true
74
74
 
75
75
  ## Customer can be added to class-level sorted set
76
- Customer.instances.add(@customer.identifier)
76
+ # Note: Add the object directly so identifier extraction is consistent
77
+ Customer.instances.add(@customer)
77
78
  Customer.instances.member?(@customer)
78
79
  #=> true
79
80
 
@@ -97,11 +98,10 @@ multi_result = @customer.destroy!
97
98
  cust = Customer.find_by_id('test@example.com')
98
99
  exists = Customer.exists?('test@example.com')
99
100
  [multi_result.results, cust.nil?, exists]
100
- #=> [[1, 0, 1, 1, 1, 1, 1], true, false]
101
+ #=> [[1, 0, 1, 1, 1, 1, 1, true], true, false]
101
102
 
102
103
  ## Customer.destroy! can be called on an already destroyed object
103
104
  @customer.destroy!
104
- #=:> MultiResult
105
105
  #==> result.successful?
106
106
  #=*> result.results
107
107
 
@@ -3,6 +3,7 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  # try/data_types/boolean_try.rb
6
+ # Issue #190: Updated to reflect JSON serialization with type preservation
6
7
 
7
8
  require_relative '../../support/helpers/test_helpers'
8
9
 
@@ -10,37 +11,49 @@ Familia.debug = false
10
11
 
11
12
  @hashkey = Familia::HashKey.new 'key'
12
13
 
13
- ## Boolean values are returned as strings, on assignment as string
14
+ ## String 'true' is stored and returned as string
14
15
  @hashkey['test'] = 'true'
15
16
  #=> "true"
16
17
 
17
- ## Boolean values are returned as strings
18
+ ## String values are returned as strings
18
19
  @hashkey['test']
19
20
  #=> "true"
20
21
 
21
- ## Trying to store a boolean value to a hash key raises an exception
22
- begin
23
- @hashkey['test'] = true
24
- rescue Familia::NotDistinguishableError => e
25
- e.message
26
- end
27
- #=> "Cannot represent true<TrueClass> as a string"
22
+ ## Boolean true is now stored with type preservation (Issue #190)
23
+ @hashkey['bool_true'] = true
24
+ #=> true
28
25
 
29
- ## Boolean values are returned as strings
30
- @hashkey['test']
31
- #=> "true"
26
+ ## Boolean true is returned as TrueClass
27
+ @hashkey['bool_true']
28
+ #=> true
32
29
 
33
- ## Trying to store a nil value to a hash key raises an exception
34
- begin
35
- @hashkey['test'] = nil
36
- rescue Familia::NotDistinguishableError => e
37
- e.message
38
- end
39
- #=> "Cannot represent <NilClass> as a string"
30
+ ## Boolean true has correct class
31
+ @hashkey['bool_true'].class
32
+ #=> TrueClass
40
33
 
41
- ## The exceptions prevented the hash from being updated
42
- @hashkey['test']
43
- #=> "true"
34
+ ## Boolean false is stored with type preservation
35
+ @hashkey['bool_false'] = false
36
+ #=> false
37
+
38
+ ## Boolean false is returned as FalseClass
39
+ @hashkey['bool_false']
40
+ #=> false
41
+
42
+ ## Boolean false has correct class
43
+ @hashkey['bool_false'].class
44
+ #=> FalseClass
45
+
46
+ ## nil is stored with type preservation
47
+ @hashkey['nil_value'] = nil
48
+ #=> nil
49
+
50
+ ## nil is returned as nil
51
+ @hashkey['nil_value']
52
+ #=> nil
53
+
54
+ ## nil has correct class
55
+ @hashkey['nil_value'].class
56
+ #=> NilClass
44
57
 
45
58
  ## Clear the hash key
46
59
  @hashkey.delete!
@@ -50,8 +50,8 @@ require_relative '../../support/helpers/test_helpers'
50
50
  @a.props.decrement 'counter', 60
51
51
  #=> 40
52
52
 
53
- ## Familia::HashKey#values_at
53
+ ## Familia::HashKey#values_at (counter is integer from HINCRBY, others are strings)
54
54
  @a.props.values_at 'fieldA', 'counter', 'fieldC'
55
- #=> ['1', '40', '3']
55
+ #=> ['1', 40, '3']
56
56
 
57
57
  @a.props.delete!