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,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,
|
|
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.
|
|
4
|
+
version: 2.0.0.pre22
|
|
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
|
|
@@ -328,6 +322,8 @@ files:
|
|
|
328
322
|
- try/edge_cases/reserved_keywords_try.rb
|
|
329
323
|
- try/edge_cases/string_coercion_try.rb
|
|
330
324
|
- try/edge_cases/ttl_side_effects_try.rb
|
|
325
|
+
- try/features/count_any_edge_cases_try.rb
|
|
326
|
+
- try/features/count_any_methods_try.rb
|
|
331
327
|
- try/features/encrypted_fields/aad_protection_try.rb
|
|
332
328
|
- try/features/encrypted_fields/concealed_string_core_try.rb
|
|
333
329
|
- try/features/encrypted_fields/context_isolation_try.rb
|
|
@@ -505,6 +501,7 @@ files:
|
|
|
505
501
|
- try/unit/data_types/hash_try.rb
|
|
506
502
|
- try/unit/data_types/list_try.rb
|
|
507
503
|
- try/unit/data_types/lock_try.rb
|
|
504
|
+
- try/unit/data_types/serialization_try.rb
|
|
508
505
|
- try/unit/data_types/sorted_set_try.rb
|
|
509
506
|
- try/unit/data_types/sorted_set_zadd_options_try.rb
|
|
510
507
|
- try/unit/data_types/string_try.rb
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
.. Added
|
|
2
|
-
.. -----
|
|
3
|
-
|
|
4
|
-
.. Changed
|
|
5
|
-
.. -------
|
|
6
|
-
|
|
7
|
-
- **ExternalIdentifier Format Flexibility**: The `external_identifier` feature now supports customizable format templates via the `format` option. This allows you to control the entire format of generated external IDs, including the prefix, separator, and overall structure.
|
|
8
|
-
|
|
9
|
-
**Default format** (unchanged behavior):
|
|
10
|
-
|
|
11
|
-
.. code-block:: ruby
|
|
12
|
-
|
|
13
|
-
class User < Familia::Horreum
|
|
14
|
-
feature :external_identifier
|
|
15
|
-
end
|
|
16
|
-
user.extid # => "ext_abc123def456ghi789"
|
|
17
|
-
|
|
18
|
-
**Custom format with different prefix**:
|
|
19
|
-
|
|
20
|
-
.. code-block:: ruby
|
|
21
|
-
|
|
22
|
-
class Customer < Familia::Horreum
|
|
23
|
-
feature :external_identifier, format: 'cust_%{id}'
|
|
24
|
-
end
|
|
25
|
-
customer.extid # => "cust_abc123def456ghi789"
|
|
26
|
-
|
|
27
|
-
**Custom format with different separator**:
|
|
28
|
-
|
|
29
|
-
.. code-block:: ruby
|
|
30
|
-
|
|
31
|
-
class APIKey < Familia::Horreum
|
|
32
|
-
feature :external_identifier, format: 'api-%{id}'
|
|
33
|
-
end
|
|
34
|
-
key.extid # => "api-abc123def456ghi789"
|
|
35
|
-
|
|
36
|
-
**Custom format without traditional prefix**:
|
|
37
|
-
|
|
38
|
-
.. code-block:: ruby
|
|
39
|
-
|
|
40
|
-
class Resource < Familia::Horreum
|
|
41
|
-
feature :external_identifier, format: 'v2/%{id}'
|
|
42
|
-
end
|
|
43
|
-
resource.extid # => "v2/abc123def456ghi789"
|
|
44
|
-
|
|
45
|
-
The `format` option accepts a Ruby format string with the `%{id}` placeholder for the generated identifier. The default format is `'ext_%{id}'`. This provides complete flexibility for various ID formatting needs including different prefixes, separators (underscore, hyphen, slash), URL paths, or no prefix at all.
|
|
46
|
-
|
|
47
|
-
.. Deprecated
|
|
48
|
-
.. ----------
|
|
49
|
-
|
|
50
|
-
.. Removed
|
|
51
|
-
.. -------
|
|
52
|
-
|
|
53
|
-
.. Fixed
|
|
54
|
-
.. -----
|
|
55
|
-
|
|
56
|
-
.. Security
|
|
57
|
-
.. --------
|
|
58
|
-
|
|
59
|
-
.. Documentation
|
|
60
|
-
.. -------------
|
|
61
|
-
|
|
62
|
-
.. AI Assistance
|
|
63
|
-
.. -------------
|
|
64
|
-
|
|
65
|
-
- **Design Review**: Claude Code provided analysis of the current implementation and recommended several idiomatic Ruby approaches for format flexibility, ultimately suggesting the format template pattern using Ruby's native string formatting.
|
|
66
|
-
- **Implementation**: Claude Code implemented the format template feature including code changes, test cases, and documentation updates.
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
.. A new scriv changelog fragment.
|
|
2
|
-
..
|
|
3
|
-
.. Uncomment the section that is right (remove the leading dots).
|
|
4
|
-
.. For top level release notes, leave all the headers commented out.
|
|
5
|
-
..
|
|
6
|
-
Added
|
|
7
|
-
-----
|
|
8
|
-
|
|
9
|
-
- Bidirectional reverse collection methods for ``participates_in`` with ``_instances`` suffix (e.g., ``user.project_team_instances``, ``user.project_team_ids``). Supports union behavior for multiple collections and custom naming via ``as:`` parameter. Closes #179.
|
|
10
|
-
|
|
11
|
-
.. Changed
|
|
12
|
-
.. -------
|
|
13
|
-
..
|
|
14
|
-
.. - A bullet item for the Changed category.
|
|
15
|
-
..
|
|
16
|
-
.. Deprecated
|
|
17
|
-
.. ----------
|
|
18
|
-
..
|
|
19
|
-
.. - A bullet item for the Deprecated category.
|
|
20
|
-
..
|
|
21
|
-
.. Removed
|
|
22
|
-
.. -------
|
|
23
|
-
..
|
|
24
|
-
.. - A bullet item for the Removed category.
|
|
25
|
-
..
|
|
26
|
-
.. Fixed
|
|
27
|
-
.. -----
|
|
28
|
-
..
|
|
29
|
-
.. - A bullet item for the Fixed category.
|
|
30
|
-
..
|
|
31
|
-
.. Security
|
|
32
|
-
.. --------
|
|
33
|
-
..
|
|
34
|
-
.. - A bullet item for the Security category.
|
|
35
|
-
..
|
|
36
|
-
.. Documentation
|
|
37
|
-
.. -------------
|
|
38
|
-
..
|
|
39
|
-
.. - A bullet item for the Documentation category.
|
|
40
|
-
..
|
|
41
|
-
AI Assistance
|
|
42
|
-
-------------
|
|
43
|
-
|
|
44
|
-
- Claude Opus 4 assisted with implementation of bidirectional participation relationships using ``_instances`` suffix pattern. Pivoted from initial dry-inflector pluralization approach based on feedback.
|
data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
.. Fixed mutex race conditions in thread safety implementation
|
|
2
|
-
..
|
|
3
|
-
|
|
4
|
-
Fixed
|
|
5
|
-
-----
|
|
6
|
-
|
|
7
|
-
- Fixed critical race condition in mutex initialization for connection chain lazy loading. The mutex itself was being lazily initialized with ``||=``, which is not atomic and could result in multiple threads creating different mutex instances, defeating synchronization. Changed to eager initialization via ``Connection.included`` hook. (`lib/familia/horreum/connection.rb`)
|
|
8
|
-
|
|
9
|
-
- Fixed critical race condition in mutex initialization for logger lazy loading. Similar to connection chain issue, the logger mutex was lazily initialized with ``||=``. Changed to eager initialization at module definition time. (`lib/familia/logging.rb`)
|
|
10
|
-
|
|
11
|
-
- Fixed logger assignment atomicity issue where ``Familia.logger=`` set ``DatabaseLogger.logger`` outside the mutex synchronization block, potentially causing ``Familia.logger`` and ``DatabaseLogger.logger`` to be temporarily out of sync during concurrent access. Moved ``DatabaseLogger.logger`` assignment inside the synchronization block. (`lib/familia/logging.rb`)
|
|
12
|
-
|
|
13
|
-
- Added explicit return statement to ``Familia.logger`` method for robustness against future refactoring. (`lib/familia/logging.rb`)
|
|
14
|
-
|
|
15
|
-
AI Assistance
|
|
16
|
-
-------------
|
|
17
|
-
|
|
18
|
-
- Code review analysis to identify critical race conditions in mutex initialization
|
|
19
|
-
- Implementation of proper eager mutex initialization patterns
|
|
20
|
-
- Test file updates to reflect new initialization approach
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
.. Added
|
|
2
|
-
.. -----
|
|
3
|
-
|
|
4
|
-
.. Changed
|
|
5
|
-
.. -------
|
|
6
|
-
|
|
7
|
-
.. Deprecated
|
|
8
|
-
.. ----------
|
|
9
|
-
|
|
10
|
-
.. Removed
|
|
11
|
-
.. -------
|
|
12
|
-
|
|
13
|
-
.. Fixed
|
|
14
|
-
.. -----
|
|
15
|
-
|
|
16
|
-
- **Participation Relationships with Symbol/String Target Classes**: Fixed four bugs that occurred when calling `participates_in` with a Symbol or String target class instead of a Class object.
|
|
17
|
-
|
|
18
|
-
**Bug 1 - NoMethodError during relationship definition**:
|
|
19
|
-
|
|
20
|
-
The error was: ``private method 'member_by_config_name' called for module Familia``.
|
|
21
|
-
|
|
22
|
-
**Background**: The `participates_in` method supports flexible target class specifications:
|
|
23
|
-
|
|
24
|
-
.. code-block:: ruby
|
|
25
|
-
|
|
26
|
-
class Domain < Familia::Horreum
|
|
27
|
-
# All three forms should work:
|
|
28
|
-
participates_in Customer, :domains # Class object (always worked)
|
|
29
|
-
participates_in :Customer, :domains # Symbol (was broken)
|
|
30
|
-
participates_in 'Customer', :domains # String (was broken)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
**Root Cause**: The method had redundant class resolution code that directly called the private `Familia.member_by_config_name` method instead of using the public `Familia.resolve_class` API.
|
|
34
|
-
|
|
35
|
-
**Solution**: Removed the redundant resolution code and now uses the already-resolved class from the public API, simplifying the implementation and fixing the visibility issue.
|
|
36
|
-
|
|
37
|
-
**Bug 2 - NoMethodError in current_participations**:
|
|
38
|
-
|
|
39
|
-
When calling `current_participations` on objects that used Symbol/String target classes, it would fail with ``undefined method 'familia_name' for Symbol``.
|
|
40
|
-
|
|
41
|
-
**Root Cause**: The `current_participations` method was calling `.familia_name` on `config.target_class`, which stores the original Symbol/String value passed to `participates_in`.
|
|
42
|
-
|
|
43
|
-
**Solution**: Use the resolved `target_class` variable instead of the stored config value. The resolved class is already available from the `Familia.resolve_class` call earlier in the method.
|
|
44
|
-
|
|
45
|
-
**Bug 3 - NoMethodError in target_class_config_name**:
|
|
46
|
-
|
|
47
|
-
When calling `current_participations`, the internal `target_class_config_name` method would fail with ``undefined method 'config_name' for Symbol``.
|
|
48
|
-
|
|
49
|
-
**Root Cause**: The `ParticipationRelationship.target_class_config_name` method was calling `.config_name` directly on the stored `target_class` value, which could be a Symbol or String.
|
|
50
|
-
|
|
51
|
-
**Solution**: Resolve the target class before calling `config_name` by using `Familia.resolve_class(target_class)`, which handles all input types (Class, Symbol, String) correctly.
|
|
52
|
-
|
|
53
|
-
**Bug 4 - Confusing error when target class not loaded**:
|
|
54
|
-
|
|
55
|
-
When the target class hasn't been loaded yet (load order issue), the error was: ``undefined method 'method_defined?' for nil``.
|
|
56
|
-
|
|
57
|
-
**Root Cause**: When `Familia.resolve_class` returns `nil` (because the target class isn't registered in `Familia.members` yet), the code would pass `nil` to `TargetMethods::Builder.build`, which then failed with a confusing error message that didn't explain the actual problem.
|
|
58
|
-
|
|
59
|
-
**Solution**: Added explicit nil check after `resolve_class` with a detailed ArgumentError that:
|
|
60
|
-
|
|
61
|
-
- Clearly states which target class couldn't be resolved
|
|
62
|
-
- Lists the three most common causes (load order, typo, not inheriting from Horreum)
|
|
63
|
-
- Shows all currently registered Familia classes for debugging
|
|
64
|
-
- Provides a clear solution for fixing the load order
|
|
65
|
-
|
|
66
|
-
**Impact**: Projects using Symbol or String target classes in `participates_in` declarations will now work correctly throughout the entire lifecycle, including relationship definition, method generation, and participation queries. When there's a load order issue or typo, developers get a clear, actionable error message instead of a confusing nil error. This pattern is common when avoiding circular dependencies or when target classes are defined in different files.
|
|
67
|
-
|
|
68
|
-
.. Security
|
|
69
|
-
.. --------
|
|
70
|
-
|
|
71
|
-
.. Documentation
|
|
72
|
-
.. -------------
|
|
73
|
-
|
|
74
|
-
.. AI Assistance
|
|
75
|
-
.. -------------
|
|
76
|
-
|
|
77
|
-
- **Root Cause Analysis**: Claude Code analyzed the error stack trace from the implementing project and identified that a private method was being called as a public method from outside the Familia module.
|
|
78
|
-
- **Fix Implementation**: Claude Code identified redundant class resolution code and simplified it to use the already-resolved class from the public API.
|
|
79
|
-
- **Test Coverage**: Claude Code created comprehensive regression tests including:
|
|
80
|
-
|
|
81
|
-
- Feature-level tests for Symbol/String target class resolution in participation relationships
|
|
82
|
-
- Unit tests for the `Familia.resolve_class` public API
|
|
83
|
-
- Edge case coverage for case-insensitive resolution and modularized classes
|
|
84
|
-
|
|
85
|
-
- **Second Bug Discovery**: During test execution, Claude Code discovered a related bug in `current_participations` that was also failing with Symbol/String target classes. The test coverage revealed that `.familia_name` was being called on the unresolved config value instead of the resolved class instance.
|
|
86
|
-
|
|
87
|
-
- **Third Bug Discovery**: Further test execution revealed another Symbol/String bug in `target_class_config_name`, where `.config_name` was being called directly on Symbol/String values. This was fixed by resolving the class first using `Familia.resolve_class`.
|
|
88
|
-
|
|
89
|
-
- **Test Coverage Refinement**: Claude Code identified and removed unrealistic test cases (all-uppercase, all-lowercase class names) that don't occur in real Ruby code and don't work with the `snake_case` method's design. Updated tests to focus on realistic naming conventions: PascalCase and snake_case, with clear documentation explaining why certain formats aren't supported.
|
|
90
|
-
|
|
91
|
-
- **Fourth Bug Discovery**: After merging to main, the implementing project revealed a load order issue where `Familia.resolve_class` returned `nil`, causing a confusing "undefined method for nil" error. Claude Code added explicit error handling with a detailed, actionable error message that helps developers quickly identify and fix load order issues, typos, or inheritance problems.
|