familia 2.0.0.pre14 → 2.0.0.pre16

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 (276) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +66 -6
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +4 -4
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre13.md +1 -1
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +623 -19
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +6 -6
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +49 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/commands.rb +53 -51
  65. data/lib/familia/data_type/serialization.rb +108 -107
  66. data/lib/familia/data_type/types/counter.rb +1 -1
  67. data/lib/familia/data_type/types/hashkey.rb +13 -10
  68. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  69. data/lib/familia/data_type/types/lock.rb +3 -2
  70. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  71. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  72. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  73. data/lib/familia/data_type.rb +75 -47
  74. data/lib/familia/distinguisher.rb +85 -0
  75. data/lib/familia/encryption/encrypted_data.rb +15 -24
  76. data/lib/familia/encryption/manager.rb +6 -4
  77. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  78. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  79. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  80. data/lib/familia/encryption/request_cache.rb +7 -7
  81. data/lib/familia/encryption.rb +2 -3
  82. data/lib/familia/errors.rb +9 -3
  83. data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
  84. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  85. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  86. data/lib/familia/features/encrypted_fields.rb +68 -66
  87. data/lib/familia/features/expiration/extensions.rb +61 -0
  88. data/lib/familia/features/expiration.rb +35 -87
  89. data/lib/familia/features/external_identifier.rb +11 -12
  90. data/lib/familia/features/object_identifier.rb +58 -20
  91. data/lib/familia/features/quantization.rb +17 -22
  92. data/lib/familia/features/relationships/README.md +97 -0
  93. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  94. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  96. data/lib/familia/features/relationships/indexing.rb +176 -256
  97. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  98. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  100. data/lib/familia/features/relationships/participation.rb +656 -0
  101. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  102. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  103. data/lib/familia/features/relationships.rb +69 -271
  104. data/lib/familia/features/safe_dump.rb +127 -132
  105. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  106. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  107. data/lib/familia/features/transient_fields.rb +5 -5
  108. data/lib/familia/features.rb +21 -21
  109. data/lib/familia/field_type.rb +24 -4
  110. data/lib/familia/horreum/core/connection.rb +229 -26
  111. data/lib/familia/horreum/core/database_commands.rb +27 -17
  112. data/lib/familia/horreum/core/serialization.rb +40 -20
  113. data/lib/familia/horreum/core/utils.rb +2 -1
  114. data/lib/familia/horreum/shared/settings.rb +2 -1
  115. data/lib/familia/horreum/subclass/definition.rb +33 -45
  116. data/lib/familia/horreum/subclass/management.rb +72 -24
  117. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  118. data/lib/familia/horreum.rb +196 -114
  119. data/lib/familia/json_serializer.rb +0 -1
  120. data/lib/familia/logging.rb +11 -114
  121. data/lib/familia/refinements/dear_json.rb +122 -0
  122. data/lib/familia/refinements/logger_trace.rb +20 -17
  123. data/lib/familia/refinements/stylize_words.rb +65 -0
  124. data/lib/familia/refinements/time_literals.rb +60 -52
  125. data/lib/familia/refinements.rb +2 -1
  126. data/lib/familia/secure_identifier.rb +60 -28
  127. data/lib/familia/settings.rb +83 -7
  128. data/lib/familia/utils.rb +5 -87
  129. data/lib/familia/verifiable_identifier.rb +4 -4
  130. data/lib/familia/version.rb +1 -1
  131. data/lib/familia.rb +72 -15
  132. data/lib/middleware/database_middleware.rb +56 -14
  133. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  134. data/try/configuration/scenarios_try.rb +1 -1
  135. data/try/connection/fiber_context_preservation_try.rb +250 -0
  136. data/try/connection/handler_constraints_try.rb +59 -0
  137. data/try/connection/operation_mode_guards_try.rb +208 -0
  138. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  139. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  140. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  141. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  142. data/try/connection/transaction_mode_strict_try.rb +98 -0
  143. data/try/connection/transaction_mode_warn_try.rb +131 -0
  144. data/try/connection/transaction_modes_try.rb +249 -0
  145. data/try/core/autoloader_try.rb +129 -11
  146. data/try/core/connection_try.rb +7 -7
  147. data/try/core/conventional_inheritance_try.rb +130 -0
  148. data/try/core/create_method_try.rb +15 -23
  149. data/try/core/database_consistency_try.rb +10 -10
  150. data/try/core/errors_try.rb +8 -11
  151. data/try/core/familia_extended_try.rb +2 -2
  152. data/try/core/familia_members_methods_try.rb +76 -0
  153. data/try/core/isolated_dbclient_try.rb +165 -0
  154. data/try/core/middleware_try.rb +16 -16
  155. data/try/core/persistence_operations_try.rb +4 -4
  156. data/try/core/pools_try.rb +42 -26
  157. data/try/core/secure_identifier_try.rb +28 -24
  158. data/try/core/time_utils_try.rb +10 -10
  159. data/try/core/tools_try.rb +1 -1
  160. data/try/core/utils_try.rb +2 -2
  161. data/try/data_types/boolean_try.rb +4 -4
  162. data/try/data_types/datatype_base_try.rb +0 -2
  163. data/try/data_types/list_try.rb +10 -10
  164. data/try/data_types/sorted_set_try.rb +5 -5
  165. data/try/data_types/string_try.rb +12 -12
  166. data/try/data_types/unsortedset_try.rb +33 -0
  167. data/try/debugging/cache_behavior_tracer.rb +7 -7
  168. data/try/debugging/debug_aad_process.rb +1 -1
  169. data/try/debugging/debug_concealed_internal.rb +1 -1
  170. data/try/debugging/debug_cross_context.rb +1 -1
  171. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  172. data/try/debugging/encryption_method_tracer.rb +10 -10
  173. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  174. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  175. data/try/encryption/config_persistence_try.rb +2 -2
  176. data/try/encryption/encryption_core_try.rb +19 -19
  177. data/try/encryption/instance_variable_scope_try.rb +1 -1
  178. data/try/encryption/module_loading_try.rb +2 -2
  179. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  180. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  181. data/try/encryption/secure_memory_handling_try.rb +1 -1
  182. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  183. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  184. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  185. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  186. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  187. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  188. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  189. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  190. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  191. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  192. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  193. data/try/features/feature_dependencies_try.rb +3 -3
  194. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  195. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  196. data/try/features/quantization/quantization_try.rb +1 -1
  197. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  198. data/try/features/relationships/indexing_try.rb +433 -0
  199. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  200. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  201. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  202. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  203. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  204. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  205. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  206. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  207. data/try/features/relationships/relationships_performance_try.rb +20 -20
  208. data/try/features/relationships/relationships_try.rb +27 -38
  209. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  210. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  211. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  212. data/try/helpers/test_cleanup.rb +86 -0
  213. data/try/helpers/test_helpers.rb +3 -3
  214. data/try/horreum/base_try.rb +3 -2
  215. data/try/horreum/commands_try.rb +1 -1
  216. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  217. data/try/horreum/initialization_try.rb +11 -7
  218. data/try/horreum/relations_try.rb +21 -13
  219. data/try/horreum/serialization_try.rb +12 -11
  220. data/try/integration/cross_component_try.rb +3 -3
  221. data/try/memory/memory_basic_test.rb +1 -1
  222. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  223. data/try/models/customer_safe_dump_try.rb +1 -1
  224. data/try/models/customer_try.rb +8 -10
  225. data/try/models/datatype_base_try.rb +3 -3
  226. data/try/models/familia_object_try.rb +9 -8
  227. data/try/performance/benchmarks_try.rb +2 -2
  228. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  229. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  230. data/try/prototypes/atomic_saves_v4.rb +1 -1
  231. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  232. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  234. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  235. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  236. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  237. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  238. data/try/prototypes/pooling/pool_siege.rb +11 -11
  239. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  240. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  241. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  242. data/try/refinements/logger_trace_methods_try.rb +44 -0
  243. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  244. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  245. metadata +77 -45
  246. data/.rubocop_todo.yml +0 -208
  247. data/docs/connection_pooling.md +0 -192
  248. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  249. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  250. data/docs/guides/Feature-System-Autoloading.md +0 -228
  251. data/docs/guides/Home.md +0 -116
  252. data/docs/guides/Relationships-Guide.md +0 -737
  253. data/docs/guides/relationships-methods.md +0 -266
  254. data/docs/reference/auditing_database_commands.rb +0 -228
  255. data/examples/permissions.rb +0 -240
  256. data/lib/familia/features/autoloadable.rb +0 -113
  257. data/lib/familia/features/relationships/cascading.rb +0 -437
  258. data/lib/familia/features/relationships/membership.rb +0 -497
  259. data/lib/familia/features/relationships/permission_management.rb +0 -264
  260. data/lib/familia/features/relationships/querying.rb +0 -615
  261. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  262. data/lib/familia/features/relationships/tracking.rb +0 -418
  263. data/lib/familia/refinements/snake_case.rb +0 -40
  264. data/lib/familia/validation/command_recorder.rb +0 -336
  265. data/lib/familia/validation/expectations.rb +0 -519
  266. data/lib/familia/validation/validation_helpers.rb +0 -443
  267. data/lib/familia/validation/validator.rb +0 -412
  268. data/lib/familia/validation.rb +0 -140
  269. data/try/data_types/set_try.rb +0 -33
  270. data/try/features/autoloadable/autoloadable_try.rb +0 -61
  271. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  272. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
  273. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  274. data/try/validation/command_validation_try.rb.disabled +0 -207
  275. data/try/validation/performance_validation_try.rb.disabled +0 -324
  276. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,684 @@
1
+ # Relationship Methods Reference
2
+
3
+ This guide provides detailed documentation for all methods automatically generated by Familia's relationships feature. Each relationship declaration creates a comprehensive set of methods for managing object associations, indexing, and tracking.
4
+
5
+ ## Method Generation Overview
6
+
7
+ The relationships feature uses consistent naming patterns to generate methods based on your declarations. Understanding these patterns helps you predict and use the available methods effectively.
8
+
9
+ ## Participation Methods (`participates_in`)
10
+
11
+ ### Basic Participation Declaration
12
+
13
+ ```ruby
14
+ class Domain < Familia::Horreum
15
+ feature :relationships
16
+ participates_in Customer, :domains
17
+ end
18
+
19
+ class Customer < Familia::Horreum
20
+ feature :relationships
21
+ set :domains # Must define the collection
22
+ end
23
+ ```
24
+
25
+ ### Generated Instance Methods
26
+
27
+ **On Domain instances:**
28
+ - **`add_to_customer_domains(customer_obj_or_id)`** - Add this domain to customer's domains collection
29
+ - **`remove_from_customer_domains(customer_obj_or_id)`** - Remove this domain from customer's domains collection
30
+ - **`in_customer_domains?(customer_obj_or_id)`** - Check if this domain is in customer's domains collection
31
+
32
+ ```ruby
33
+ domain = Domain.new(name: "example.com")
34
+ customer = Customer.new(name: "Acme Corp")
35
+
36
+ # Explicit method calls
37
+ domain.add_to_customer_domains(customer)
38
+ domain.in_customer_domains?(customer) # => true
39
+ domain.remove_from_customer_domains(customer)
40
+ domain.in_customer_domains?(customer) # => false
41
+ ```
42
+
43
+ ### Collection Operator Support
44
+
45
+ **Ruby-like syntax with automatic bidirectional updates:**
46
+ ```ruby
47
+ customer.domains << domain # Equivalent to domain.add_to_customer_domains(customer)
48
+ customer.domains.delete(domain.identifier) # Removes relationship bidirectionally
49
+ ```
50
+
51
+ ### Method Naming Pattern
52
+ `{action}_{to|from}_{lowercase_class_name}_{collection_name}` or `in_{lowercase_class_name}_{collection_name}?`
53
+
54
+ ## Class-Level Tracking Methods (`class_participates_in`)
55
+
56
+ ### Declaration
57
+ ```ruby
58
+ class Customer < Familia::Horreum
59
+ feature :relationships
60
+ class_participates_in :all_customers, score: :created_at
61
+ class_participates_in :active_customers, score: ->(customer) {
62
+ customer.status == 'active' ? customer.last_activity : 0
63
+ }
64
+ end
65
+ ```
66
+
67
+ ### Generated Class Methods
68
+
69
+ **Collection access:**
70
+ - **`Customer.all_customers`** - Returns the sorted set collection directly
71
+ - **`Customer.active_customers`** - Returns the active customers sorted set
72
+
73
+ **Manual management (rarely needed):**
74
+ - **`Customer.add_to_all_customers(customer)`** - Manually add customer to tracking
75
+ - **`Customer.remove_from_all_customers(customer)`** - Manually remove customer from tracking
76
+
77
+ ### Collection Operations
78
+
79
+ ```ruby
80
+ # Query operations (delegated to underlying sorted set)
81
+ Customer.all_customers.size # Count of tracked customers
82
+ Customer.all_customers.range(0, 9) # First 10 customers (by score)
83
+ Customer.all_customers.range_by_score(min_score, max_score) # Customers in score range
84
+
85
+ # Score-based queries
86
+ recent_customers = Customer.all_customers.range_by_score(
87
+ (Time.now - 1.week).to_i, '+inf'
88
+ )
89
+
90
+ # Get customer with scores
91
+ customers_with_scores = Customer.all_customers.range(0, -1, with_scores: true)
92
+ # => [["customer_id_1", 1634567890], ["customer_id_2", 1634567920], ...]
93
+ ```
94
+
95
+ ### Automatic Behavior
96
+ - Objects are **automatically added** to class-level tracking collections when saved
97
+ - Objects are **automatically removed** when destroyed
98
+ - Scores are **automatically calculated** using the provided field or lambda
99
+ - No manual method calls required for basic lifecycle tracking
100
+
101
+ ### Scored Participation in Parent Collections
102
+
103
+ ```ruby
104
+ class User < Familia::Horreum
105
+ feature :relationships
106
+ participates_in Team, :active_users, score: :last_seen
107
+ participates_in Team, :top_performers, score: ->(user) { user.performance_score }
108
+ end
109
+
110
+ class Team < Familia::Horreum
111
+ feature :relationships
112
+ sorted_set :active_users, :top_performers
113
+ end
114
+ ```
115
+
116
+ **Generated methods on Team instances:**
117
+ - **`team.active_users`** - Access the scored collection
118
+ - **`team.top_performers`** - Access the performance-scored collection
119
+
120
+ **Usage:**
121
+ ```ruby
122
+ team = Team.new(name: "Development")
123
+ user = User.new(name: "Alice", last_seen: Time.now.to_i)
124
+
125
+ # User automatically added with score when relationship established
126
+ team.active_users << user
127
+
128
+ # Query by score ranges
129
+ recently_active = team.active_users.range_by_score(
130
+ (Time.now - 1.hour).to_i, '+inf'
131
+ )
132
+ ```
133
+
134
+ ## Indexing Methods (`indexed_by`)
135
+
136
+ The `indexed_by` declaration creates Valkey/Redis hash-based indexes for O(1) field lookups with automatic management.
137
+
138
+ ### Class-Level Indexing (`class_indexed_by`)
139
+
140
+ ```ruby
141
+ class Customer < Familia::Horreum
142
+ feature :relationships
143
+ field :email, :username, :api_key
144
+
145
+ class_indexed_by :email, :email_lookup
146
+ class_indexed_by :username, :username_lookup
147
+ class_indexed_by :api_key, :api_key_lookup
148
+ end
149
+ ```
150
+
151
+ ### Generated Class Methods
152
+
153
+ **Index access:**
154
+ - **`Customer.email_lookup`** - Returns the hash index directly
155
+ - **`Customer.username_lookup`** - Returns the username hash index
156
+ - **`Customer.api_key_lookup`** - Returns the API key hash index
157
+
158
+ **Convenience lookup methods:**
159
+ - **`Customer.find_by_email(email)`** - Find customer ID by email (O(1) lookup)
160
+ - **`Customer.find_by_username(username)`** - Find customer ID by username
161
+ - **`Customer.find_by_api_key(api_key)`** - Find customer ID by API key
162
+
163
+ ### Generated Instance Methods (rarely used manually)
164
+
165
+ **Index management:**
166
+ - **`customer.add_to_class_email_lookup`** - Manually add to email index
167
+ - **`customer.remove_from_class_email_lookup`** - Manually remove from email index
168
+ - **`customer.update_class_email_lookup(old_email)`** - Update index when email changes
169
+
170
+ ### Usage Examples
171
+
172
+ ```ruby
173
+ # Automatic indexing on save
174
+ customer = Customer.new(
175
+ email: "alice@example.com",
176
+ username: "alice123",
177
+ api_key: "ak_abcd1234"
178
+ )
179
+ customer.save # Automatically added to all indexes
180
+
181
+ # O(1) lookups
182
+ customer_id = Customer.find_by_email("alice@example.com")
183
+ customer = Customer.load(customer_id) if customer_id
184
+
185
+ # Direct index access
186
+ all_emails = Customer.email_lookup.to_h
187
+ # => {"alice@example.com" => "customer_1", "bob@example.com" => "customer_2"}
188
+
189
+ # Index operations
190
+ Customer.email_lookup.get("alice@example.com") # => "customer_1"
191
+ Customer.email_lookup.set("new@example.com", "customer_3")
192
+ ```
193
+
194
+ ### Automatic Behavior
195
+ - Objects are **automatically indexed** when saved
196
+ - Indexes are **automatically updated** when indexed fields change
197
+ - Objects are **automatically removed** from indexes when destroyed
198
+ - No manual index management required for standard lifecycle operations
199
+
200
+ **Redis key pattern:** `{class_name.downcase}:{index_name}`
201
+
202
+ ### Relationship-Scoped Indexing (`indexed_by` with `target:`)
203
+
204
+ ```ruby
205
+ class Customer < Familia::Horreum
206
+ feature :relationships
207
+ field :name
208
+ set :domains
209
+ end
210
+
211
+ class Domain < Familia::Horreum
212
+ feature :relationships
213
+ field :name, :subdomain, :port
214
+ participates_in Customer, :domains
215
+
216
+ # Index domains by name within each customer (domains can have same name across customers)
217
+ indexed_by :name, :domain_index, target: Customer
218
+ indexed_by :subdomain, :subdomain_index, target: Customer
219
+ indexed_by :port, :port_index, target: Customer
220
+ end
221
+ ```
222
+
223
+ ### Generated Methods on Target Class (Customer)
224
+
225
+ **Single lookups:**
226
+ - **`customer.find_by_name(domain_name)`** - Find domain ID by name within this customer
227
+ - **`customer.find_by_subdomain(subdomain)`** - Find domain ID by subdomain within this customer
228
+ - **`customer.find_by_port(port)`** - Find domain ID by port within this customer
229
+
230
+ **Multiple lookups:**
231
+ - **`customer.find_all_by_name(domain_names)`** - Find multiple domain IDs by names
232
+ - **`customer.find_all_by_subdomain(subdomains)`** - Find multiple domain IDs by subdomains
233
+ - **`customer.find_all_by_port(ports)`** - Find multiple domain IDs by ports
234
+
235
+ **Direct index access:**
236
+ - **`customer.domain_index`** - Access the name index hash directly
237
+ - **`customer.subdomain_index`** - Access the subdomain index hash directly
238
+ - **`customer.port_index`** - Access the port index hash directly
239
+
240
+ ### Usage Examples
241
+
242
+ ```ruby
243
+ customer = Customer.new(name: "Acme Corp")
244
+ domain1 = Domain.new(name: "example.com", subdomain: "www", port: 443)
245
+ domain2 = Domain.new(name: "api.example.com", subdomain: "api", port: 443)
246
+
247
+ # Establish relationships (automatic indexing)
248
+ customer.domains << domain1
249
+ customer.domains << domain2
250
+
251
+ # O(1) lookups within customer scope
252
+ domain_id = customer.find_by_name("example.com")
253
+ api_domain_id = customer.find_by_subdomain("api")
254
+
255
+ # Multiple lookups
256
+ ssl_domains = customer.find_all_by_port([443, 8443])
257
+
258
+ # Direct index access
259
+ all_domain_names = customer.domain_index.to_h
260
+ # => {"example.com" => "domain_1", "api.example.com" => "domain_2"}
261
+
262
+ # Check if customer has domain with specific name
263
+ has_domain = customer.domain_index.get("example.com").present?
264
+ ```
265
+
266
+ ### Generated Methods on Indexed Class (Domain)
267
+
268
+ **Index management (automatic):**
269
+ - **`domain.add_to_customer_domain_index(customer)`** - Add to customer's domain name index
270
+ - **`domain.remove_from_customer_domain_index(customer)`** - Remove from customer's domain name index
271
+ - **`domain.update_customer_domain_index(customer, old_name)`** - Update index when name changes
272
+
273
+ ### Automatic Behavior
274
+ - Domain is **automatically indexed** when added to customer's domains collection
275
+ - Index is **automatically updated** when domain's indexed fields change
276
+ - Domain is **automatically removed** from index when relationship is removed
277
+ - All index management happens transparently during relationship operations
278
+
279
+ **Redis key pattern:** `{target_class.downcase}:{target_id}:{index_name}`
280
+
281
+ ### When to Use Each Indexing Context
282
+
283
+ **Class-level indexing (`class_indexed_by`):**
284
+ - Use for **system-wide unique** field lookups
285
+ - Examples: email addresses, usernames, API keys, social security numbers
286
+ - Best when field values should be unique across all instances
287
+
288
+ **Relationship-scoped indexing (`indexed_by` with `target:`):**
289
+ - Use for **context-specific** field lookups
290
+ - Examples: domain names per customer, project names per team, usernames per organization
291
+ - Best when field values are unique within a specific parent context but may duplicate across different parents
292
+
293
+ ## Complete API Reference
294
+
295
+ ### Method Naming Conventions
296
+
297
+ **Participation methods:**
298
+ - `{add_to|remove_from}_{target_class_downcase}_{collection_name}(target)`
299
+ - `in_{target_class_downcase}_{collection_name}?(target)`
300
+
301
+ **Class-level tracking:**
302
+ - `{add_to|remove_from}_{collection_name}(object)` (class methods)
303
+ - `{ClassName}.{collection_name}` (collection accessor)
304
+
305
+ **Class-level indexing:**
306
+ - `{add_to|remove_from}_class_{index_name}` (instance methods)
307
+ - `{ClassName}.{index_name}` (index accessor)
308
+ - `{ClassName}.find_by_{field_name}(value)` (convenience lookup)
309
+
310
+ **Relationship-scoped indexing:**
311
+ - `{target_instance}.find_by_{field_name}(value)` (single lookup)
312
+ - `{target_instance}.find_all_by_{field_name}(values)` (multiple lookup)
313
+ - `{target_instance}.{index_name}` (direct index access)
314
+
315
+ ## Key Benefits of the Relationships API
316
+
317
+ - **Automatic Management**: Save operations update indexes and tracking automatically
318
+ - **Ruby-Idiomatic**: Use `<<` operator for natural collection syntax
319
+ - **Consistent Storage**: All indexes stored at class level for architectural simplicity
320
+ - **Predictable Naming**: Method names follow consistent patterns for easy discovery
321
+ - **O(1) Performance**: Hash-based indexes provide constant-time lookups
322
+ - **Bidirectional Sync**: Relationship changes automatically update both sides
323
+
324
+ ## Advanced Implementation Patterns
325
+
326
+ ### Error Handling and Edge Cases
327
+
328
+ **Handling Missing Objects:**
329
+ ```ruby
330
+ # Safe relationship operations
331
+ begin
332
+ customer.domains << domain
333
+ rescue Familia::Problem => e
334
+ Rails.logger.error "Failed to establish relationship: #{e.message}"
335
+ end
336
+
337
+ # Check if objects exist before relating
338
+ if domain.persisted? && customer.persisted?
339
+ customer.domains << domain
340
+ end
341
+
342
+ # Batch operations with error handling
343
+ domain_ids.each do |id|
344
+ next unless Domain.exists?(id) # Custom existence check
345
+ customer.domains.add(id)
346
+ end
347
+ ```
348
+
349
+ **Index Consistency Validation:**
350
+ ```ruby
351
+ class Customer < Familia::Horreum
352
+ feature :relationships
353
+
354
+ def validate_email_index_consistency
355
+ stored_id = self.class.email_lookup.get(email)
356
+ return true if stored_id == identifier
357
+
358
+ Rails.logger.warn "Email index inconsistency: #{email} -> #{stored_id} vs #{identifier}"
359
+ # Repair the index
360
+ self.class.email_lookup.set(email, identifier)
361
+ false
362
+ end
363
+
364
+ after_save :validate_email_index_consistency
365
+ end
366
+ ```
367
+
368
+ ### Performance Optimization Techniques
369
+
370
+ **Lazy Loading Patterns:**
371
+ ```ruby
372
+ class Customer < Familia::Horreum
373
+ feature :relationships
374
+ set :domains
375
+
376
+ # Memoized relationship loading
377
+ def domain_objects
378
+ @domain_objects ||= begin
379
+ domain_ids = domains.to_a
380
+ Domain.multiget(*domain_ids).compact
381
+ end
382
+ end
383
+
384
+ # Paginated relationship loading
385
+ def recent_domains(limit = 10)
386
+ recent_ids = domains.range(0, limit - 1)
387
+ Domain.multiget(*recent_ids).compact
388
+ end
389
+
390
+ # Selective field loading
391
+ def domain_names
392
+ domains.to_a.map do |id|
393
+ Domain.new(domain_id: id).name # Load only name field
394
+ end
395
+ end
396
+ end
397
+ ```
398
+
399
+ **Bulk Operations with Transaction Safety:**
400
+ ```ruby
401
+ class Team < Familia::Horreum
402
+ feature :relationships
403
+ set :members
404
+
405
+ def bulk_add_members(user_ids)
406
+ # Validate all IDs exist first
407
+ valid_ids = user_ids.select { |id| User.exists?(id) }
408
+
409
+ # Use Valkey/Redis pipeline for bulk operations
410
+ Familia.redis.pipelined do |pipeline|
411
+ valid_ids.each do |user_id|
412
+ members.add(user_id)
413
+ # Update reverse indexes in same pipeline
414
+ user = User.new(user_id: user_id)
415
+ user.add_to_team_members(self)
416
+ end
417
+ end
418
+
419
+ valid_ids.size
420
+ end
421
+
422
+ def bulk_remove_members(user_ids)
423
+ # Atomic bulk removal
424
+ removed_count = 0
425
+ user_ids.each do |user_id|
426
+ if members.delete(user_id)
427
+ removed_count += 1
428
+ # Clean up reverse relationship
429
+ user = User.new(user_id: user_id)
430
+ user.remove_from_team_members(self)
431
+ end
432
+ end
433
+ removed_count
434
+ end
435
+ end
436
+ ```
437
+
438
+ ### Custom Scoring and Complex Relationships
439
+
440
+ **Dynamic Scoring with Context:**
441
+ ```ruby
442
+ class Project < Familia::Horreum
443
+ feature :relationships
444
+ field :priority, :created_at, :deadline
445
+
446
+ participates_in Team, :projects, score: :calculated_priority
447
+
448
+ private
449
+
450
+ def calculated_priority
451
+ base_priority = priority.to_i
452
+ urgency_multiplier = deadline_urgency_factor
453
+ age_factor = project_age_factor
454
+
455
+ (base_priority * urgency_multiplier * age_factor).to_i
456
+ end
457
+
458
+ def deadline_urgency_factor
459
+ return 1.0 unless deadline
460
+
461
+ days_until_deadline = (deadline.to_time - Time.now) / 1.day
462
+ return 3.0 if days_until_deadline <= 1 # Critical
463
+ return 2.0 if days_until_deadline <= 7 # High
464
+ 1.0 # Normal
465
+ end
466
+
467
+ def project_age_factor
468
+ days_old = (Time.now - created_at.to_time) / 1.day
469
+ [1.0 + (days_old / 30.0), 2.0].min # Cap at 2x multiplier
470
+ end
471
+ end
472
+ ```
473
+
474
+ **Multi-Level Relationships:**
475
+ ```ruby
476
+ class Organization < Familia::Horreum
477
+ feature :relationships
478
+ set :departments
479
+
480
+ # Find all users across all departments
481
+ def all_users
482
+ department_ids = departments.to_a
483
+ user_ids = []
484
+
485
+ department_ids.each do |dept_id|
486
+ dept = Department.load(dept_id)
487
+ user_ids.concat(dept.users.to_a) if dept
488
+ end
489
+
490
+ User.multiget(*user_ids.uniq).compact
491
+ end
492
+
493
+ # Hierarchical relationship queries
494
+ def users_in_department(department_name)
495
+ dept_id = find_by_name(department_name)
496
+ return [] unless dept_id
497
+
498
+ dept = Department.load(dept_id)
499
+ return [] unless dept
500
+
501
+ user_ids = dept.users.to_a
502
+ User.multiget(*user_ids).compact
503
+ end
504
+ end
505
+ ```
506
+
507
+ ### Advanced Indexing Patterns
508
+
509
+ **Composite Index Keys:**
510
+ ```ruby
511
+ class ApiKey < Familia::Horreum
512
+ feature :relationships
513
+ field :customer_id, :environment, :key_type, :key_hash
514
+
515
+ # Composite index for environment + type lookups
516
+ indexed_by :environment_and_type, :env_type_lookup, target: Customer
517
+
518
+ private
519
+
520
+ def environment_and_type
521
+ "#{environment}:#{key_type}" # e.g., "production:read_write"
522
+ end
523
+ end
524
+
525
+ # Usage
526
+ customer.find_by_environment_and_type("production:read_only")
527
+ customer.find_all_by_environment_and_type(["staging:read_write", "production:read_write"])
528
+ ```
529
+
530
+ **Time-Based Index Partitioning:**
531
+ ```ruby
532
+ class Event < Familia::Horreum
533
+ feature :relationships
534
+ field :event_type, :timestamp, :user_id
535
+
536
+ participates_in User, :events, score: :timestamp
537
+ indexed_by :daily_partition, :daily_events, target: User
538
+
539
+ private
540
+
541
+ def daily_partition
542
+ Time.at(timestamp).strftime('%Y%m%d') # e.g., "20241215"
543
+ end
544
+ end
545
+
546
+ # Usage - find today's events for user
547
+ today = Time.now.strftime('%Y%m%d')
548
+ todays_event_ids = user.find_all_by_daily_partition([today])
549
+ ```
550
+
551
+ ### Testing Relationship Methods
552
+
553
+ **Unit Testing Patterns:**
554
+ ```ruby
555
+ # test/models/relationship_test.rb
556
+ class RelationshipTest < Minitest::Test
557
+ def setup
558
+ @customer = Customer.create(email: "test@example.com", name: "Test Corp")
559
+ @domain = Domain.create(name: "test.com", status: "active")
560
+ end
561
+
562
+ def test_bidirectional_relationship_establishment
563
+ @customer.domains << @domain
564
+
565
+ # Test both sides of relationship
566
+ assert @customer.domains.member?(@domain.identifier)
567
+ assert @domain.in_customer_domains?(@customer.identifier)
568
+ end
569
+
570
+ def test_relationship_removal
571
+ @customer.domains << @domain
572
+ @customer.domains.delete(@domain.identifier)
573
+
574
+ # Verify complete cleanup
575
+ refute @customer.domains.member?(@domain.identifier)
576
+ refute @domain.in_customer_domains?(@customer.identifier)
577
+ end
578
+
579
+ def test_index_automatic_maintenance
580
+ @customer.save # Should trigger indexing
581
+
582
+ found_id = Customer.find_by_email("test@example.com")
583
+ assert_equal @customer.identifier, found_id
584
+ end
585
+
586
+ def test_scoped_index_lookup
587
+ @customer.domains << @domain
588
+
589
+ found_id = @customer.find_by_name("test.com")
590
+ assert_equal @domain.identifier, found_id
591
+ end
592
+
593
+ def test_scored_relationship_ordering
594
+ project = Project.create(name: "Test Project")
595
+ task1 = Task.create(title: "Low priority", priority: 1)
596
+ task2 = Task.create(title: "High priority", priority: 10)
597
+
598
+ project.tasks << task1
599
+ project.tasks << task2
600
+
601
+ # Should be ordered by priority (high to low)
602
+ task_ids = project.tasks.range(0, -1, order: 'DESC')
603
+ assert_equal task2.identifier, task_ids.first
604
+ assert_equal task1.identifier, task_ids.last
605
+ end
606
+ end
607
+ ```
608
+
609
+ ### Complete Production Example
610
+
611
+ ```ruby
612
+ # Real-world e-commerce system
613
+ class Customer < Familia::Horreum
614
+ feature :relationships
615
+ field :email, :username, :tier, :created_at, :last_activity
616
+
617
+ # Global unique lookups
618
+ class_indexed_by :email, :email_lookup
619
+ class_indexed_by :username, :username_lookup
620
+
621
+ # Tiered customer tracking
622
+ class_participates_in :all_customers, score: :created_at
623
+ class_participates_in :active_customers, score: :last_activity
624
+ class_participates_in :premium_customers,
625
+ score: ->(c) { c.tier == 'premium' ? c.last_activity : 0 }
626
+
627
+ # Relationships
628
+ set :orders, :addresses, :payment_methods
629
+
630
+ def recent_orders(limit = 10)
631
+ order_ids = orders.range(0, limit - 1)
632
+ Order.multiget(*order_ids).compact
633
+ end
634
+
635
+ def total_spent
636
+ recent_orders(100).sum(&:total_amount)
637
+ end
638
+ end
639
+
640
+ class Order < Familia::Horreum
641
+ feature :relationships
642
+ field :total_amount, :status, :created_at, :order_number
643
+
644
+ participates_in Customer, :orders, score: :created_at
645
+ indexed_by :order_number, :order_lookup, target: Customer
646
+ indexed_by :status, :status_lookup, target: Customer
647
+
648
+ set :line_items
649
+ end
650
+
651
+ class Address < Familia::Horreum
652
+ feature :relationships
653
+ field :street, :city, :state, :zip_code, :address_type
654
+
655
+ participates_in Customer, :addresses
656
+ indexed_by :address_type, :address_type_lookup, target: Customer
657
+ end
658
+
659
+ # Usage in production
660
+ customer = Customer.create(
661
+ email: "alice@example.com",
662
+ username: "alice_smith",
663
+ tier: "premium"
664
+ )
665
+
666
+ # Automatic indexing and tracking
667
+ Customer.find_by_email("alice@example.com") # => customer.identifier
668
+ Customer.premium_customers.range(0, 9) # Recent premium customers
669
+
670
+ # Complex relationship queries
671
+ order = Order.create(order_number: "ORD-12345", status: "shipped")
672
+ customer.orders << order
673
+
674
+ customer.find_by_order_number("ORD-12345") # => order.identifier
675
+ shipped_orders = customer.find_all_by_status(["shipped", "delivered"])
676
+ ```
677
+
678
+ ---
679
+
680
+ ## See Also
681
+
682
+ - **[Relationships Feature Guide](feature-relationships.md)** - Conceptual introduction to relationships
683
+ - **[Technical Reference](../reference/api-technical.md#relationships-feature-v200-pre7)** - Advanced implementation patterns
684
+ - **[Feature System Guide](feature-system.md)** - Understanding Familia's feature architecture