familia 2.0.0.pre21 → 2.0.0.pre22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.talismanrc +5 -1
  3. data/CHANGELOG.rst +43 -0
  4. data/Gemfile.lock +1 -1
  5. data/lib/familia/connection/operation_core.rb +1 -2
  6. data/lib/familia/connection/pipelined_core.rb +1 -3
  7. data/lib/familia/connection/transaction_core.rb +1 -2
  8. data/lib/familia/data_type/serialization.rb +76 -51
  9. data/lib/familia/data_type/types/sorted_set.rb +5 -10
  10. data/lib/familia/data_type/types/stringkey.rb +22 -0
  11. data/lib/familia/features/external_identifier.rb +29 -0
  12. data/lib/familia/features/object_identifier.rb +47 -0
  13. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +15 -15
  14. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +8 -0
  15. data/lib/familia/horreum/database_commands.rb +6 -1
  16. data/lib/familia/horreum/management.rb +141 -10
  17. data/lib/familia/horreum/persistence.rb +3 -0
  18. data/lib/familia/identifier_extractor.rb +1 -1
  19. data/lib/familia/version.rb +1 -1
  20. data/lib/multi_result.rb +59 -31
  21. data/try/features/count_any_edge_cases_try.rb +486 -0
  22. data/try/features/count_any_methods_try.rb +197 -0
  23. data/try/features/external_identifier/external_identifier_try.rb +134 -0
  24. data/try/features/object_identifier/object_identifier_try.rb +138 -0
  25. data/try/features/relationships/indexing_rebuild_try.rb +6 -0
  26. data/try/integration/data_types/datatype_pipelines_try.rb +5 -3
  27. data/try/integration/data_types/datatype_transactions_try.rb +13 -7
  28. data/try/integration/models/customer_try.rb +3 -3
  29. data/try/unit/data_types/boolean_try.rb +35 -22
  30. data/try/unit/data_types/hash_try.rb +2 -2
  31. data/try/unit/data_types/serialization_try.rb +386 -0
  32. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +2 -1
  33. metadata +4 -7
  34. data/changelog.d/20251105_flexible_external_identifier_format.rst +0 -66
  35. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +0 -44
  36. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +0 -20
  37. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +0 -91
  38. data/changelog.d/20251107_optimized_redis_exists_checks.rst +0 -94
  39. data/changelog.d/20251108_frozen_string_literal_pragma.rst +0 -44
@@ -0,0 +1,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.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.
@@ -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.