familia 2.0.0.pre24 → 2.0.0.pre26

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.rst +57 -0
  4. data/CLAUDE.md +1 -1
  5. data/Gemfile.lock +1 -1
  6. data/docs/guides/feature-relationships-indexing.md +104 -9
  7. data/docs/guides/feature-relationships-methods.md +37 -5
  8. data/docs/overview.md +9 -0
  9. data/lib/familia/base.rb +0 -2
  10. data/lib/familia/data_type/serialization.rb +8 -9
  11. data/lib/familia/data_type/settings.rb +0 -8
  12. data/lib/familia/data_type/types/json_stringkey.rb +155 -0
  13. data/lib/familia/data_type.rb +5 -4
  14. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +281 -15
  15. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +2 -1
  16. data/lib/familia/features/relationships/indexing.rb +57 -27
  17. data/lib/familia/features/relationships/participation/through_model_operations.rb +4 -3
  18. data/lib/familia/features/relationships/participation.rb +6 -6
  19. data/lib/familia/features/safe_dump.rb +0 -3
  20. data/lib/familia/horreum/management.rb +29 -0
  21. data/lib/familia/horreum/persistence.rb +4 -1
  22. data/lib/familia/horreum/settings.rb +2 -10
  23. data/lib/familia/horreum.rb +1 -2
  24. data/lib/familia/version.rb +1 -1
  25. data/try/features/relationships/class_level_multi_index_auto_try.rb +318 -0
  26. data/try/features/relationships/class_level_multi_index_rebuild_try.rb +393 -0
  27. data/try/features/relationships/class_level_multi_index_try.rb +349 -0
  28. data/try/features/relationships/prefix_vs_config_name_try.rb +418 -0
  29. data/try/integration/familia_extended_try.rb +1 -1
  30. data/try/integration/scenarios_try.rb +4 -3
  31. data/try/unit/data_types/json_stringkey_try.rb +431 -0
  32. data/try/unit/horreum/settings_try.rb +0 -11
  33. metadata +7 -1
@@ -0,0 +1,418 @@
1
+ # try/features/relationships/prefix_vs_config_name_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Tests for the bug fix where reverse lookup methods now correctly use `prefix`
6
+ # instead of `config_name` for Redis key matching.
7
+ #
8
+ # Background: When a class declares an explicit `prefix` that differs from its
9
+ # computed `config_name`, reverse lookups (like *_instances, *_ids) would fail
10
+ # to find the correct keys because they were matching against `config_name`
11
+ # instead of `prefix`.
12
+ #
13
+ # Example: CustomDomain with `prefix :customdomain` (no underscore) vs
14
+ # `config_name` returning "custom_domain" (with underscore)
15
+
16
+ require_relative '../../support/helpers/test_helpers'
17
+
18
+ Familia.debug = false
19
+
20
+ # Scenario 1: Default prefix (no explicit prefix set)
21
+ # prefix should equal config_name.to_sym
22
+ class ::PrefixTestSimpleModel < Familia::Horreum
23
+ feature :relationships
24
+
25
+ identifier_field :model_id
26
+ field :model_id
27
+ field :name
28
+
29
+ sorted_set :participants
30
+ end
31
+
32
+ class ::PrefixTestSimpleParticipant < Familia::Horreum
33
+ feature :relationships
34
+
35
+ identifier_field :participant_id
36
+ field :participant_id
37
+ field :created_at
38
+
39
+ participates_in PrefixTestSimpleModel, :participants, score: :created_at
40
+ end
41
+
42
+ # Scenario 2: Explicit prefix matching config_name
43
+ # Should work exactly the same as default
44
+ class ::PrefixMatchingTeam < Familia::Horreum
45
+ feature :relationships
46
+ prefix :prefix_matching_team # Matches config_name
47
+
48
+ identifier_field :team_id
49
+ field :team_id
50
+ field :name
51
+
52
+ sorted_set :members
53
+ end
54
+
55
+ class ::PrefixMatchingMember < Familia::Horreum
56
+ feature :relationships
57
+
58
+ identifier_field :member_id
59
+ field :member_id
60
+ field :joined_at
61
+
62
+ participates_in PrefixMatchingTeam, :members, score: :joined_at
63
+ end
64
+
65
+ # Scenario 3: THE BUG CASE - Explicit prefix differs from config_name
66
+ # CustomDomain: config_name = "custom_domain", prefix = :customdomain
67
+ class ::PrefixMismatchedDomain < Familia::Horreum
68
+ feature :relationships
69
+ prefix :mismatcheddomain # No underscore - differs from config_name "prefix_mismatched_domain"
70
+
71
+ identifier_field :domain_id
72
+ field :domain_id
73
+ field :display_domain
74
+ field :created_at
75
+
76
+ participates_in PrefixTestSimpleModel, :participants, score: :created_at
77
+ end
78
+
79
+ # Scenario 4: Another mismatched prefix case - APIKey pattern
80
+ # APIKey: config_name = "api_key", prefix = :apikey
81
+ class ::PrefixTestAPIKey < Familia::Horreum
82
+ feature :relationships
83
+ prefix :ptapikey # Differs from config_name "prefix_test_a_p_i_key"
84
+
85
+ identifier_field :key_id
86
+ field :key_id
87
+ field :created_at
88
+
89
+ sorted_set :authorized_resources
90
+ end
91
+
92
+ class ::PrefixTestAPIResource < Familia::Horreum
93
+ feature :relationships
94
+
95
+ identifier_field :resource_id
96
+ field :resource_id
97
+ field :name
98
+ field :created_at
99
+
100
+ participates_in PrefixTestAPIKey, :authorized_resources, score: :created_at
101
+ end
102
+
103
+ # Scenario 5: Namespaced class with explicit prefix
104
+ # Tests that demodularization doesn't interfere
105
+ module ::PrefixTestNS
106
+ class CustomDomain < Familia::Horreum
107
+ feature :relationships
108
+ prefix :ptnscustomdomain # Explicit, doesn't match "custom_domain"
109
+
110
+ identifier_field :domain_id
111
+ field :domain_id
112
+ field :created_at
113
+
114
+ participates_in PrefixTestSimpleModel, :participants, score: :created_at
115
+ end
116
+ end
117
+
118
+ # Scenario 6: OAuth2Provider-like - complex snake_case edge case
119
+ # config_name would be "o_auth2provider"
120
+ class ::PrefixTestOAuthProvider < Familia::Horreum
121
+ feature :relationships
122
+ prefix :ptoauthprovider # Differs from config_name
123
+
124
+ identifier_field :provider_id
125
+ field :provider_id
126
+ field :name
127
+
128
+ sorted_set :connected_users
129
+ end
130
+
131
+ class ::PrefixTestOAuthUser < Familia::Horreum
132
+ feature :relationships
133
+
134
+ identifier_field :user_id
135
+ field :user_id
136
+ field :connected_at
137
+
138
+ participates_in PrefixTestOAuthProvider, :connected_users, score: :connected_at
139
+ end
140
+
141
+ # Setup test instances
142
+ @simple_model = PrefixTestSimpleModel.new(model_id: 'model_1', name: 'Test Model')
143
+ @simple_participant = PrefixTestSimpleParticipant.new(participant_id: 'part_1', created_at: Familia.now.to_i)
144
+
145
+ @matching_team = PrefixMatchingTeam.new(team_id: 'team_1', name: 'Engineering')
146
+ @matching_member = PrefixMatchingMember.new(member_id: 'member_1', joined_at: Familia.now.to_i)
147
+
148
+ @mismatched_domain = PrefixMismatchedDomain.new(
149
+ domain_id: 'cd_1',
150
+ display_domain: 'example.com',
151
+ created_at: Familia.now.to_i
152
+ )
153
+
154
+ @api_key = PrefixTestAPIKey.new(key_id: 'key_1', created_at: Familia.now.to_i)
155
+ @api_resource = PrefixTestAPIResource.new(
156
+ resource_id: 'res_1',
157
+ name: 'Protected Resource',
158
+ created_at: Familia.now.to_i
159
+ )
160
+
161
+ @ns_domain = PrefixTestNS::CustomDomain.new(domain_id: 'otcd_1', created_at: Familia.now.to_i)
162
+
163
+ @oauth_provider = PrefixTestOAuthProvider.new(provider_id: 'oauth_1', name: 'Google')
164
+ @oauth_user = PrefixTestOAuthUser.new(user_id: 'ouser_1', connected_at: Familia.now.to_i)
165
+
166
+ # Scenario 1: Default prefix (no explicit prefix set)
167
+
168
+ ## PrefixTestSimpleModel prefix equals config_name as symbol (default behavior)
169
+ PrefixTestSimpleModel.prefix
170
+ #=> :prefix_test_simple_model
171
+
172
+ ## PrefixTestSimpleModel config_name is snake_case string
173
+ PrefixTestSimpleModel.config_name
174
+ #=> "prefix_test_simple_model"
175
+
176
+ ## Default prefix matches config_name when converted
177
+ PrefixTestSimpleModel.prefix.to_s == PrefixTestSimpleModel.config_name
178
+ #=> true
179
+
180
+ ## PrefixTestSimpleParticipant also has default prefix
181
+ PrefixTestSimpleParticipant.prefix
182
+ #=> :prefix_test_simple_participant
183
+
184
+ # Scenario 2: Explicit prefix matching config_name
185
+
186
+ ## PrefixMatchingTeam has explicit prefix that matches config_name
187
+ PrefixMatchingTeam.prefix
188
+ #=> :prefix_matching_team
189
+
190
+ ## PrefixMatchingTeam config_name matches prefix
191
+ PrefixMatchingTeam.config_name
192
+ #=> "prefix_matching_team"
193
+
194
+ ## Matching prefix and config_name work the same
195
+ PrefixMatchingTeam.prefix.to_s == PrefixMatchingTeam.config_name
196
+ #=> true
197
+
198
+ # Scenario 3: THE BUG CASE - Explicit prefix differs from config_name
199
+
200
+ ## PrefixMismatchedDomain has explicit prefix without underscore
201
+ PrefixMismatchedDomain.prefix
202
+ #=> :mismatcheddomain
203
+
204
+ ## PrefixMismatchedDomain config_name has underscores (snake_case)
205
+ PrefixMismatchedDomain.config_name
206
+ #=> "prefix_mismatched_domain"
207
+
208
+ ## PrefixMismatchedDomain prefix differs from config_name
209
+ PrefixMismatchedDomain.prefix.to_s == PrefixMismatchedDomain.config_name
210
+ #=> false
211
+
212
+ # Scenario 4: PrefixTestAPIKey prefix edge case
213
+
214
+ ## PrefixTestAPIKey has explicit prefix
215
+ PrefixTestAPIKey.prefix
216
+ #=> :ptapikey
217
+
218
+ ## PrefixTestAPIKey config_name follows snake_case convention
219
+ PrefixTestAPIKey.config_name
220
+ #=> "prefix_test_api_key"
221
+
222
+ ## PrefixTestAPIKey prefix differs from config_name
223
+ PrefixTestAPIKey.prefix.to_s == PrefixTestAPIKey.config_name
224
+ #=> false
225
+
226
+ # Scenario 5: Namespaced class verification
227
+
228
+ ## Namespaced PrefixTestNS::CustomDomain has explicit prefix
229
+ PrefixTestNS::CustomDomain.prefix
230
+ #=> :ptnscustomdomain
231
+
232
+ ## Namespaced config_name only uses demodularized name
233
+ PrefixTestNS::CustomDomain.config_name
234
+ #=> "custom_domain"
235
+
236
+ ## Namespaced prefix is distinct from config_name
237
+ PrefixTestNS::CustomDomain.prefix.to_s == PrefixTestNS::CustomDomain.config_name
238
+ #=> false
239
+
240
+ # Scenario 6: PrefixTestOAuthProvider edge case
241
+
242
+ ## PrefixTestOAuthProvider has explicit prefix
243
+ PrefixTestOAuthProvider.prefix
244
+ #=> :ptoauthprovider
245
+
246
+ ## PrefixTestOAuthProvider snake_case config_name
247
+ PrefixTestOAuthProvider.config_name
248
+ #=> "prefix_test_o_auth_provider"
249
+
250
+ ## PrefixTestOAuthProvider prefix differs from config_name
251
+ PrefixTestOAuthProvider.prefix.to_s == PrefixTestOAuthProvider.config_name
252
+ #=> false
253
+
254
+ # Functional tests: Reverse lookups with mismatched prefix/config_name
255
+
256
+ ## Save all test instances for functional tests
257
+ [@simple_model, @simple_participant, @matching_team, @matching_member,
258
+ @mismatched_domain, @api_key, @api_resource, @ns_domain,
259
+ @oauth_provider, @oauth_user].each(&:save)
260
+ true
261
+ #=> true
262
+
263
+ ## Add PrefixTestSimpleParticipant to PrefixTestSimpleModel (default prefix case)
264
+ @simple_participant.add_to_prefix_test_simple_model_participants(@simple_model)
265
+ @simple_participant.in_prefix_test_simple_model_participants?(@simple_model)
266
+ #=> true
267
+
268
+ ## PrefixTestSimpleParticipant reverse lookup finds PrefixTestSimpleModel instances
269
+ @simple_participant.prefix_test_simple_model_instances.map(&:identifier)
270
+ #=> ["model_1"]
271
+
272
+ ## Add PrefixMatchingMember to PrefixMatchingTeam (matching prefix case)
273
+ @matching_member.add_to_prefix_matching_team_members(@matching_team)
274
+ @matching_member.in_prefix_matching_team_members?(@matching_team)
275
+ #=> true
276
+
277
+ ## PrefixMatchingMember reverse lookup finds PrefixMatchingTeam instances
278
+ @matching_member.prefix_matching_team_instances.map(&:identifier)
279
+ #=> ["team_1"]
280
+
281
+ ## Add PrefixMismatchedDomain to PrefixTestSimpleModel (THE BUG CASE - mismatched prefix)
282
+ @mismatched_domain.add_to_prefix_test_simple_model_participants(@simple_model)
283
+ @mismatched_domain.in_prefix_test_simple_model_participants?(@simple_model)
284
+ #=> true
285
+
286
+ ## PrefixMismatchedDomain reverse lookup must find PrefixTestSimpleModel via prefix not config_name
287
+ @mismatched_domain.prefix_test_simple_model_instances.map(&:identifier)
288
+ #=> ["model_1"]
289
+
290
+ ## PrefixMismatchedDomain ids method also works
291
+ @mismatched_domain.prefix_test_simple_model_ids
292
+ #=> ["model_1"]
293
+
294
+ ## PrefixMismatchedDomain boolean check works
295
+ @mismatched_domain.prefix_test_simple_model?
296
+ #=> true
297
+
298
+ ## PrefixMismatchedDomain count works
299
+ @mismatched_domain.prefix_test_simple_model_count
300
+ #=> 1
301
+
302
+ ## Add PrefixTestAPIResource to PrefixTestAPIKey (mismatched prefix case)
303
+ @api_resource.add_to_prefix_test_api_key_authorized_resources(@api_key)
304
+ @api_resource.in_prefix_test_api_key_authorized_resources?(@api_key)
305
+ #=> true
306
+
307
+ ## PrefixTestAPIResource reverse lookup finds PrefixTestAPIKey via prefix not config_name
308
+ @api_resource.prefix_test_api_key_instances.map(&:identifier)
309
+ #=> ["key_1"]
310
+
311
+ ## Add PrefixTestNS::CustomDomain to PrefixTestSimpleModel (namespaced with mismatched prefix)
312
+ @ns_domain.add_to_prefix_test_simple_model_participants(@simple_model)
313
+ @ns_domain.in_prefix_test_simple_model_participants?(@simple_model)
314
+ #=> true
315
+
316
+ ## PrefixTestNS::CustomDomain reverse lookup finds PrefixTestSimpleModel
317
+ @ns_domain.prefix_test_simple_model_instances.map(&:identifier)
318
+ #=> ["model_1"]
319
+
320
+ ## Add PrefixTestOAuthUser to PrefixTestOAuthProvider (mismatched prefix case)
321
+ @oauth_user.add_to_prefix_test_o_auth_provider_connected_users(@oauth_provider)
322
+ @oauth_user.in_prefix_test_o_auth_provider_connected_users?(@oauth_provider)
323
+ #=> true
324
+
325
+ ## PrefixTestOAuthUser reverse lookup finds PrefixTestOAuthProvider via prefix
326
+ @oauth_user.prefix_test_o_auth_provider_instances.map(&:identifier)
327
+ #=> ["oauth_1"]
328
+
329
+ # Verify dbkey format uses prefix, not config_name
330
+
331
+ ## PrefixTestSimpleModel dbkey uses default prefix (which equals config_name)
332
+ @simple_model.dbkey.start_with?("prefix_test_simple_model:")
333
+ #=> true
334
+
335
+ ## PrefixMismatchedDomain dbkey uses explicit prefix
336
+ @mismatched_domain.dbkey.start_with?("mismatcheddomain:")
337
+ #=> true
338
+
339
+ ## PrefixTestAPIKey dbkey uses explicit prefix
340
+ @api_key.dbkey.start_with?("ptapikey:")
341
+ #=> true
342
+
343
+ ## PrefixTestNS::CustomDomain dbkey uses explicit prefix
344
+ @ns_domain.dbkey.start_with?("ptnscustomdomain:")
345
+ #=> true
346
+
347
+ ## PrefixTestOAuthProvider dbkey uses explicit prefix
348
+ @oauth_provider.dbkey.start_with?("ptoauthprovider:")
349
+ #=> true
350
+
351
+ # Verify participation tracking uses prefix in keys
352
+
353
+ ## PrefixTestSimpleParticipant participations include key with prefix_test_simple_model prefix
354
+ @simple_participant_keys = @simple_participant.participations.members
355
+ @simple_participant_keys.any? { |k| k.start_with?("prefix_test_simple_model:") }
356
+ #=> true
357
+
358
+ ## PrefixMismatchedDomain participations include key with prefix_test_simple_model prefix
359
+ @mismatched_domain_keys = @mismatched_domain.participations.members
360
+ @mismatched_domain_keys.any? { |k| k.start_with?("prefix_test_simple_model:") }
361
+ #=> true
362
+
363
+ ## PrefixTestAPIResource participations include key with ptapikey prefix (not prefix_test_api_key)
364
+ @api_resource_keys = @api_resource.participations.members
365
+ @api_resource_keys.any? { |k| k.start_with?("ptapikey:") }
366
+ #=> true
367
+
368
+ ## PrefixTestOAuthUser participations include key with ptoauthprovider prefix
369
+ @oauth_user_keys = @oauth_user.participations.members
370
+ @oauth_user_keys.any? { |k| k.start_with?("ptoauthprovider:") }
371
+ #=> true
372
+
373
+ # Bug fix verification: Keys use prefix, NOT config_name
374
+ # If the old bug existed, these tests would fail because config_name would be
375
+ # used for matching but keys are stored with prefix
376
+
377
+ ## PrefixTestAPIResource keys do NOT contain config_name pattern (prefix_test_api_key)
378
+ # This verifies that the key is "ptapikey:..." not "prefix_test_api_key:..."
379
+ @api_resource_keys.none? { |k| k.start_with?("prefix_test_api_key:") }
380
+ #=> true
381
+
382
+ ## PrefixTestOAuthUser keys do NOT contain config_name pattern
383
+ # This verifies that the key is "ptoauthprovider:..." not "prefix_test_o_auth_provider:..."
384
+ @oauth_user_keys.none? { |k| k.start_with?("prefix_test_o_auth_provider:") }
385
+ #=> true
386
+
387
+ ## PrefixTestAPIKey prefix is different from config_name
388
+ # This is the critical condition for the bug: prefix != config_name
389
+ PrefixTestAPIKey.prefix.to_s != PrefixTestAPIKey.config_name
390
+ #=> true
391
+
392
+ ## PrefixTestOAuthProvider prefix is different from config_name
393
+ PrefixTestOAuthProvider.prefix.to_s != PrefixTestOAuthProvider.config_name
394
+ #=> true
395
+
396
+ ## participating_ids_for_target finds IDs when prefix differs from config_name
397
+ # This is the core bug fix test - the method must use prefix, not config_name
398
+ @api_resource.participating_ids_for_target(PrefixTestAPIKey).include?("key_1")
399
+ #=> true
400
+
401
+ ## participating_in_target? returns true when prefix differs from config_name
402
+ @api_resource.participating_in_target?(PrefixTestAPIKey)
403
+ #=> true
404
+
405
+ ## PrefixTestOAuthUser also finds its target despite prefix != config_name
406
+ @oauth_user.participating_ids_for_target(PrefixTestOAuthProvider).include?("oauth_1")
407
+ #=> true
408
+
409
+ ## PrefixTestOAuthUser participating_in_target? also works
410
+ @oauth_user.participating_in_target?(PrefixTestOAuthProvider)
411
+ #=> true
412
+
413
+ ## Cleanup test data completes without errors
414
+ [@simple_model, @simple_participant, @matching_team, @matching_member,
415
+ @mismatched_domain, @api_key, @api_resource, @ns_domain,
416
+ @oauth_provider, @oauth_user].compact.each { |obj| obj.destroy if obj.respond_to?(:destroy) && obj.exists? }
417
+ true
418
+ #=> true
@@ -11,7 +11,7 @@ require_relative '../support/helpers/test_helpers'
11
11
  ## Has all datatype relativess
12
12
  registered_types = Familia::DataType.registered_types.keys
13
13
  registered_types.collect(&:to_s).sort
14
- #=> ["counter", "hash", "hashkey", "list", "listkey", "lock", "set", "sorted_set", "string", "stringkey", "unsorted_set", "zset"]
14
+ #=> ["counter", "hash", "hashkey", "json_string", "json_stringkey", "jsonkey", "list", "listkey", "lock", "set", "sorted_set", "string", "stringkey", "unsorted_set", "zset"]
15
15
 
16
16
  ## Familia created class methods for datatype list class
17
17
  Familia::Horreum::DefinitionMethods.public_method_defined? :list?
@@ -65,7 +65,7 @@ rescue StandardError => e
65
65
  end
66
66
  #=> false
67
67
 
68
- ## serialization method configuration methods exist
68
+ ## JSON serialization is hard-coded for consistency
69
69
  begin
70
70
  custom_class = Class.new(Familia::Horreum) do
71
71
  identifier_field :id
@@ -73,8 +73,9 @@ begin
73
73
  field :data
74
74
  end
75
75
 
76
- # Check if these methods exist
77
- custom_class.respond_to?(:dump_method) && custom_class.respond_to?(:load_method)
76
+ # Verify that custom serialization methods have been removed
77
+ # dump_method and load_method are no longer available
78
+ !custom_class.respond_to?(:dump_method) && !custom_class.respond_to?(:load_method)
78
79
  rescue StandardError => e
79
80
  false
80
81
  end