familia 2.0.0.pre18 → 2.0.0.pre21

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 (370) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.rst +205 -88
  8. data/CLAUDE.md +62 -10
  9. data/Gemfile +3 -3
  10. data/Gemfile.lock +27 -62
  11. data/README.md +39 -0
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
  15. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
  16. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
  17. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
  18. data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
  19. data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
  20. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  21. data/docs/guides/encryption.md +486 -0
  22. data/docs/guides/feature-encrypted-fields.md +123 -7
  23. data/docs/guides/feature-expiration.md +177 -133
  24. data/docs/guides/feature-external-identifiers.md +415 -443
  25. data/docs/guides/feature-object-identifiers.md +400 -269
  26. data/docs/guides/feature-quantization.md +120 -6
  27. data/docs/guides/feature-relationships-indexing.md +318 -0
  28. data/docs/guides/feature-relationships-methods.md +146 -604
  29. data/docs/guides/feature-relationships-participation.md +263 -0
  30. data/docs/guides/feature-relationships.md +118 -136
  31. data/docs/guides/feature-system-devs.md +176 -693
  32. data/docs/guides/feature-system.md +119 -6
  33. data/docs/guides/feature-transient-fields.md +81 -0
  34. data/docs/guides/field-system.md +778 -0
  35. data/docs/guides/index.md +32 -15
  36. data/docs/guides/logging.md +187 -0
  37. data/docs/guides/optimized-loading.md +674 -0
  38. data/docs/guides/thread-safety-monitoring.md +61 -0
  39. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  40. data/docs/migrating/v2.0.0-pre19.md +197 -0
  41. data/docs/migrating/v2.0.0-pre22.md +241 -0
  42. data/docs/overview.md +7 -9
  43. data/docs/reference/api-technical.md +267 -320
  44. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  45. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  46. data/examples/autoloader/mega_customer.rb +2 -0
  47. data/examples/datatype_standalone.rb +282 -0
  48. data/examples/encrypted_fields.rb +2 -1
  49. data/examples/json_usage_patterns.rb +2 -0
  50. data/examples/relationships.rb +3 -0
  51. data/examples/safe_dump.rb +2 -1
  52. data/examples/sampling_demo.rb +53 -0
  53. data/examples/single_connection_transaction_confusions.rb +2 -1
  54. data/familia.gemspec +2 -1
  55. data/lib/familia/base.rb +2 -0
  56. data/lib/familia/connection/behavior.rb +254 -0
  57. data/lib/familia/connection/handlers.rb +97 -0
  58. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  59. data/lib/familia/connection/middleware.rb +34 -24
  60. data/lib/familia/connection/operation_core.rb +3 -1
  61. data/lib/familia/connection/operations.rb +2 -0
  62. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
  63. data/lib/familia/connection/transaction_core.rb +75 -9
  64. data/lib/familia/connection.rb +21 -5
  65. data/lib/familia/data_type/class_methods.rb +3 -1
  66. data/lib/familia/data_type/connection.rb +153 -7
  67. data/lib/familia/data_type/database_commands.rb +9 -4
  68. data/lib/familia/data_type/serialization.rb +10 -4
  69. data/lib/familia/data_type/settings.rb +2 -0
  70. data/lib/familia/data_type/types/counter.rb +2 -0
  71. data/lib/familia/data_type/types/hashkey.rb +8 -6
  72. data/lib/familia/data_type/types/listkey.rb +2 -0
  73. data/lib/familia/data_type/types/lock.rb +2 -0
  74. data/lib/familia/data_type/types/sorted_set.rb +2 -0
  75. data/lib/familia/data_type/types/stringkey.rb +2 -0
  76. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  77. data/lib/familia/data_type.rb +2 -0
  78. data/lib/familia/encryption/encrypted_data.rb +4 -2
  79. data/lib/familia/encryption/manager.rb +2 -0
  80. data/lib/familia/encryption/provider.rb +2 -0
  81. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  82. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  83. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  84. data/lib/familia/encryption/registry.rb +2 -0
  85. data/lib/familia/encryption/request_cache.rb +2 -0
  86. data/lib/familia/encryption.rb +9 -2
  87. data/lib/familia/errors.rb +53 -14
  88. data/lib/familia/features/autoloader.rb +2 -0
  89. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  90. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  91. data/lib/familia/features/encrypted_fields.rb +2 -2
  92. data/lib/familia/features/expiration/extensions.rb +11 -11
  93. data/lib/familia/features/expiration.rb +29 -21
  94. data/lib/familia/features/external_identifier.rb +33 -7
  95. data/lib/familia/features/object_identifier.rb +2 -0
  96. data/lib/familia/features/quantization.rb +3 -1
  97. data/lib/familia/features/relationships/README.md +3 -1
  98. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  99. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
  100. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  101. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
  102. data/lib/familia/features/relationships/indexing.rb +40 -42
  103. data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
  104. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  105. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  106. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  107. data/lib/familia/features/relationships/participation.rb +155 -69
  108. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  109. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  110. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  111. data/lib/familia/features/relationships.rb +5 -3
  112. data/lib/familia/features/safe_dump.rb +2 -0
  113. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  114. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  115. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  116. data/lib/familia/features/transient_fields.rb +2 -0
  117. data/lib/familia/features.rb +2 -0
  118. data/lib/familia/field_type.rb +5 -2
  119. data/lib/familia/horreum/connection.rb +28 -36
  120. data/lib/familia/horreum/database_commands.rb +131 -10
  121. data/lib/familia/horreum/definition.rb +18 -7
  122. data/lib/familia/horreum/management.rb +233 -57
  123. data/lib/familia/horreum/persistence.rb +314 -122
  124. data/lib/familia/horreum/related_fields.rb +2 -0
  125. data/lib/familia/horreum/serialization.rb +26 -4
  126. data/lib/familia/horreum/settings.rb +2 -0
  127. data/lib/familia/horreum/utils.rb +2 -8
  128. data/lib/familia/horreum.rb +46 -13
  129. data/lib/familia/identifier_extractor.rb +2 -0
  130. data/lib/familia/instrumentation.rb +156 -0
  131. data/lib/familia/json_serializer.rb +2 -0
  132. data/lib/familia/logging.rb +94 -37
  133. data/lib/familia/refinements/dear_json.rb +2 -0
  134. data/lib/familia/refinements/stylize_words.rb +2 -14
  135. data/lib/familia/refinements/time_literals.rb +2 -0
  136. data/lib/familia/refinements.rb +2 -0
  137. data/lib/familia/secure_identifier.rb +10 -2
  138. data/lib/familia/settings.rb +9 -7
  139. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  140. data/lib/familia/thread_safety/monitor.rb +328 -0
  141. data/lib/familia/utils.rb +13 -0
  142. data/lib/familia/verifiable_identifier.rb +3 -1
  143. data/lib/familia/version.rb +3 -1
  144. data/lib/familia.rb +31 -4
  145. data/lib/middleware/database_command_counter.rb +152 -0
  146. data/lib/middleware/database_logger.rb +325 -129
  147. data/lib/multi_result.rb +2 -0
  148. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  149. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  150. data/try/edge_cases/json_serialization_try.rb +2 -0
  151. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  152. data/try/edge_cases/race_conditions_try.rb +4 -0
  153. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  154. data/try/edge_cases/string_coercion_try.rb +6 -4
  155. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  156. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  157. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  158. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  159. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  160. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  161. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  162. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  164. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  165. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  166. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  167. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  168. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  169. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  170. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  171. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  172. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  173. data/try/features/encryption/config_persistence_try.rb +4 -0
  174. data/try/features/encryption/core_try.rb +4 -0
  175. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  176. data/try/features/encryption/module_loading_try.rb +4 -0
  177. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  178. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  179. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  180. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  181. data/try/features/expiration/expiration_try.rb +5 -1
  182. data/try/features/external_identifier/external_identifier_try.rb +171 -8
  183. data/try/features/feature_dependencies_try.rb +2 -0
  184. data/try/features/feature_improvements_try.rb +2 -0
  185. data/try/features/field_groups_try.rb +2 -0
  186. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  187. data/try/features/object_identifier/object_identifier_try.rb +2 -0
  188. data/try/features/quantization/quantization_try.rb +4 -0
  189. data/try/features/real_feature_integration_try.rb +2 -0
  190. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/indexing_rebuild_try.rb +600 -0
  192. data/try/features/relationships/indexing_try.rb +30 -4
  193. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  194. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  195. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  196. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  197. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  198. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  199. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  200. data/try/features/relationships/relationships_api_changes_try.rb +6 -4
  201. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  202. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  203. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  204. data/try/features/relationships/relationships_performance_try.rb +4 -0
  205. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  206. data/try/features/relationships/relationships_try.rb +6 -4
  207. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  208. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  209. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  210. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  211. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  212. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  213. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  214. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  215. data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
  216. data/try/integration/connection/handler_constraints_try.rb +4 -0
  217. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  218. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  219. data/try/integration/connection/operation_mode_guards_try.rb +5 -1
  220. data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
  221. data/try/integration/connection/pools_try.rb +4 -0
  222. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  223. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  224. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  225. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  226. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  227. data/try/integration/connection/transaction_modes_try.rb +4 -0
  228. data/try/integration/conventional_inheritance_try.rb +4 -0
  229. data/try/integration/create_method_try.rb +26 -22
  230. data/try/integration/cross_component_try.rb +4 -0
  231. data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
  232. data/try/integration/data_types/datatype_transactions_try.rb +251 -0
  233. data/try/integration/database_consistency_try.rb +4 -0
  234. data/try/integration/familia_extended_try.rb +4 -0
  235. data/try/integration/familia_members_methods_try.rb +4 -0
  236. data/try/integration/models/customer_safe_dump_try.rb +9 -1
  237. data/try/integration/models/customer_try.rb +4 -0
  238. data/try/integration/models/datatype_base_try.rb +4 -0
  239. data/try/integration/models/familia_object_try.rb +5 -1
  240. data/try/integration/persistence_operations_try.rb +166 -10
  241. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  242. data/try/integration/save_methods_consistency_try.rb +241 -0
  243. data/try/integration/scenarios_try.rb +4 -0
  244. data/try/integration/secure_identifier_try.rb +4 -0
  245. data/try/integration/transaction_safety_core_try.rb +176 -0
  246. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  247. data/try/integration/verifiable_identifier_try.rb +4 -0
  248. data/try/investigation/pipeline_routing/README.md +228 -0
  249. data/try/performance/benchmarks_try.rb +4 -0
  250. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  251. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  252. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  253. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  254. data/try/support/debugging/debug_aad_process.rb +3 -0
  255. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  256. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  257. data/try/support/debugging/debug_context_aad.rb +3 -0
  258. data/try/support/debugging/debug_context_simple.rb +3 -0
  259. data/try/support/debugging/debug_cross_context.rb +3 -0
  260. data/try/support/debugging/debug_database_load.rb +3 -0
  261. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  262. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  263. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  264. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  265. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  266. data/try/support/debugging/debug_load_path.rb +3 -0
  267. data/try/support/debugging/debug_method_definition.rb +3 -0
  268. data/try/support/debugging/debug_method_resolution.rb +3 -0
  269. data/try/support/debugging/debug_minimal.rb +3 -0
  270. data/try/support/debugging/debug_provider.rb +3 -0
  271. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  272. data/try/support/debugging/debug_string_class.rb +3 -0
  273. data/try/support/debugging/debug_test.rb +3 -0
  274. data/try/support/debugging/debug_test_design.rb +3 -0
  275. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  276. data/try/support/debugging/provider_diagnostics.rb +4 -0
  277. data/try/support/helpers/test_cleanup.rb +4 -0
  278. data/try/support/helpers/test_helpers.rb +5 -0
  279. data/try/support/memory/memory_basic_test.rb +4 -0
  280. data/try/support/memory/memory_detailed_test.rb +4 -0
  281. data/try/support/memory/memory_search_for_string.rb +4 -0
  282. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  283. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  284. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  285. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  286. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  287. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  288. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  289. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  290. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  291. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  292. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  293. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  294. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  295. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  296. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  297. data/try/thread_safety/README.md +496 -0
  298. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  299. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  300. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  301. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  302. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  303. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  304. data/try/thread_safety/field_registration_race_try.rb +222 -0
  305. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  306. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  307. data/try/thread_safety/module_config_race_try.rb +175 -0
  308. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  309. data/try/unit/core/autoloader_try.rb +4 -0
  310. data/try/unit/core/base_enhancements_try.rb +4 -0
  311. data/try/unit/core/connection_try.rb +4 -0
  312. data/try/unit/core/errors_try.rb +4 -0
  313. data/try/unit/core/extensions_try.rb +4 -0
  314. data/try/unit/core/familia_logger_try.rb +2 -0
  315. data/try/unit/core/familia_try.rb +4 -0
  316. data/try/unit/core/middleware_sampling_try.rb +335 -0
  317. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  318. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  319. data/try/unit/core/middleware_try.rb +4 -0
  320. data/try/unit/core/settings_try.rb +4 -0
  321. data/try/unit/core/time_utils_try.rb +4 -0
  322. data/try/unit/core/tools_try.rb +4 -0
  323. data/try/unit/core/utils_try.rb +37 -0
  324. data/try/unit/data_types/boolean_try.rb +5 -1
  325. data/try/unit/data_types/counter_try.rb +4 -0
  326. data/try/unit/data_types/datatype_base_try.rb +4 -0
  327. data/try/unit/data_types/hash_try.rb +4 -0
  328. data/try/unit/data_types/list_try.rb +4 -0
  329. data/try/unit/data_types/lock_try.rb +4 -0
  330. data/try/unit/data_types/sorted_set_try.rb +4 -0
  331. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  332. data/try/unit/data_types/string_try.rb +5 -1
  333. data/try/unit/data_types/unsortedset_try.rb +4 -0
  334. data/try/unit/familia_resolve_class_try.rb +116 -0
  335. data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
  336. data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
  337. data/try/unit/horreum/base_try.rb +5 -1
  338. data/try/unit/horreum/class_methods_try.rb +6 -2
  339. data/try/unit/horreum/commands_try.rb +4 -0
  340. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  341. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
  342. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  343. data/try/unit/horreum/field_categories_try.rb +4 -0
  344. data/try/unit/horreum/field_definition_try.rb +4 -0
  345. data/try/unit/horreum/initialization_try.rb +5 -1
  346. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  347. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  348. data/try/unit/horreum/relations_try.rb +8 -4
  349. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  350. data/try/unit/horreum/serialization_try.rb +6 -2
  351. data/try/unit/horreum/settings_try.rb +4 -0
  352. data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
  353. data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
  354. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  355. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  356. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  357. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  358. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  359. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  360. data/try/unit/thread_safety_monitor_try.rb +149 -0
  361. metadata +81 -14
  362. data/.github/workflows/code-quality.yml +0 -138
  363. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  364. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  365. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  366. data/docs/archive/README.md +0 -64
  367. data/docs/archive/api-reference.md +0 -333
  368. data/docs/guides/core-field-system.md +0 -806
  369. data/docs/guides/implementation.md +0 -276
  370. data/docs/guides/security-model.md +0 -183
@@ -1,5 +1,9 @@
1
+ # lib/familia/features/relationships/indexing/unique_index_generators.rb
2
+ #
1
3
  # frozen_string_literal: true
2
4
 
5
+ require_relative 'rebuild_strategies'
6
+
3
7
  module Familia
4
8
  module Features
5
9
  module Relationships
@@ -49,11 +53,11 @@ module Familia
49
53
  # @param indexed_class [Class] The class being indexed (e.g., Employee)
50
54
  # @param field [Symbol] The field to index
51
55
  # @param index_name [Symbol] Name of the index
52
- # @param within [Class, Symbol, nil] Parent class for instance-scoped index
56
+ # @param within [Class, Symbol, nil] Scope class for instance-scoped index
53
57
  # @param query [Boolean] Whether to generate query methods
54
58
  def setup(indexed_class:, field:, index_name:, within:, query:)
55
59
  # Normalize parameters and determine scope type
56
- target_class, scope_type = if within
60
+ scope_class, scope_type = if within
57
61
  k = Familia.resolve_class(within)
58
62
  [k, :instance]
59
63
  else
@@ -63,7 +67,8 @@ module Familia
63
67
  # Store metadata for this indexing relationship
64
68
  indexed_class.indexing_relationships << IndexingRelationship.new(
65
69
  field: field,
66
- target_class: target_class,
70
+ scope_class: scope_class,
71
+ within: within,
67
72
  index_name: index_name,
68
73
  query: query,
69
74
  cardinality: :unique,
@@ -73,10 +78,10 @@ module Familia
73
78
  case scope_type
74
79
  when :instance
75
80
  # Instance-scoped index (within: Company)
76
- if query && target_class.is_a?(Class)
77
- generate_query_methods_destination(indexed_class, field, target_class, index_name)
81
+ if query && scope_class.is_a?(Class)
82
+ generate_query_methods_destination(indexed_class, field, scope_class, index_name)
78
83
  end
79
- generate_mutation_methods_self(indexed_class, field, target_class, index_name)
84
+ generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
80
85
  when :class
81
86
  # Class-level index (no within:)
82
87
  indexed_class.send(:ensure_index_field, indexed_class, index_name, :class_hashkey)
@@ -85,7 +90,8 @@ module Familia
85
90
  end
86
91
  end
87
92
 
88
- # Generates query methods ON THE PARENT CLASS (Company when within: Company):
93
+ # Generates query methods ON THE SCOPE CLASS (Company when within: Company)
94
+ #
89
95
  # - company.find_by_badge_number(badge) - find by field value
90
96
  # - company.find_all_by_badge_number([badges]) - batch lookup
91
97
  # - company.badge_index - DataType accessor
@@ -93,17 +99,20 @@ module Familia
93
99
  #
94
100
  # @param indexed_class [Class] The class being indexed (e.g., Employee)
95
101
  # @param field [Symbol] The field to index (e.g., :badge_number)
96
- # @param target_class [Class] The parent class (e.g., Company)
102
+ # @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
97
103
  # @param index_name [Symbol] Name of the index (e.g., :badge_index)
98
- def generate_query_methods_destination(indexed_class, field, target_class, index_name)
99
- # Resolve target class using Familia pattern
100
- actual_target_class = Familia.resolve_class(target_class)
104
+ def generate_query_methods_destination(indexed_class, field, scope_class, index_name)
105
+ # Resolve scope class using Familia pattern
106
+ actual_scope_class = Familia.resolve_class(scope_class)
101
107
 
102
108
  # Ensure the index field is declared (creates accessor that returns DataType)
103
- actual_target_class.send(:ensure_index_field, actual_target_class, index_name, :hashkey)
109
+ actual_scope_class.send(:ensure_index_field, actual_scope_class, index_name, :hashkey)
110
+
111
+ # Get scope_class_config for method naming (needed for rebuild methods)
112
+ scope_class_config = actual_scope_class.config_name
104
113
 
105
114
  # Generate instance query method (e.g., company.find_by_badge_number)
106
- actual_target_class.class_eval do
115
+ actual_scope_class.class_eval do
107
116
  define_method(:"find_by_#{field}") do |provided_value|
108
117
  # Use declared field accessor instead of manual instantiation
109
118
  index_hash = send(index_name)
@@ -121,7 +130,10 @@ module Familia
121
130
 
122
131
  # Generate bulk query method (e.g., company.find_all_by_badge_number)
123
132
  define_method(:"find_all_by_#{field}") do |provided_ids|
124
- provided_ids = Array(provided_ids)
133
+ # Convert to array and filter nil inputs before querying Redis.
134
+ # This prevents wasteful lookups for empty string keys (nil.to_s → "").
135
+ # Output may contain fewer elements than input (standard ORM behavior).
136
+ provided_ids = Array(provided_ids).compact
125
137
  return [] if provided_ids.empty?
126
138
 
127
139
  # Use declared field accessor instead of manual instantiation
@@ -129,7 +141,8 @@ module Familia
129
141
 
130
142
  # Get all identifiers from the hash
131
143
  record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
132
- # Filter out nil values and instantiate objects
144
+
145
+ # Filter out nil values (non-existent records) and instantiate objects
133
146
  record_ids.compact.map { |record_id|
134
147
  indexed_class.find_by_identifier(record_id)
135
148
  }
@@ -139,77 +152,166 @@ module Familia
139
152
  # No need to manually define it here
140
153
 
141
154
  # Generate method to rebuild the unique index for this parent instance
142
- define_method(:"rebuild_#{index_name}") do
143
- # Use declared field accessor instead of manual instantiation
144
- index_hash = send(index_name)
155
+ define_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
156
+ # Find the collection containing the indexed class.
157
+ #
158
+ # Strategy 1: Check if indexed_class has a participation relationship
159
+ # pointing back to this scope class. Participation relationships are
160
+ # stored on the PARTICIPANT class (indexed_class), not the target.
161
+ #
162
+ # Example: When RebuildTestEmployee.participates_in(RebuildTestCompany, :employees),
163
+ # the relationship is stored on RebuildTestEmployee, and we need to find it
164
+ # by matching target_class (RebuildTestCompany) with self.class.
165
+ collection = nil
166
+ if indexed_class.respond_to?(:participation_relationships)
167
+ participation = indexed_class.participation_relationships.find do |rel|
168
+ rel.target_class == self.class
169
+ end
170
+
171
+ if participation
172
+ collection = send(participation.collection_name)
173
+ end
174
+ end
145
175
 
146
- # Clear existing index using DataType method
147
- index_hash.clear
176
+ # Strategy 2: Fallback to checking related_fields for explicit class: option
177
+ unless collection
178
+ if self.class.respond_to?(:related_fields)
179
+ self.class.related_fields&.each do |name, field_def|
180
+ # Check if this DataType's class option matches the indexed class
181
+ if field_def.opts[:class] == indexed_class
182
+ collection = send(name)
183
+ break
184
+ end
185
+ end
186
+ end
187
+ end
148
188
 
149
- # Rebuild from all existing objects
150
- # This would need to scan through all objects belonging to this parent
151
- # Implementation depends on how objects are stored/tracked
189
+ if collection
190
+ # Find the IndexingRelationship to get cardinality metadata
191
+ index_config = indexed_class.indexing_relationships.find { |rel| rel.index_name == index_name }
192
+
193
+ # Strategy 2: Use participation-based rebuild
194
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
195
+ self, # scope_instance (e.g., company)
196
+ indexed_class, # e.g., Employee
197
+ field, # e.g., :badge_number
198
+ :"add_to_#{scope_class_config}_#{index_name}", # e.g., :add_to_company_badge_index
199
+ collection,
200
+ index_config.cardinality, # :unique or :multi
201
+ batch_size: batch_size,
202
+ &progress_block
203
+ )
204
+ else
205
+ # Strategy 3: Fall back to SCAN with filtering
206
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_scan(
207
+ indexed_class,
208
+ field,
209
+ :"add_to_#{scope_class_config}_#{index_name}",
210
+ scope_instance: self,
211
+ batch_size: batch_size,
212
+ &progress_block
213
+ )
214
+ end
152
215
  end
153
216
  end
154
217
  end
155
218
 
156
- # Generates mutation methods ON THE INDEXED CLASS (Employee):
157
- # Instance methods for parent-scoped unique index operations:
158
- # - employee.add_to_company_badge_index(company)
219
+ # Generates mutation methods ON THE INDEXED CLASS (Employee)
220
+ #
221
+ # Instance methods for scope-scoped unique index operations:
222
+ # - employee.add_to_company_badge_index(company) - automatically validates uniqueness
159
223
  # - employee.remove_from_company_badge_index(company)
160
224
  # - employee.update_in_company_badge_index(company, old_badge)
225
+ # - employee.guard_unique_company_badge_index!(company) - manual validation
161
226
  #
162
227
  # @param indexed_class [Class] The class being indexed (e.g., Employee)
163
228
  # @param field [Symbol] The field to index (e.g., :badge_number)
164
- # @param target_class [Class] The parent class (e.g., Company)
229
+ # @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
165
230
  # @param index_name [Symbol] Name of the index (e.g., :badge_index)
166
- def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
167
- target_class_config = target_class.config_name
231
+ def generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
232
+ scope_class_config = scope_class.config_name
168
233
  indexed_class.class_eval do
169
- method_name = :"add_to_#{target_class_config}_#{index_name}"
170
- Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
234
+ method_name = :"add_to_#{scope_class_config}_#{index_name}"
235
+ Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
171
236
 
172
- define_method(method_name) do |target_instance|
173
- return unless target_instance
237
+ define_method(method_name) do |scope_instance|
238
+ return unless scope_instance
174
239
 
175
240
  field_value = send(field)
176
241
  return unless field_value
177
242
 
178
- # Use declared field accessor on target instance
179
- index_hash = target_instance.send(index_name)
243
+ # Automatically validate uniqueness before adding to index.
244
+ # Skip validation inside transactions since guard methods require read
245
+ # operations not available in MULTI/EXEC blocks.
246
+ unless Fiber[:familia_transaction]
247
+ guard_method = :"guard_unique_#{scope_class_config}_#{index_name}!"
248
+ send(guard_method, scope_instance) if respond_to?(guard_method)
249
+ end
250
+
251
+ # Use declared field accessor on scope instance
252
+ index_hash = scope_instance.send(index_name)
180
253
 
181
- # Use HashKey DataType method
254
+ # Set the value (guard already validated uniqueness)
182
255
  index_hash[field_value.to_s] = identifier
183
256
  end
184
257
 
185
- method_name = :"remove_from_#{target_class_config}_#{index_name}"
186
- Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
258
+ # Add a guard method to enforce unique constraint on this instance-scoped index
259
+ #
260
+ # @param scope_instance [Object] The scope instance providing uniqueness context (e.g., a Company)
261
+ # @raise [Familia::RecordExistsError] if a record with the same field value
262
+ # exists in the scope's index. Values are compared as strings.
263
+ # @return [void]
264
+ #
265
+ # @example
266
+ # employee.guard_unique_company_badge_index!(company)
267
+ #
268
+ method_name = :"guard_unique_#{scope_class_config}_#{index_name}!"
269
+ Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
270
+
271
+ define_method(method_name) do |scope_instance|
272
+ return unless scope_instance
187
273
 
188
- define_method(method_name) do |target_instance|
189
- return unless target_instance
274
+ field_value = send(field)
275
+ return unless field_value
276
+
277
+ # Use declared field accessor on scope instance
278
+ index_hash = scope_instance.send(index_name)
279
+ existing_id = index_hash.get(field_value.to_s)
280
+
281
+ if existing_id && existing_id != identifier
282
+ raise Familia::RecordExistsError,
283
+ "#{self.class} exists in #{scope_instance.class} with #{field}=#{field_value}"
284
+ end
285
+ end
286
+
287
+ method_name = :"remove_from_#{scope_class_config}_#{index_name}"
288
+ Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
289
+
290
+ define_method(method_name) do |scope_instance|
291
+ return unless scope_instance
190
292
 
191
293
  field_value = send(field)
192
294
  return unless field_value
193
295
 
194
- # Use declared field accessor on target instance
195
- index_hash = target_instance.send(index_name)
296
+ # Use declared field accessor on scope instance
297
+ index_hash = scope_instance.send(index_name)
196
298
 
197
299
  # Remove using HashKey DataType method
198
300
  index_hash.remove(field_value.to_s)
199
301
  end
200
302
 
201
- method_name = :"update_in_#{target_class_config}_#{index_name}"
202
- Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
303
+ method_name = :"update_in_#{scope_class_config}_#{index_name}"
304
+ Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
203
305
 
204
- define_method(method_name) do |target_instance, old_field_value = nil|
205
- return unless target_instance
306
+ define_method(method_name) do |scope_instance, old_field_value = nil|
307
+ return unless scope_instance
206
308
 
207
309
  new_field_value = send(field)
208
310
 
209
311
  # Use Familia's transaction method for atomicity with DataType abstraction
210
- target_instance.transaction do |_tx|
211
- # Use declared field accessor on target instance
212
- index_hash = target_instance.send(index_name)
312
+ scope_instance.transaction do |_tx|
313
+ # Use declared field accessor on scope instance
314
+ index_hash = scope_instance.send(index_name)
213
315
 
214
316
  # Remove old value if provided
215
317
  index_hash.remove(old_field_value.to_s) if old_field_value
@@ -228,10 +330,12 @@ module Familia
228
330
  # - Employee.email_index
229
331
  # - Employee.rebuild_email_index
230
332
  def generate_query_methods_class(field, index_name, indexed_class)
333
+ # Generate class-level single record method
231
334
  indexed_class.define_singleton_method(:"find_by_#{field}") do |provided_id|
232
- index_hash = send(index_name) # Access the class-level hashkey DataType
335
+ index_hash = send(index_name) # access the class-level hashkey DataType
233
336
 
234
- # Get the identifier from the hash using .get method.
337
+ # Get the identifier from the db hashkey using .get method.
338
+ #
235
339
  # We use .get instead of [] because it's part of the standard interface
236
340
  # common across all DataType classes (List, UnsortedSet, SortedSet, HashKey).
237
341
  # While unique indexes always use HashKey, using .get maintains consistency
@@ -245,12 +349,18 @@ module Familia
245
349
 
246
350
  # Generate class-level bulk query method
247
351
  indexed_class.define_singleton_method(:"find_all_by_#{field}") do |provided_ids|
248
- provided_ids = Array(provided_ids)
352
+ # Convert to array and filter nil inputs before querying Redis.
353
+ # This prevents wasteful lookups for empty string keys (nil.to_s → "").
354
+ # Output may contain fewer elements than input (standard ORM behavior).
355
+ provided_ids = Array(provided_ids).compact
249
356
  return [] if provided_ids.empty?
250
357
 
251
- index_hash = send(index_name) # Access the class-level hashkey DataType
358
+ index_hash = send(index_name) # access the class-level hashkey DataType
359
+
360
+ # Get multiple identifiers from the db hashkey using .values_at
252
361
  record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
253
- # Filter out nil values and instantiate objects
362
+
363
+ # Filter out nil values (non-existent records) and instantiate objects
254
364
  record_ids.compact.map { |record_id|
255
365
  indexed_class.find_by_identifier(record_id)
256
366
  }
@@ -260,15 +370,26 @@ module Familia
260
370
  # No need to manually create it - Horreum handles this automatically
261
371
 
262
372
  # Generate method to rebuild the class-level index
263
- indexed_class.define_singleton_method(:"rebuild_#{index_name}") do
264
- index_hash = send(index_name) # Access the class-level hashkey DataType
265
-
266
- # Clear existing index using DataType method
267
- index_hash.clear
268
-
269
- # Rebuild from all existing objects
270
- # This would need to scan through all objects of this class
271
- # Implementation depends on how objects are stored/tracked
373
+ indexed_class.define_singleton_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
374
+ if respond_to?(:instances)
375
+ # Strategy 1: Use instances collection (fastest)
376
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_instances(
377
+ self, # indexed_class (e.g., User)
378
+ field, # e.g., :email
379
+ :"add_to_class_#{index_name}", # e.g., :add_to_class_email_lookup
380
+ batch_size: batch_size,
381
+ &progress_block
382
+ )
383
+ else
384
+ # Strategy 3: Fall back to SCAN
385
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_scan(
386
+ self,
387
+ field,
388
+ :"add_to_class_#{index_name}",
389
+ batch_size: batch_size,
390
+ &progress_block
391
+ )
392
+ end
272
393
  end
273
394
  end
274
395
 
@@ -285,9 +406,28 @@ module Familia
285
406
 
286
407
  return unless field_value
287
408
 
409
+ # Just set the value - uniqueness should be validated before save
288
410
  index_hash[field_value.to_s] = identifier
289
411
  end
290
412
 
413
+ # Add a guard method to enforce unique constraint on this specific index
414
+ #
415
+ # @raise [Familia::RecordExistsError] if a record with the same
416
+ # field value exists. Values are compared as strings.
417
+ #
418
+ # @return [void]
419
+ define_method(:"guard_unique_#{index_name}!") do
420
+ field_value = send(field)
421
+ return unless field_value
422
+
423
+ index_hash = self.class.send(index_name)
424
+ existing_id = index_hash.get(field_value.to_s)
425
+
426
+ if existing_id && existing_id != identifier
427
+ raise Familia::RecordExistsError, "#{self.class} exists #{field}=#{field_value}"
428
+ end
429
+ end
430
+
291
431
  define_method(:"remove_from_class_#{index_name}") do
292
432
  index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
293
433
  field_value = send(field)
@@ -1,8 +1,11 @@
1
1
  # lib/familia/features/relationships/indexing.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'indexing_relationship'
4
6
  require_relative 'indexing/multi_index_generators'
5
7
  require_relative 'indexing/unique_index_generators'
8
+ require_relative 'indexing/rebuild_strategies'
6
9
 
7
10
  module Familia
8
11
  module Features
@@ -50,7 +53,7 @@ module Familia
50
53
  # Terminology:
51
54
  # - unique_index: 1:1 field-to-object mapping (HashKey)
52
55
  # - multi_index: 1:many field-to-objects mapping (UnsortedSet, no scores)
53
- # - within: parent class for instance-scoped indexes
56
+ # - within: scope class providing uniqueness boundary for instance-scoped indexes
54
57
  # - query: whether to generate find_by_* methods (default: true)
55
58
  #
56
59
  # Key Patterns:
@@ -89,7 +92,7 @@ module Familia
89
92
  #
90
93
  # @param field [Symbol] The field to index on
91
94
  # @param index_name [Symbol] Name of the index
92
- # @param within [Class, Symbol] The parent class that owns the index
95
+ # @param within [Class, Symbol] The scope class providing uniqueness context
93
96
  # @param query [Boolean] Whether to generate query methods
94
97
  #
95
98
  # @example Instance-scoped multi-value indexing
@@ -109,7 +112,7 @@ module Familia
109
112
  #
110
113
  # @param field [Symbol] The field to index on
111
114
  # @param index_name [Symbol] Name of the index hash
112
- # @param within [Class, Symbol] Optional parent class for instance-scoped unique index
115
+ # @param within [Class, Symbol] Optional scope class for instance-scoped unique index
113
116
  # @param query [Boolean] Whether to generate query methods
114
117
  #
115
118
  # @example Class-level unique index
@@ -136,70 +139,68 @@ module Familia
136
139
 
137
140
  # Ensure proper DataType field is declared for index
138
141
  # Similar to ensure_collection_field in participation system
139
- def ensure_index_field(target_class, index_name, field_type)
140
- return if target_class.method_defined?(index_name) || target_class.respond_to?(index_name)
142
+ def ensure_index_field(scope_class, index_name, field_type)
143
+ return if scope_class.method_defined?(index_name) || scope_class.respond_to?(index_name)
141
144
 
142
- target_class.send(field_type, index_name)
145
+ scope_class.send(field_type, index_name)
143
146
  end
144
147
  end
145
148
 
146
149
  # Instance methods for indexed objects
147
150
  module ModelInstanceMethods
148
- # Update all indexes for a given parent context
149
- # For class-level indexes (class_indexed_by), parent_context should be nil
150
- # For relationship indexes (indexed_by), parent_context should be the parent instance
151
- def update_all_indexes(old_values = {}, parent_context = nil)
151
+ # Update all indexes for a given scope context
152
+ # For class-level indexes (unique_index without within:), scope_context should be nil
153
+ # For instance-scoped indexes (with within:), scope_context should be the scope instance
154
+ def update_all_indexes(old_values = {}, scope_context = nil)
152
155
  return unless self.class.respond_to?(:indexing_relationships)
153
156
 
154
157
  self.class.indexing_relationships.each do |config|
155
158
  field = config.field
156
159
  index_name = config.index_name
157
- target_class = config.target_class
158
160
  old_field_value = old_values[field]
159
161
 
160
162
  # Determine which update method to call
161
- if target_class == self.class
163
+ if config.within.nil?
162
164
  # Class-level index (unique_index without within:)
163
165
  send("update_in_class_#{index_name}", old_field_value)
164
166
  else
165
- # Relationship index (unique_index or multi_index with within:) - requires parent context
166
- next unless parent_context
167
+ # Instance-scoped index (unique_index or multi_index with within:) - requires scope context
168
+ next unless scope_context
167
169
 
168
170
  # Use config_name for method naming
169
- target_class_config = Familia.resolve_class(config.target_class).config_name
170
- send("update_in_#{target_class_config}_#{index_name}", parent_context, old_field_value)
171
+ scope_class_config = Familia.resolve_class(config.scope_class).config_name
172
+ send("update_in_#{scope_class_config}_#{index_name}", scope_context, old_field_value)
171
173
  end
172
174
  end
173
175
  end
174
176
 
175
- # Remove from all indexes for a given parent context
176
- # For class-level indexes (class_indexed_by), parent_context should be nil
177
- # For relationship indexes (indexed_by), parent_context should be the parent instance
178
- def remove_from_all_indexes(parent_context = nil)
177
+ # Remove from all indexes for a given scope context
178
+ # For class-level indexes (unique_index without within:), scope_context should be nil
179
+ # For instance-scoped indexes (with within:), scope_context should be the scope instance
180
+ def remove_from_all_indexes(scope_context = nil)
179
181
  return unless self.class.respond_to?(:indexing_relationships)
180
182
 
181
183
  self.class.indexing_relationships.each do |config|
182
184
  index_name = config.index_name
183
- target_class = config.target_class
184
185
 
185
186
  # Determine which remove method to call
186
- if target_class == self.class
187
+ if config.within.nil?
187
188
  # Class-level index (unique_index without within:)
188
189
  send("remove_from_class_#{index_name}")
189
190
  else
190
- # Relationship index (unique_index or multi_index with within:) - requires parent context
191
- next unless parent_context
191
+ # Instance-scoped index (unique_index or multi_index with within:) - requires scope context
192
+ next unless scope_context
192
193
 
193
194
  # Use config_name for method naming
194
- target_class_config = Familia.resolve_class(config.target_class).config_name
195
- send("remove_from_#{target_class_config}_#{index_name}", parent_context)
195
+ scope_class_config = Familia.resolve_class(config.scope_class).config_name
196
+ send("remove_from_#{scope_class_config}_#{index_name}", scope_context)
196
197
  end
197
198
  end
198
199
  end
199
200
 
200
201
  # Get all indexes this object appears in
201
- # Note: For target-scoped indexes, this only shows class-level indexes
202
- # since target-scoped indexes require a specific target instance
202
+ # Note: For instance-scoped indexes, this only shows class-level indexes
203
+ # since instance-scoped indexes require a specific scope instance
203
204
  #
204
205
  # @return [Array<Hash>] Array of index information
205
206
  def current_indexings
@@ -210,19 +211,18 @@ module Familia
210
211
  self.class.indexing_relationships.each do |config|
211
212
  field = config.field
212
213
  index_name = config.index_name
213
- target_class = config.target_class
214
214
  cardinality = config.cardinality
215
215
  field_value = send(field)
216
216
 
217
217
  next unless field_value
218
218
 
219
- if target_class == self.class
219
+ if config.within.nil?
220
220
  # Class-level index (unique_index without within:) - check hash key using DataType
221
221
  index_hash = self.class.send(index_name)
222
222
  next unless index_hash.key?(field_value.to_s)
223
223
 
224
224
  memberships << {
225
- target_class: 'class',
225
+ scope_class: 'class',
226
226
  index_name: index_name,
227
227
  field: field,
228
228
  field_value: field_value,
@@ -231,17 +231,17 @@ module Familia
231
231
  type: 'unique_index',
232
232
  }
233
233
  else
234
- # Instance-scoped index (unique_index or multi_index with within:) - cannot check without target instance
235
- # This would require scanning all possible target instances
234
+ # Instance-scoped index (unique_index or multi_index with within:) - cannot check without scope instance
235
+ # This would require scanning all possible scope instances
236
236
  memberships << {
237
- target_class: config.target_class_config_name,
237
+ scope_class: config.scope_class_config_name,
238
238
  index_name: index_name,
239
239
  field: field,
240
240
  field_value: field_value,
241
- index_key: 'target_dependent',
241
+ index_key: 'scope_dependent',
242
242
  cardinality: cardinality,
243
243
  type: cardinality == :unique ? 'unique_index' : 'multi_index',
244
- note: 'Requires target instance for verification',
244
+ note: 'Requires scope instance for verification',
245
245
  }
246
246
  end
247
247
  end
@@ -249,9 +249,9 @@ module Familia
249
249
  memberships
250
250
  end
251
251
 
252
- # Check if this object is indexed in a specific target
252
+ # Check if this object is indexed in a specific scope
253
253
  # For class-level indexes, checks the hash key
254
- # For target-scoped indexes, returns false (requires target instance)
254
+ # For instance-scoped indexes, returns false (requires scope instance)
255
255
  def indexed_in?(index_name)
256
256
  return false unless self.class.respond_to?(:indexing_relationships)
257
257
 
@@ -262,14 +262,12 @@ module Familia
262
262
  field_value = send(field)
263
263
  return false unless field_value
264
264
 
265
- target_class = config.target_class
266
-
267
- if target_class == self.class
265
+ if config.within.nil?
268
266
  # Class-level index (class_indexed_by) - check hash key using DataType
269
267
  index_hash = self.class.send(index_name)
270
268
  index_hash.key?(field_value.to_s)
271
269
  else
272
- # Target-scoped index (indexed_by) - cannot verify without target instance
270
+ # Instance-scoped index (with within:) - cannot verify without scope instance
273
271
  false
274
272
  end
275
273
  end
@@ -1,3 +1,5 @@
1
+ # lib/familia/features/relationships/indexing_relationship.rb
2
+ #
1
3
  # frozen_string_literal: true
2
4
 
3
5
  module Familia
@@ -14,20 +16,30 @@ module Familia
14
16
  # Similar to ParticipationRelationship but for attribute-based lookups
15
17
  # rather than collection membership.
16
18
  #
19
+ # Terminology:
20
+ # - `scope_class`: The class that provides the uniqueness boundary for
21
+ # instance-scoped indexes. For example, in `unique_index :badge_number,
22
+ # :badge_index, within: Company`, the Company is the scope class.
23
+ # - `within`: Preserves the original DSL parameter to explicitly distinguish
24
+ # class-level indexes (within: nil) from instance-scoped indexes (within:
25
+ # SomeClass). This avoids brittle class comparisons and prevents issues
26
+ # with inheritance scenarios.
27
+ #
17
28
  IndexingRelationship = Data.define(
18
29
  :field, # Symbol - field being indexed (e.g., :email, :department)
19
30
  :index_name, # Symbol - name of the index (e.g., :email_index, :dept_index)
20
- :target_class, # Class/Symbol - parent class for instance-scoped indexes (within:)
31
+ :scope_class, # Class/Symbol - scope class for instance-scoped indexes (within:)
32
+ :within, # Class/Symbol/nil - within: parameter (nil for class-level, Class for instance-scoped)
21
33
  :cardinality, # Symbol - :unique (1:1) or :multi (1:many)
22
- :query # Boolean - whether to generate query methods
34
+ :query, # Boolean - whether to generate query methods
23
35
  ) do
24
36
  #
25
- # Get the normalized config name for the target class
37
+ # Get the normalized config name for the scope class
26
38
  #
27
39
  # @return [String] The config name (e.g., "user", "company", "test_company")
28
40
  #
29
- def target_class_config_name
30
- target_class.config_name
41
+ def scope_class_config_name
42
+ scope_class.config_name
31
43
  end
32
44
  end
33
45
  end