familia 2.0.0.pre21 → 2.0.0.pre23

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +8 -5
  3. data/.talismanrc +5 -1
  4. data/CHANGELOG.rst +76 -0
  5. data/Gemfile.lock +8 -8
  6. data/docs/1106-participates_in-bidirectional-solution.md +201 -58
  7. data/examples/through_relationships.rb +275 -0
  8. data/lib/familia/connection/operation_core.rb +1 -2
  9. data/lib/familia/connection/pipelined_core.rb +1 -3
  10. data/lib/familia/connection/transaction_core.rb +1 -2
  11. data/lib/familia/data_type/serialization.rb +76 -51
  12. data/lib/familia/data_type/types/sorted_set.rb +5 -10
  13. data/lib/familia/data_type/types/stringkey.rb +22 -0
  14. data/lib/familia/features/external_identifier.rb +29 -0
  15. data/lib/familia/features/object_identifier.rb +47 -0
  16. data/lib/familia/features/relationships/README.md +1 -1
  17. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +15 -15
  18. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +8 -0
  19. data/lib/familia/features/relationships/participation/participant_methods.rb +59 -10
  20. data/lib/familia/features/relationships/participation/target_methods.rb +51 -7
  21. data/lib/familia/features/relationships/participation/through_model_operations.rb +150 -0
  22. data/lib/familia/features/relationships/participation.rb +39 -15
  23. data/lib/familia/features/relationships/participation_relationship.rb +19 -1
  24. data/lib/familia/features/relationships.rb +1 -1
  25. data/lib/familia/horreum/database_commands.rb +6 -1
  26. data/lib/familia/horreum/management.rb +141 -10
  27. data/lib/familia/horreum/persistence.rb +3 -0
  28. data/lib/familia/identifier_extractor.rb +1 -1
  29. data/lib/familia/version.rb +1 -1
  30. data/lib/multi_result.rb +59 -31
  31. data/pr_agent.toml +6 -1
  32. data/try/features/count_any_edge_cases_try.rb +486 -0
  33. data/try/features/count_any_methods_try.rb +197 -0
  34. data/try/features/external_identifier/external_identifier_try.rb +134 -0
  35. data/try/features/object_identifier/object_identifier_try.rb +138 -0
  36. data/try/features/relationships/indexing_rebuild_try.rb +6 -0
  37. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  38. data/try/features/relationships/participation_commands_verification_try.rb +1 -1
  39. data/try/features/relationships/participation_method_prefix_try.rb +133 -0
  40. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  41. data/try/features/relationships/{participation_bidirectional_try.rb → participation_reverse_methods_try.rb} +6 -6
  42. data/try/features/relationships/participation_through_try.rb +173 -0
  43. data/try/integration/data_types/datatype_pipelines_try.rb +5 -3
  44. data/try/integration/data_types/datatype_transactions_try.rb +13 -7
  45. data/try/integration/models/customer_try.rb +3 -3
  46. data/try/unit/data_types/boolean_try.rb +35 -22
  47. data/try/unit/data_types/hash_try.rb +2 -2
  48. data/try/unit/data_types/serialization_try.rb +386 -0
  49. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +2 -1
  50. metadata +9 -8
  51. data/changelog.d/20251105_flexible_external_identifier_format.rst +0 -66
  52. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +0 -44
  53. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +0 -20
  54. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +0 -91
  55. data/changelog.d/20251107_optimized_redis_exists_checks.rst +0 -94
  56. data/changelog.d/20251108_frozen_string_literal_pragma.rst +0 -44
@@ -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!
@@ -0,0 +1,386 @@
1
+ # try/unit/data_types/serialization_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Test coverage for DataType serialization/deserialization behavior
6
+ # Issue #190: Unify DataType and Horreum serialization for type preservation
7
+
8
+ require_relative '../../support/helpers/test_helpers'
9
+
10
+ Familia.debug = false
11
+
12
+ # Create test instances
13
+ @bone = Bone.new('serialize_test_token')
14
+
15
+ # ========================================
16
+ # DataType Serialization Behavior (Issue #190)
17
+ # Now uses JSON serialization for type preservation
18
+ # ========================================
19
+
20
+ ## HashKey stores string values correctly
21
+ @bone.props['string_field'] = 'hello'
22
+ @bone.props['string_field']
23
+ #=> 'hello'
24
+
25
+ ## HashKey stores integer with type preservation
26
+ @bone.props['int_field'] = 42
27
+ @bone.props['int_field']
28
+ #=> 42
29
+
30
+ ## HashKey stores float with type preservation
31
+ @bone.props['float_field'] = 3.14
32
+ @bone.props['float_field']
33
+ #=> 3.14
34
+
35
+ ## HashKey stores symbol as string (symbols serialize to strings in JSON)
36
+ @bone.props['symbol_field'] = :active
37
+ @bone.props['symbol_field']
38
+ #=> 'active'
39
+
40
+ ## HashKey stores boolean true with type preservation
41
+ @bone.props['bool_true'] = true
42
+ @bone.props['bool_true']
43
+ #=> true
44
+
45
+ ## HashKey stores boolean true as TrueClass
46
+ @bone.props['bool_true'].class
47
+ #=> TrueClass
48
+
49
+ ## HashKey stores boolean false with type preservation
50
+ @bone.props['bool_false'] = false
51
+ @bone.props['bool_false']
52
+ #=> false
53
+
54
+ ## HashKey stores boolean false as FalseClass
55
+ @bone.props['bool_false'].class
56
+ #=> FalseClass
57
+
58
+ ## HashKey stores nil with type preservation
59
+ @bone.props['nil_field'] = nil
60
+ @bone.props['nil_field']
61
+ #=> nil
62
+
63
+ ## HashKey stores hash with type preservation
64
+ @bone.props['hash_field'] = { 'key' => 'value' }
65
+ @bone.props['hash_field']
66
+ #=> {'key'=>'value'}
67
+
68
+ ## HashKey stores array with type preservation
69
+ @bone.props['array_field'] = [1, 2, 3]
70
+ @bone.props['array_field']
71
+ #=> [1, 2, 3]
72
+
73
+ # ========================================
74
+ # List Serialization Behavior
75
+ # ========================================
76
+
77
+ ## List stores string values correctly
78
+ @bone.owners.delete!
79
+ @bone.owners.push('owner1')
80
+ @bone.owners.first
81
+ #=> 'owner1'
82
+
83
+ ## List stores integer with type preservation
84
+ @bone.owners.delete!
85
+ @bone.owners.push(123)
86
+ @bone.owners.first
87
+ #=> 123
88
+
89
+ ## List stores boolean with type preservation
90
+ @bone.owners.delete!
91
+ @bone.owners.push(true)
92
+ @bone.owners.first
93
+ #=> true
94
+
95
+ ## List stores nil with type preservation
96
+ @bone.owners.delete!
97
+ @bone.owners.push(nil)
98
+ @bone.owners.first
99
+ #=> nil
100
+
101
+ # ========================================
102
+ # Set Serialization Behavior
103
+ # ========================================
104
+
105
+ ## Set stores string values correctly
106
+ @bone.tags.delete!
107
+ @bone.tags.add('tag1')
108
+ @bone.tags.members.include?('tag1')
109
+ #=> true
110
+
111
+ ## Set stores integer with type preservation
112
+ @bone.tags.delete!
113
+ @bone.tags.add(42)
114
+ @bone.tags.members.include?(42)
115
+ #=> true
116
+
117
+ ## Set stores boolean with type preservation
118
+ @bone.tags.delete!
119
+ @bone.tags.add(true)
120
+ @bone.tags.members.include?(true)
121
+ #=> true
122
+
123
+ # ========================================
124
+ # SortedSet Serialization Behavior
125
+ # ========================================
126
+
127
+ ## SortedSet stores string values correctly
128
+ @bone.metrics.delete!
129
+ @bone.metrics.add('metric1', 1.0)
130
+ @bone.metrics.members.include?('metric1')
131
+ #=> true
132
+
133
+ ## SortedSet stores integer member with type preservation
134
+ @bone.metrics.delete!
135
+ @bone.metrics.add(999, 1.0)
136
+ @bone.metrics.members.include?(999)
137
+ #=> true
138
+
139
+ ## SortedSet stores boolean member with type preservation
140
+ @bone.metrics.delete!
141
+ @bone.metrics.add(true, 1.0)
142
+ @bone.metrics.members.include?(true)
143
+ #=> true
144
+
145
+ # ========================================
146
+ # Horreum Field Serialization (for comparison)
147
+ # Uses JSON encoding - type preserved
148
+ # ========================================
149
+
150
+ ## Horreum field stores string with JSON encoding
151
+ @customer = Customer.new
152
+ @customer.custid = 'serialization_test'
153
+ @customer.role = 'admin'
154
+ @customer.save
155
+ loaded = Customer.find_by_id('serialization_test')
156
+ loaded.role
157
+ #=> 'admin'
158
+
159
+ ## Horreum field stores boolean true (JSON encoded)
160
+ @customer.verified = true
161
+ @customer.save
162
+ @loaded_customer = Customer.find_by_id('serialization_test')
163
+ @loaded_customer.verified
164
+ #=> true
165
+
166
+ ## Horreum verified field is actually boolean, not string
167
+ @loaded_customer.verified.class
168
+ #=> TrueClass
169
+
170
+ ## Horreum field stores boolean false (JSON encoded)
171
+ @customer.reset_requested = false
172
+ @customer.save
173
+ @loaded_customer2 = Customer.find_by_id('serialization_test')
174
+ @loaded_customer2.reset_requested
175
+ #=> false
176
+
177
+ ## Horreum reset_requested field is actually boolean, not string
178
+ @loaded_customer2.reset_requested.class
179
+ #=> FalseClass
180
+
181
+ # ========================================
182
+ # Type Round-Trip Comparison (Unified Behavior)
183
+ # ========================================
184
+
185
+ ## Integer round-trip in HashKey now preserves type (Issue #190)
186
+ @bone.props['roundtrip_int'] = 100
187
+ retrieved = @bone.props['roundtrip_int']
188
+ retrieved.class
189
+ #=> Integer
190
+
191
+ ## Boolean round-trip in HashKey preserves type
192
+ @bone.props['roundtrip_bool'] = true
193
+ @bone.props['roundtrip_bool'].class
194
+ #=> TrueClass
195
+
196
+ ## DataType and Horreum now use same JSON serialization
197
+ @session = Session.new
198
+ @session.sessid = 'roundtrip_test'
199
+ # Both DataType and Horreum fields now preserve types consistently
200
+
201
+ # ========================================
202
+ # Horreum serialize_value Comprehensive Tests
203
+ # (Issue #190: Document behavior for unification)
204
+ # ========================================
205
+
206
+ ## Horreum serialize_value: string gets JSON encoded with quotes
207
+ @customer.serialize_value('hello')
208
+ #=> '"hello"'
209
+
210
+ ## Horreum serialize_value: empty string gets JSON encoded
211
+ @customer.serialize_value('')
212
+ #=> '""'
213
+
214
+ ## Horreum serialize_value: integer becomes JSON number (no quotes)
215
+ @customer.serialize_value(42)
216
+ #=> '42'
217
+
218
+ ## Horreum serialize_value: zero becomes JSON number
219
+ @customer.serialize_value(0)
220
+ #=> '0'
221
+
222
+ ## Horreum serialize_value: negative integer
223
+ @customer.serialize_value(-99)
224
+ #=> '-99'
225
+
226
+ ## Horreum serialize_value: float becomes JSON number
227
+ @customer.serialize_value(3.14159)
228
+ #=> '3.14159'
229
+
230
+ ## Horreum serialize_value: boolean true becomes JSON true
231
+ @customer.serialize_value(true)
232
+ #=> 'true'
233
+
234
+ ## Horreum serialize_value: boolean false becomes JSON false
235
+ @customer.serialize_value(false)
236
+ #=> 'false'
237
+
238
+ ## Horreum serialize_value: nil becomes JSON null
239
+ @customer.serialize_value(nil)
240
+ #=> 'null'
241
+
242
+ ## Horreum serialize_value: symbol becomes JSON string
243
+ @customer.serialize_value(:active)
244
+ #=> '"active"'
245
+
246
+ ## Horreum serialize_value: hash becomes JSON object
247
+ @customer.serialize_value({ name: 'test', count: 5 })
248
+ #=> '{"name":"test","count":5}'
249
+
250
+ ## Horreum serialize_value: array becomes JSON array
251
+ @customer.serialize_value([1, 'two', true, nil])
252
+ #=> '[1,"two",true,null]'
253
+
254
+ ## Horreum serialize_value: nested structures work
255
+ @customer.serialize_value({ users: [{ id: 1 }, { id: 2 }] })
256
+ #=> '{"users":[{"id":1},{"id":2}]}'
257
+
258
+ # ========================================
259
+ # Horreum deserialize_value Comprehensive Tests
260
+ # ========================================
261
+
262
+ ## Horreum deserialize_value: JSON string becomes Ruby string
263
+ @customer.deserialize_value('"hello"')
264
+ #=> 'hello'
265
+
266
+ ## Horreum deserialize_value: JSON number becomes Ruby integer
267
+ @customer.deserialize_value('42')
268
+ #=> 42
269
+
270
+ ## Horreum deserialize_value: JSON number is actually Integer class
271
+ @customer.deserialize_value('42').class
272
+ #=> Integer
273
+
274
+ ## Horreum deserialize_value: JSON float becomes Ruby float
275
+ @customer.deserialize_value('3.14159')
276
+ #=> 3.14159
277
+
278
+ ## Horreum deserialize_value: JSON float is actually Float class
279
+ @customer.deserialize_value('3.14159').class
280
+ #=> Float
281
+
282
+ ## Horreum deserialize_value: JSON true becomes Ruby true
283
+ @customer.deserialize_value('true')
284
+ #=> true
285
+
286
+ ## Horreum deserialize_value: JSON true is TrueClass
287
+ @customer.deserialize_value('true').class
288
+ #=> TrueClass
289
+
290
+ ## Horreum deserialize_value: JSON false becomes Ruby false
291
+ @customer.deserialize_value('false')
292
+ #=> false
293
+
294
+ ## Horreum deserialize_value: JSON false is FalseClass
295
+ @customer.deserialize_value('false').class
296
+ #=> FalseClass
297
+
298
+ ## Horreum deserialize_value: JSON null becomes Ruby nil
299
+ @customer.deserialize_value('null')
300
+ #=> nil
301
+
302
+ ## Horreum deserialize_value: JSON object becomes Ruby hash
303
+ @customer.deserialize_value('{"name":"test","count":5}')
304
+ #=> {"name"=>"test", "count"=>5}
305
+
306
+ ## Horreum deserialize_value: JSON array becomes Ruby array
307
+ @customer.deserialize_value('[1,"two",true,null]')
308
+ #=> [1, "two", true, nil]
309
+
310
+ ## Horreum deserialize_value: nil input returns nil
311
+ @customer.deserialize_value(nil)
312
+ #=> nil
313
+
314
+ ## Horreum deserialize_value: empty string returns nil
315
+ @customer.deserialize_value('')
316
+ #=> nil
317
+
318
+ ## Horreum deserialize_value: plain unquoted string (legacy data) returns as-is
319
+ # This handles data stored before JSON encoding was used
320
+ @customer.deserialize_value('plain string without quotes')
321
+ #=> 'plain string without quotes'
322
+
323
+ # ========================================
324
+ # Familia Object Serialization (shared behavior)
325
+ # ========================================
326
+
327
+ ## Horreum serialize_value: string value gets JSON encoded
328
+ # When storing a value from another Familia object's field
329
+ @ref_customer = Customer.new('reference_test@example.com')
330
+ @ref_customer.custid = 'reference_test@example.com'
331
+ # Note: Horreum.serialize_value uses JsonSerializer.dump, which JSON-encodes
332
+ # all values, including strings. This is different from DataType#serialize_value,
333
+ # which has special handling for Familia objects.
334
+ @customer.serialize_value(@ref_customer.custid)
335
+ #=> '"reference_test@example.com"'
336
+
337
+ # ========================================
338
+ # Round-trip Type Preservation Tests
339
+ # ========================================
340
+
341
+ ## Round-trip: integer preserves type through Horreum serialization
342
+ serialized = @customer.serialize_value(42)
343
+ @customer.deserialize_value(serialized)
344
+ #=> 42
345
+
346
+ ## Round-trip: integer class preserved
347
+ serialized = @customer.serialize_value(42)
348
+ @customer.deserialize_value(serialized).class
349
+ #=> Integer
350
+
351
+ ## Round-trip: boolean true preserves type
352
+ serialized = @customer.serialize_value(true)
353
+ @customer.deserialize_value(serialized)
354
+ #=> true
355
+
356
+ ## Round-trip: boolean true class preserved
357
+ serialized = @customer.serialize_value(true)
358
+ @customer.deserialize_value(serialized).class
359
+ #=> TrueClass
360
+
361
+ ## Round-trip: boolean false preserves type
362
+ serialized = @customer.serialize_value(false)
363
+ @customer.deserialize_value(serialized)
364
+ #=> false
365
+
366
+ ## Round-trip: nil preserves type
367
+ serialized = @customer.serialize_value(nil)
368
+ @customer.deserialize_value(serialized)
369
+ #=> nil
370
+
371
+ ## Round-trip: hash preserves structure
372
+ serialized = @customer.serialize_value({ active: true, count: 10 })
373
+ @customer.deserialize_value(serialized)
374
+ #=> {"active"=>true, "count"=>10}
375
+
376
+ ## Round-trip: array preserves structure and types
377
+ serialized = @customer.serialize_value([1, 'two', true, nil, 3.5])
378
+ @customer.deserialize_value(serialized)
379
+ #=> [1, "two", true, nil, 3.5]
380
+
381
+ # Cleanup
382
+ @bone.props.delete!
383
+ @bone.owners.delete!
384
+ @bone.tags.delete!
385
+ @bone.metrics.delete!
386
+ @customer.destroy! rescue nil
@@ -259,8 +259,9 @@ destroy_result = model.destroy!
259
259
 
260
260
  # Should result in success and also complete in a reasonable amount of
261
261
  # time (under 100ms for this test). I acknowledge this is flaky.
262
+ # Note: 42 results = 40 related field DELs + 1 main object DEL + 1 instances ZREM
262
263
  [destroy_result.class, destroy_result.successful?, destroy_result.results.size]
263
- #=> [MultiResult, true, 41]
264
+ #=> [MultiResult, true, 42]
264
265
  #=%> 100
265
266
 
266
267
  ## Verify transaction_fallback_integration_try.rb bug is fixed
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: familia
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre21
4
+ version: 2.0.0.pre23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -168,12 +168,6 @@ files:
168
168
  - bin/irb
169
169
  - bin/try
170
170
  - bin/tryouts
171
- - changelog.d/20251105_flexible_external_identifier_format.rst
172
- - changelog.d/20251107_112554_delano_179_participation_asymmetry.rst
173
- - changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst
174
- - changelog.d/20251107_fix_participates_in_symbol_resolution.rst
175
- - changelog.d/20251107_optimized_redis_exists_checks.rst
176
- - changelog.d/20251108_frozen_string_literal_pragma.rst
177
171
  - changelog.d/README.md
178
172
  - changelog.d/scriv.ini
179
173
  - docs/1106-participates_in-bidirectional-solution.md
@@ -224,6 +218,7 @@ files:
224
218
  - examples/safe_dump.rb
225
219
  - examples/sampling_demo.rb
226
220
  - examples/single_connection_transaction_confusions.rb
221
+ - examples/through_relationships.rb
227
222
  - familia.gemspec
228
223
  - lib/familia.rb
229
224
  - lib/familia/base.rb
@@ -281,6 +276,7 @@ files:
281
276
  - lib/familia/features/relationships/participation/participant_methods.rb
282
277
  - lib/familia/features/relationships/participation/rebuild_strategies.md
283
278
  - lib/familia/features/relationships/participation/target_methods.rb
279
+ - lib/familia/features/relationships/participation/through_model_operations.rb
284
280
  - lib/familia/features/relationships/participation_membership.rb
285
281
  - lib/familia/features/relationships/participation_relationship.rb
286
282
  - lib/familia/features/relationships/score_encoding.rb
@@ -328,6 +324,8 @@ files:
328
324
  - try/edge_cases/reserved_keywords_try.rb
329
325
  - try/edge_cases/string_coercion_try.rb
330
326
  - try/edge_cases/ttl_side_effects_try.rb
327
+ - try/features/count_any_edge_cases_try.rb
328
+ - try/features/count_any_methods_try.rb
331
329
  - try/features/encrypted_fields/aad_protection_try.rb
332
330
  - try/features/encrypted_fields/concealed_string_core_try.rb
333
331
  - try/features/encrypted_fields/context_isolation_try.rb
@@ -365,12 +363,14 @@ files:
365
363
  - try/features/relationships/indexing_commands_verification_try.rb
366
364
  - try/features/relationships/indexing_rebuild_try.rb
367
365
  - try/features/relationships/indexing_try.rb
368
- - try/features/relationships/participation_bidirectional_try.rb
369
366
  - try/features/relationships/participation_commands_verification_spec.rb
370
367
  - try/features/relationships/participation_commands_verification_try.rb
368
+ - try/features/relationships/participation_method_prefix_try.rb
371
369
  - try/features/relationships/participation_performance_improvements_try.rb
372
370
  - try/features/relationships/participation_reverse_index_try.rb
371
+ - try/features/relationships/participation_reverse_methods_try.rb
373
372
  - try/features/relationships/participation_target_class_resolution_try.rb
373
+ - try/features/relationships/participation_through_try.rb
374
374
  - try/features/relationships/participation_unresolved_target_try.rb
375
375
  - try/features/relationships/relationships_api_changes_try.rb
376
376
  - try/features/relationships/relationships_edge_cases_try.rb
@@ -505,6 +505,7 @@ files:
505
505
  - try/unit/data_types/hash_try.rb
506
506
  - try/unit/data_types/list_try.rb
507
507
  - try/unit/data_types/lock_try.rb
508
+ - try/unit/data_types/serialization_try.rb
508
509
  - try/unit/data_types/sorted_set_try.rb
509
510
  - try/unit/data_types/sorted_set_zadd_options_try.rb
510
511
  - try/unit/data_types/string_try.rb