familia 2.0.0.pre19 → 2.0.0.pre22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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/.talismanrc +5 -1
  8. data/CHANGELOG.rst +220 -112
  9. data/CLAUDE.md +28 -1
  10. data/Gemfile +1 -1
  11. data/Gemfile.lock +20 -17
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  15. data/docs/guides/encryption.md +486 -0
  16. data/docs/guides/feature-encrypted-fields.md +123 -7
  17. data/docs/guides/feature-expiration.md +161 -117
  18. data/docs/guides/feature-external-identifiers.md +415 -443
  19. data/docs/guides/feature-object-identifiers.md +400 -269
  20. data/docs/guides/feature-quantization.md +120 -6
  21. data/docs/guides/feature-relationships-indexing.md +318 -0
  22. data/docs/guides/feature-relationships-methods.md +146 -604
  23. data/docs/guides/feature-relationships-participation.md +263 -0
  24. data/docs/guides/feature-relationships.md +118 -136
  25. data/docs/guides/feature-system-devs.md +176 -693
  26. data/docs/guides/feature-system.md +119 -6
  27. data/docs/guides/feature-transient-fields.md +81 -0
  28. data/docs/guides/field-system.md +778 -0
  29. data/docs/guides/index.md +32 -15
  30. data/docs/guides/logging.md +187 -0
  31. data/docs/guides/optimized-loading.md +674 -0
  32. data/docs/guides/thread-safety-monitoring.md +61 -0
  33. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  34. data/docs/migrating/v2.0.0-pre22.md +241 -0
  35. data/docs/overview.md +7 -9
  36. data/docs/reference/api-technical.md +267 -320
  37. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  38. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  39. data/examples/autoloader/mega_customer.rb +2 -0
  40. data/examples/datatype_standalone.rb +4 -3
  41. data/examples/encrypted_fields.rb +2 -1
  42. data/examples/json_usage_patterns.rb +2 -0
  43. data/examples/relationships.rb +3 -0
  44. data/examples/safe_dump.rb +2 -1
  45. data/examples/sampling_demo.rb +53 -0
  46. data/examples/single_connection_transaction_confusions.rb +2 -1
  47. data/familia.gemspec +2 -1
  48. data/lib/familia/base.rb +2 -0
  49. data/lib/familia/connection/behavior.rb +2 -0
  50. data/lib/familia/connection/handlers.rb +2 -0
  51. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  52. data/lib/familia/connection/middleware.rb +34 -24
  53. data/lib/familia/connection/operation_core.rb +3 -2
  54. data/lib/familia/connection/operations.rb +2 -0
  55. data/lib/familia/connection/pipelined_core.rb +3 -3
  56. data/lib/familia/connection/transaction_core.rb +69 -2
  57. data/lib/familia/connection.rb +18 -3
  58. data/lib/familia/data_type/class_methods.rb +3 -1
  59. data/lib/familia/data_type/connection.rb +2 -0
  60. data/lib/familia/data_type/database_commands.rb +2 -0
  61. data/lib/familia/data_type/serialization.rb +79 -52
  62. data/lib/familia/data_type/settings.rb +2 -0
  63. data/lib/familia/data_type/types/counter.rb +2 -0
  64. data/lib/familia/data_type/types/hashkey.rb +7 -5
  65. data/lib/familia/data_type/types/listkey.rb +2 -0
  66. data/lib/familia/data_type/types/lock.rb +2 -0
  67. data/lib/familia/data_type/types/sorted_set.rb +7 -10
  68. data/lib/familia/data_type/types/stringkey.rb +24 -0
  69. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  70. data/lib/familia/data_type.rb +2 -0
  71. data/lib/familia/encryption/encrypted_data.rb +4 -2
  72. data/lib/familia/encryption/manager.rb +2 -0
  73. data/lib/familia/encryption/provider.rb +2 -0
  74. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  75. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  76. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  77. data/lib/familia/encryption/registry.rb +2 -0
  78. data/lib/familia/encryption/request_cache.rb +2 -0
  79. data/lib/familia/encryption.rb +9 -2
  80. data/lib/familia/errors.rb +2 -0
  81. data/lib/familia/features/autoloader.rb +2 -0
  82. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  83. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  84. data/lib/familia/features/encrypted_fields.rb +2 -2
  85. data/lib/familia/features/expiration/extensions.rb +3 -1
  86. data/lib/familia/features/expiration.rb +12 -4
  87. data/lib/familia/features/external_identifier.rb +62 -7
  88. data/lib/familia/features/object_identifier.rb +49 -0
  89. data/lib/familia/features/quantization.rb +3 -1
  90. data/lib/familia/features/relationships/README.md +3 -1
  91. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  92. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +138 -9
  93. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +97 -21
  95. data/lib/familia/features/relationships/indexing.rb +3 -0
  96. data/lib/familia/features/relationships/indexing_relationship.rb +3 -1
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  98. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  100. data/lib/familia/features/relationships/participation.rb +155 -69
  101. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  102. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  103. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  104. data/lib/familia/features/relationships.rb +5 -3
  105. data/lib/familia/features/safe_dump.rb +2 -0
  106. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  107. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  108. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  109. data/lib/familia/features/transient_fields.rb +2 -0
  110. data/lib/familia/features.rb +2 -0
  111. data/lib/familia/field_type.rb +3 -1
  112. data/lib/familia/horreum/connection.rb +17 -1
  113. data/lib/familia/horreum/database_commands.rb +8 -1
  114. data/lib/familia/horreum/definition.rb +16 -6
  115. data/lib/familia/horreum/management.rb +353 -52
  116. data/lib/familia/horreum/persistence.rb +179 -108
  117. data/lib/familia/horreum/related_fields.rb +2 -0
  118. data/lib/familia/horreum/serialization.rb +23 -4
  119. data/lib/familia/horreum/settings.rb +2 -0
  120. data/lib/familia/horreum/utils.rb +2 -0
  121. data/lib/familia/horreum.rb +15 -1
  122. data/lib/familia/identifier_extractor.rb +3 -1
  123. data/lib/familia/instrumentation.rb +156 -0
  124. data/lib/familia/json_serializer.rb +2 -0
  125. data/lib/familia/logging.rb +92 -32
  126. data/lib/familia/refinements/dear_json.rb +2 -0
  127. data/lib/familia/refinements/stylize_words.rb +2 -14
  128. data/lib/familia/refinements/time_literals.rb +2 -0
  129. data/lib/familia/refinements.rb +2 -0
  130. data/lib/familia/secure_identifier.rb +10 -2
  131. data/lib/familia/settings.rb +2 -0
  132. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  133. data/lib/familia/thread_safety/monitor.rb +328 -0
  134. data/lib/familia/utils.rb +13 -0
  135. data/lib/familia/verifiable_identifier.rb +3 -1
  136. data/lib/familia/version.rb +3 -1
  137. data/lib/familia.rb +31 -4
  138. data/lib/middleware/database_command_counter.rb +152 -0
  139. data/lib/middleware/database_logger.rb +295 -170
  140. data/lib/multi_result.rb +61 -31
  141. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  142. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  143. data/try/edge_cases/json_serialization_try.rb +2 -0
  144. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  145. data/try/edge_cases/race_conditions_try.rb +4 -0
  146. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  147. data/try/edge_cases/string_coercion_try.rb +2 -0
  148. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  149. data/try/features/count_any_edge_cases_try.rb +486 -0
  150. data/try/features/count_any_methods_try.rb +197 -0
  151. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  152. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  153. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  154. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  155. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  156. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  157. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  158. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  159. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  160. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  161. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  162. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  164. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  165. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  166. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  167. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  168. data/try/features/encryption/config_persistence_try.rb +4 -0
  169. data/try/features/encryption/core_try.rb +4 -0
  170. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  171. data/try/features/encryption/module_loading_try.rb +4 -0
  172. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  173. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  174. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  175. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  176. data/try/features/expiration/expiration_try.rb +4 -0
  177. data/try/features/external_identifier/external_identifier_try.rb +305 -8
  178. data/try/features/feature_dependencies_try.rb +2 -0
  179. data/try/features/feature_improvements_try.rb +2 -0
  180. data/try/features/field_groups_try.rb +2 -0
  181. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  182. data/try/features/object_identifier/object_identifier_try.rb +140 -0
  183. data/try/features/quantization/quantization_try.rb +4 -0
  184. data/try/features/real_feature_integration_try.rb +2 -0
  185. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  186. data/try/features/relationships/indexing_rebuild_try.rb +606 -0
  187. data/try/features/relationships/indexing_try.rb +2 -0
  188. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  189. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  190. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  192. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  193. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  194. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  195. data/try/features/relationships/relationships_api_changes_try.rb +2 -0
  196. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  197. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  198. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  199. data/try/features/relationships/relationships_performance_try.rb +4 -0
  200. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  201. data/try/features/relationships/relationships_try.rb +6 -4
  202. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  203. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  204. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  205. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  206. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  207. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  208. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  209. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  210. data/try/integration/connection/fiber_context_preservation_try.rb +4 -0
  211. data/try/integration/connection/handler_constraints_try.rb +4 -0
  212. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  213. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  214. data/try/integration/connection/operation_mode_guards_try.rb +4 -0
  215. data/try/integration/connection/pipeline_fallback_integration_try.rb +3 -0
  216. data/try/integration/connection/pools_try.rb +4 -0
  217. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  218. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  219. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  220. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  221. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  222. data/try/integration/connection/transaction_modes_try.rb +4 -0
  223. data/try/integration/conventional_inheritance_try.rb +4 -0
  224. data/try/integration/create_method_try.rb +4 -0
  225. data/try/integration/cross_component_try.rb +4 -0
  226. data/try/integration/data_types/datatype_pipelines_try.rb +9 -3
  227. data/try/integration/data_types/datatype_transactions_try.rb +17 -7
  228. data/try/integration/database_consistency_try.rb +4 -0
  229. data/try/integration/familia_extended_try.rb +4 -0
  230. data/try/integration/familia_members_methods_try.rb +4 -0
  231. data/try/integration/models/customer_safe_dump_try.rb +4 -0
  232. data/try/integration/models/customer_try.rb +7 -3
  233. data/try/integration/models/datatype_base_try.rb +4 -0
  234. data/try/integration/models/familia_object_try.rb +4 -0
  235. data/try/integration/persistence_operations_try.rb +4 -0
  236. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  237. data/try/integration/save_methods_consistency_try.rb +241 -0
  238. data/try/integration/scenarios_try.rb +4 -0
  239. data/try/integration/secure_identifier_try.rb +4 -0
  240. data/try/integration/transaction_safety_core_try.rb +176 -0
  241. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  242. data/try/integration/verifiable_identifier_try.rb +4 -0
  243. data/try/investigation/pipeline_routing/README.md +228 -0
  244. data/try/performance/benchmarks_try.rb +4 -0
  245. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  246. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  247. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  248. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  249. data/try/support/debugging/debug_aad_process.rb +3 -0
  250. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  251. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  252. data/try/support/debugging/debug_context_aad.rb +3 -0
  253. data/try/support/debugging/debug_context_simple.rb +3 -0
  254. data/try/support/debugging/debug_cross_context.rb +3 -0
  255. data/try/support/debugging/debug_database_load.rb +3 -0
  256. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  257. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  258. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  259. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  260. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  261. data/try/support/debugging/debug_load_path.rb +3 -0
  262. data/try/support/debugging/debug_method_definition.rb +3 -0
  263. data/try/support/debugging/debug_method_resolution.rb +3 -0
  264. data/try/support/debugging/debug_minimal.rb +3 -0
  265. data/try/support/debugging/debug_provider.rb +3 -0
  266. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  267. data/try/support/debugging/debug_string_class.rb +3 -0
  268. data/try/support/debugging/debug_test.rb +3 -0
  269. data/try/support/debugging/debug_test_design.rb +3 -0
  270. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  271. data/try/support/debugging/provider_diagnostics.rb +4 -0
  272. data/try/support/helpers/test_cleanup.rb +4 -0
  273. data/try/support/helpers/test_helpers.rb +5 -0
  274. data/try/support/memory/memory_basic_test.rb +4 -0
  275. data/try/support/memory/memory_detailed_test.rb +4 -0
  276. data/try/support/memory/memory_search_for_string.rb +4 -0
  277. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  278. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  279. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  280. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  281. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  282. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  283. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  284. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  285. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  286. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  287. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  288. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  289. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  290. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  291. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  292. data/try/thread_safety/README.md +496 -0
  293. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  294. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  295. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  296. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  297. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  298. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  299. data/try/thread_safety/field_registration_race_try.rb +222 -0
  300. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  301. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  302. data/try/thread_safety/module_config_race_try.rb +175 -0
  303. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  304. data/try/unit/core/autoloader_try.rb +4 -0
  305. data/try/unit/core/base_enhancements_try.rb +4 -0
  306. data/try/unit/core/connection_try.rb +4 -0
  307. data/try/unit/core/errors_try.rb +4 -0
  308. data/try/unit/core/extensions_try.rb +4 -0
  309. data/try/unit/core/familia_logger_try.rb +2 -0
  310. data/try/unit/core/familia_try.rb +4 -0
  311. data/try/unit/core/middleware_sampling_try.rb +335 -0
  312. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  313. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  314. data/try/unit/core/middleware_try.rb +4 -0
  315. data/try/unit/core/settings_try.rb +4 -0
  316. data/try/unit/core/time_utils_try.rb +4 -0
  317. data/try/unit/core/tools_try.rb +4 -0
  318. data/try/unit/core/utils_try.rb +37 -0
  319. data/try/unit/data_types/boolean_try.rb +39 -22
  320. data/try/unit/data_types/counter_try.rb +4 -0
  321. data/try/unit/data_types/datatype_base_try.rb +4 -0
  322. data/try/unit/data_types/hash_try.rb +6 -2
  323. data/try/unit/data_types/list_try.rb +4 -0
  324. data/try/unit/data_types/lock_try.rb +4 -0
  325. data/try/unit/data_types/serialization_try.rb +386 -0
  326. data/try/unit/data_types/sorted_set_try.rb +4 -0
  327. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  328. data/try/unit/data_types/string_try.rb +4 -0
  329. data/try/unit/data_types/unsortedset_try.rb +4 -0
  330. data/try/unit/familia_resolve_class_try.rb +116 -0
  331. data/try/unit/horreum/auto_indexing_on_save_try.rb +5 -1
  332. data/try/unit/horreum/automatic_index_validation_try.rb +2 -0
  333. data/try/unit/horreum/base_try.rb +4 -0
  334. data/try/unit/horreum/class_methods_try.rb +4 -0
  335. data/try/unit/horreum/commands_try.rb +4 -0
  336. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  337. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +6 -1
  338. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  339. data/try/unit/horreum/field_categories_try.rb +4 -0
  340. data/try/unit/horreum/field_definition_try.rb +4 -0
  341. data/try/unit/horreum/initialization_try.rb +4 -0
  342. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  343. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  344. data/try/unit/horreum/relations_try.rb +4 -0
  345. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  346. data/try/unit/horreum/serialization_try.rb +4 -0
  347. data/try/unit/horreum/settings_try.rb +4 -0
  348. data/try/unit/horreum/unique_index_edge_cases_try.rb +4 -0
  349. data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -0
  350. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  351. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  352. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  353. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  354. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  355. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  356. data/try/unit/thread_safety_monitor_try.rb +149 -0
  357. metadata +69 -17
  358. data/.github/workflows/code-quality.yml +0 -138
  359. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +0 -91
  360. data/changelog.d/20251011_203905_delano_next.rst +0 -30
  361. data/changelog.d/20251011_212633_delano_next.rst +0 -13
  362. data/changelog.d/20251011_221253_delano_next.rst +0 -26
  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
@@ -0,0 +1,479 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ module Features
5
+ module Relationships
6
+ module Indexing
7
+ # RebuildStrategies provides atomic index rebuild operations with zero downtime.
8
+ #
9
+ # All rebuild strategies follow a consistent pattern:
10
+ # 1. Build index in temporary key
11
+ # 2. Batch processing with transactions per batch (not entire rebuild)
12
+ # 3. Atomic swap via Lua script at completion
13
+ # 4. Progress callbacks throughout
14
+ #
15
+ # This ensures:
16
+ # - Zero downtime during rebuild (live index remains available)
17
+ # - Memory efficiency (batch processing)
18
+ # - Consistent progress reporting
19
+ # - Safe failure handling (temp key abandoned on error)
20
+ #
21
+ # @example Via instances collection
22
+ # RebuildStrategies.rebuild_via_instances(
23
+ # User,
24
+ # :email,
25
+ # :add_to_email_index,
26
+ # batch_size: 100
27
+ # ) { |progress| puts "Processed: #{progress[:completed]}/#{progress[:total]}" }
28
+ #
29
+ # @example Via participation relationship
30
+ # RebuildStrategies.rebuild_via_participation(
31
+ # company,
32
+ # Employee,
33
+ # :department,
34
+ # :add_to_company_dept_index,
35
+ # company.employees_collection,
36
+ # batch_size: 100
37
+ # )
38
+ #
39
+ # @example Via SCAN (fallback for complex scenarios)
40
+ # RebuildStrategies.rebuild_via_scan(
41
+ # User,
42
+ # :email,
43
+ # :add_to_email_index,
44
+ # batch_size: 100
45
+ # )
46
+ #
47
+ module RebuildStrategies
48
+ module_function
49
+
50
+ # Rebuilds index by loading objects from ModelClass.instances sorted set.
51
+ #
52
+ # This is the preferred strategy for models with class-level indexes that
53
+ # maintain an instances collection. It's efficient because:
54
+ # - Direct access to all object identifiers via ZRANGE
55
+ # - Bulk loading via load_multi
56
+ # - No key pattern matching required
57
+ #
58
+ # Process:
59
+ # 1. Enumerate identifiers from ModelClass.instances.members
60
+ # 2. Load objects in batches via load_multi(identifiers).compact
61
+ # 3. Build temp index via transactions (one per batch)
62
+ # 4. Atomic swap temp -> final key via Lua
63
+ #
64
+ # @param indexed_class [Class] The model class being indexed (e.g., User)
65
+ # @param field [Symbol] The field to index (e.g., :email)
66
+ # @param add_method [Symbol] The mutation method to call (e.g., :add_to_email_index)
67
+ # @param batch_size [Integer] Number of objects per batch (default: 100)
68
+ # @yield [Hash] Progress info: {completed:, total:, rate:, elapsed:}
69
+ # @return [Integer] Number of objects processed
70
+ #
71
+ # @example Rebuild user email index
72
+ # count = RebuildStrategies.rebuild_via_instances(
73
+ # User,
74
+ # :email,
75
+ # :add_to_email_index,
76
+ # batch_size: 100
77
+ # ) { |p| puts "#{p[:completed]}/#{p[:total]} (#{p[:rate]}/s)" }
78
+ #
79
+ def rebuild_via_instances(indexed_class, field, add_method, index_hashkey, batch_size: 100, &progress)
80
+ unless indexed_class.respond_to?(:instances)
81
+ raise ArgumentError, "#{indexed_class.name} does not have an instances collection"
82
+ end
83
+
84
+ instances = indexed_class.instances
85
+ total = instances.size
86
+ start_time = Familia.now
87
+
88
+ Familia.info "[Rebuild] Starting via_instances for #{indexed_class.name}.#{field} (#{total} objects)"
89
+
90
+ # Determine the final index key by examining the class-level index
91
+ # Extract index name from add_method (e.g., add_to_email_index -> email_index)
92
+ # or add_to_class_email_index -> email_index
93
+ index_name = add_method.to_s.gsub(/^(add_to|update_in|remove_from)_(class_)?/, '')
94
+
95
+ # Access the class-level index directly
96
+ unless indexed_class.respond_to?(index_name)
97
+ raise ArgumentError, "#{indexed_class.name} does not have index accessor: #{index_name}"
98
+ end
99
+
100
+ index_hashkey = indexed_class.send(index_name)
101
+ final_key = index_hashkey.dbkey
102
+ temp_key = RebuildStrategies.build_temp_key(final_key)
103
+
104
+ processed = 0
105
+ indexed_count = 0
106
+
107
+ # Process in batches - use members to get deserialized identifiers
108
+ instances.members.each_slice(batch_size) do |identifiers|
109
+ # Bulk load objects, filtering out nils (deleted/missing objects)
110
+ objects = indexed_class.load_multi(identifiers).compact
111
+
112
+ # Transaction per batch (NOT entire rebuild)
113
+ batch_indexed = 0
114
+ indexed_class.transaction do |tx|
115
+ objects.each do |obj|
116
+ value = obj.send(field)
117
+ # Skip nil/empty field values gracefully
118
+ next unless value && !value.to_s.strip.empty?
119
+
120
+ # For class-level indexes, use HSET with serialized value for consistency
121
+ tx.hset(temp_key, value.to_s, index_hashkey.serialize_value(obj))
122
+ batch_indexed += 1
123
+ end
124
+ end
125
+
126
+ processed += identifiers.size
127
+ indexed_count += batch_indexed
128
+ elapsed = Familia.now - start_time
129
+ rate = processed / elapsed
130
+
131
+ progress&.call(
132
+ completed: processed,
133
+ total: total,
134
+ rate: rate.round(2),
135
+ elapsed: elapsed.round(2)
136
+ )
137
+ end
138
+
139
+ # Atomic swap: temp -> final (ZERO DOWNTIME)
140
+ RebuildStrategies.atomic_swap(temp_key, final_key, indexed_class.dbclient)
141
+
142
+ elapsed = Familia.now - start_time
143
+ Familia.info "[Rebuild] Completed via_instances: #{indexed_count} indexed (#{processed} total) in #{elapsed.round(2)}s"
144
+
145
+ indexed_count
146
+ end
147
+
148
+ # Rebuilds index by loading objects from a participation collection.
149
+ #
150
+ # This strategy is for instance-scoped indexes where objects participate
151
+ # in a parent's collection (e.g., employees in company.employees_collection).
152
+ #
153
+ # Process:
154
+ # 1. Enumerate members from collection (SortedSet, UnsortedSet, or ListKey)
155
+ # 2. Load objects in batches via load_multi(identifiers).compact
156
+ # 3. Build temp index via transactions (one per batch)
157
+ # 4. Atomic swap temp -> final key via Lua
158
+ #
159
+ # @param scope_instance [Object] The parent instance providing scope (e.g., company)
160
+ # @param indexed_class [Class] The model class being indexed (e.g., Employee)
161
+ # @param field [Symbol] The field to index (e.g., :badge_number)
162
+ # @param add_method [Symbol] The mutation method (e.g., :add_to_company_badge_index)
163
+ # @param collection [DataType] The collection containing members (SortedSet/UnsortedSet/ListKey)
164
+ # @param cardinality [Symbol] The index cardinality (:unique or :multi) - must be :unique
165
+ # @param batch_size [Integer] Number of objects per batch (default: 100)
166
+ # @yield [Hash] Progress info: {completed:, total:, rate:, elapsed:}
167
+ # @return [Integer] Number of objects processed
168
+ #
169
+ # @example Rebuild company badge index
170
+ # count = RebuildStrategies.rebuild_via_participation(
171
+ # company,
172
+ # Employee,
173
+ # :badge_number,
174
+ # :add_to_company_badge_index,
175
+ # company.employees_collection,
176
+ # :unique,
177
+ # batch_size: 100
178
+ # )
179
+ #
180
+ def rebuild_via_participation(scope_instance, indexed_class, field, add_method, collection, cardinality, index_hashkey, batch_size: 100, &progress)
181
+ total = collection.size
182
+ start_time = Familia.now
183
+
184
+ scope_class = scope_instance.class.name
185
+ Familia.info "[Rebuild] Starting via_participation for #{scope_class}##{indexed_class.name}.#{field} (#{total} objects)"
186
+
187
+ # Guard: This method only supports unique indexes
188
+ if cardinality != :unique
189
+ raise ArgumentError, <<~ERROR.strip
190
+ rebuild_via_participation only supports unique indexes (cardinality: :unique)
191
+ Received cardinality: #{cardinality.inspect} for field: #{field}
192
+
193
+ Multi-indexes require field-value-specific keys and use specialized 4-phase rebuild logic.
194
+ Use the dedicated rebuild method generated on the scope instance instead.
195
+ ERROR
196
+ end
197
+
198
+ # Build temp key for the unique index.
199
+ #
200
+ # Extract index name from add_method. The add_method follows the pattern:
201
+ # add_to_{scope_class_config}_{index_name}
202
+ #
203
+ # For example:
204
+ # add_to_test_company_badge_index -> badge_index
205
+ # add_to_company_badge_index -> badge_index
206
+ #
207
+ # We need to remove the "add_to_{scope_class_config}_" prefix.
208
+ scope_class_config = scope_instance.class.config_name
209
+ prefix = "add_to_#{scope_class_config}_"
210
+ index_name = add_method.to_s.gsub(/^#{Regexp.escape(prefix)}/, '')
211
+
212
+ # Get the actual index accessor from the scope instance to derive the correct key.
213
+ # This ensures we use the same dbkey as the actual index DataType.
214
+ unless scope_instance.respond_to?(index_name)
215
+ raise ArgumentError, "#{scope_instance.class} does not have index accessor: #{index_name}"
216
+ end
217
+
218
+ index_datatype = scope_instance.send(index_name)
219
+ final_key = index_datatype.dbkey
220
+ temp_key = RebuildStrategies.build_temp_key(final_key)
221
+
222
+ processed = 0
223
+ indexed_count = 0
224
+
225
+ # Process in batches - use members to get deserialized identifiers
226
+ collection.members.each_slice(batch_size) do |identifiers|
227
+ objects = indexed_class.load_multi(identifiers).compact
228
+
229
+ # Transaction per batch
230
+ batch_indexed = 0
231
+ scope_instance.transaction do |tx|
232
+ objects.each do |obj|
233
+ value = obj.send(field)
234
+ next unless value && !value.to_s.strip.empty?
235
+
236
+ # For unique index: HSET temp_key field_value serialized_identifier
237
+ # For multi-index: SADD temp_key:field_value identifier
238
+ tx.hset(temp_key, value.to_s, index_hashkey.serialize_value(obj))
239
+ batch_indexed += 1
240
+ end
241
+ end
242
+
243
+ processed += identifiers.size
244
+ indexed_count += batch_indexed
245
+ elapsed = Familia.now - start_time
246
+ rate = processed / elapsed
247
+
248
+ progress&.call(
249
+ completed: processed,
250
+ total: total,
251
+ rate: rate.round(2),
252
+ elapsed: elapsed.round(2)
253
+ )
254
+ end
255
+
256
+ # Atomic swap
257
+ RebuildStrategies.atomic_swap(temp_key, final_key, scope_instance.dbclient)
258
+
259
+ elapsed = Familia.now - start_time
260
+ Familia.info "[Rebuild] Completed via_participation: #{indexed_count} indexed (#{processed} total) in #{elapsed.round(2)}s"
261
+
262
+ indexed_count
263
+ end
264
+
265
+ # Rebuilds index by scanning all keys matching a pattern.
266
+ #
267
+ # This is the fallback strategy when:
268
+ # - No instances collection available
269
+ # - No participation relationship
270
+ # - Need to rebuild from raw keys
271
+ #
272
+ # Uses SCAN (not KEYS) for memory-efficient iteration. Filters by scope
273
+ # if scope_instance provided.
274
+ #
275
+ # Process:
276
+ # 1. Use redis.scan_each(match: pattern, count: batch_size)
277
+ # 2. Filter by scope_instance if provided
278
+ # 3. Load objects in batches via load_multi_by_keys
279
+ # 4. Build temp index via transactions (one per batch)
280
+ # 5. Atomic swap temp -> final key via Lua
281
+ #
282
+ # @param indexed_class [Class] The model class being indexed
283
+ # @param field [Symbol] The field to index
284
+ # @param add_method [Symbol] The mutation method
285
+ # @param scope_instance [Object, nil] Optional scope for filtering
286
+ # @param batch_size [Integer] Number of keys per SCAN iteration (default: 100)
287
+ # @yield [Hash] Progress info: {completed:, scanned:, rate:, elapsed:}
288
+ # @return [Integer] Number of objects processed
289
+ #
290
+ # @example Rebuild without instances collection
291
+ # count = RebuildStrategies.rebuild_via_scan(
292
+ # User,
293
+ # :email,
294
+ # :add_to_email_index,
295
+ # batch_size: 100
296
+ # )
297
+ #
298
+ def rebuild_via_scan(indexed_class, field, add_method, index_hashkey, scope_instance: nil, batch_size: 100, &progress)
299
+ start_time = Familia.now
300
+
301
+ # Build key pattern for SCAN
302
+ # For instance-scoped indexes, we still scan all objects of indexed_class
303
+ # (not scoped under parent), then filter by scope during processing
304
+ pattern = "#{indexed_class.config_name}:*:object"
305
+
306
+ Familia.info "[Rebuild] Starting via_scan for #{indexed_class.name}.#{field} (pattern: #{pattern})"
307
+ Familia.warn "[Rebuild] Using SCAN fallback - consider adding instances collection for better performance"
308
+
309
+ # Determine final key by examining the index
310
+ # Extract index name from add_method (e.g., add_to_class_email_index -> email_index)
311
+ # For instance-scoped: add_to_rebuild_test_company_badge_index -> badge_index
312
+ index_name = add_method.to_s.gsub(/^(add_to|update_in|remove_from)_(class_)?/, '')
313
+
314
+ # Strip scope class config prefix if present (e.g., rebuild_test_company_badge_index -> badge_index)
315
+ # For instance-scoped indexes, the index lives on scope_instance, not indexed_class
316
+ if scope_instance
317
+ scope_config = scope_instance.class.config_name
318
+ index_name = index_name.gsub(/^#{scope_config}_/, '')
319
+ end
320
+
321
+ # For instance-scoped indexes, check scope_instance for accessor
322
+ # For class-level indexes, check indexed_class
323
+ index_owner = scope_instance || indexed_class
324
+ unless index_owner.respond_to?(index_name)
325
+ raise ArgumentError, "#{index_owner.class.name} does not have index accessor: #{index_name}"
326
+ end
327
+
328
+ index_hashkey = index_owner.send(index_name)
329
+ final_key = index_hashkey.dbkey
330
+ temp_key = RebuildStrategies.build_temp_key(final_key)
331
+
332
+ processed = 0
333
+ indexed_count = 0
334
+ scanned = 0
335
+ redis = indexed_class.dbclient
336
+
337
+ # Use SCAN (not KEYS) for memory efficiency
338
+ batch = []
339
+ redis.scan_each(match: pattern, count: batch_size) do |key|
340
+ batch << key
341
+ scanned += 1
342
+
343
+ # Process in batches
344
+ if batch.size >= batch_size
345
+ batch_indexed = RebuildStrategies.process_scan_batch(batch, indexed_class, field, temp_key, index_hashkey, scope_instance)
346
+ processed += batch.size
347
+ indexed_count += batch_indexed
348
+
349
+ elapsed = Familia.now - start_time
350
+ rate = processed / elapsed
351
+
352
+ progress&.call(
353
+ completed: processed,
354
+ scanned: scanned,
355
+ rate: rate.round(2),
356
+ elapsed: elapsed.round(2)
357
+ )
358
+
359
+ batch.clear
360
+ end
361
+ end
362
+
363
+ # Process remaining batch
364
+ unless batch.empty?
365
+ batch_indexed = RebuildStrategies.process_scan_batch(batch, indexed_class, field, temp_key, index_hashkey, scope_instance)
366
+ processed += batch.size
367
+ indexed_count += batch_indexed
368
+ end
369
+
370
+ # Atomic swap
371
+ RebuildStrategies.atomic_swap(temp_key, final_key, redis)
372
+
373
+ elapsed = Familia.now - start_time
374
+ Familia.info "[Rebuild] Completed via_scan: #{indexed_count} indexed (#{processed} total) in #{elapsed.round(2)}s (scanned: #{scanned})"
375
+
376
+ indexed_count
377
+ end
378
+
379
+ # Processes a batch of keys from SCAN (module_function helper)
380
+ #
381
+ # @param keys [Array<String>] Array of Redis keys
382
+ # @param indexed_class [Class] The model class
383
+ # @param field [Symbol] The field to index
384
+ # @param temp_key [String] The temporary index key
385
+ # @param scope_instance [Object, nil] Optional scope instance. If provided, only objects belonging to this scope will be indexed.
386
+ # @return [Integer] Number of objects indexed in this batch
387
+ #
388
+ def process_scan_batch(keys, indexed_class, field, temp_key, index_hashkey, scope_instance)
389
+ # Load objects by keys
390
+ objects = indexed_class.load_multi_by_keys(keys).compact
391
+
392
+ # For instance-scoped indexes, filter objects by scope
393
+ if scope_instance
394
+ # Get the participation collection for this scope
395
+ participation = indexed_class.participation_relationships.find do |rel|
396
+ rel.target_class == scope_instance.class
397
+ end
398
+
399
+ if participation
400
+ collection_name = participation.collection_name
401
+ scope_collection = scope_instance.send(collection_name)
402
+ # Filter to only objects that belong to this scope
403
+ objects = objects.select { |obj| scope_collection.member?(obj.identifier) }
404
+ end
405
+ end
406
+
407
+ # Transaction per batch
408
+ batch_indexed = 0
409
+ indexed_class.transaction do |tx|
410
+ objects.each do |obj|
411
+ value = obj.send(field)
412
+ next unless value && !value.to_s.strip.empty?
413
+
414
+ tx.hset(temp_key, value.to_s, index_hashkey.serialize_value(obj))
415
+ batch_indexed += 1
416
+ end
417
+ end
418
+ batch_indexed
419
+ rescue StandardError => e
420
+ Familia.warn "[Rebuild] Error processing batch: #{e.message}"
421
+ 0
422
+ end
423
+
424
+ # Builds a temporary key name for atomic swaps
425
+ #
426
+ # @param base_key [String] The final index key
427
+ # @return [String] Temporary key with timestamp suffix
428
+ #
429
+ def build_temp_key(base_key)
430
+ timestamp = Familia.now.to_i
431
+ "#{base_key}:rebuild:#{timestamp}"
432
+ end
433
+
434
+ # Performs atomic swap of temp key to final key.
435
+ #
436
+ # This ensures zero downtime during rebuild:
437
+ # 1. DEL final_key (remove old index)
438
+ # 2. RENAME temp_key final_key (atomically replace)
439
+ #
440
+ # RENAME is atomic, so the old index remains queryable until replaced:
441
+ # - Partial updates
442
+ # - Race conditions
443
+ # - Stale data visibility
444
+ #
445
+ # @param temp_key [String] The temporary key containing rebuilt index
446
+ # @param final_key [String] The live index key
447
+ # @param redis [Redis] The Redis connection
448
+ #
449
+ def atomic_swap(temp_key, final_key, redis)
450
+ # Check if temp key exists first - RENAME fails on non-existent keys
451
+ unless redis.exists(temp_key) > 0
452
+ Familia.info "[Rebuild] No temp key to swap (empty result set)"
453
+ # Just ensure final key is cleared
454
+ redis.del(final_key)
455
+ return
456
+ end
457
+
458
+ # Atomic swap: DEL final key, then RENAME temp -> final
459
+ # RENAME is already atomic, so we just need to clear the final key first
460
+ redis.del(final_key)
461
+ redis.rename(temp_key, final_key)
462
+ Familia.info "[Rebuild] Atomic swap completed: #{temp_key} -> #{final_key}"
463
+ rescue Redis::CommandError => e
464
+ # If temp key doesn't exist, just log and return (already handled above)
465
+ if e.message.include?("no such key")
466
+ Familia.info "[Rebuild] Temp key vanished during swap (concurrent operation?)"
467
+ return
468
+ end
469
+
470
+ # For other errors, preserve temp key for debugging
471
+ Familia.warn "[Rebuild] Atomic swap failed: #{e.message}"
472
+ Familia.warn "[Rebuild] Temp key preserved for debugging: #{temp_key}"
473
+ raise
474
+ end
475
+ end
476
+ end
477
+ end
478
+ end
479
+ end
@@ -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
@@ -104,6 +108,9 @@ module Familia
104
108
  # Ensure the index field is declared (creates accessor that returns DataType)
105
109
  actual_scope_class.send(:ensure_index_field, actual_scope_class, index_name, :hashkey)
106
110
 
111
+ # Get scope_class_config for method naming (needed for rebuild methods)
112
+ scope_class_config = actual_scope_class.config_name
113
+
107
114
  # Generate instance query method (e.g., company.find_by_badge_number)
108
115
  actual_scope_class.class_eval do
109
116
  define_method(:"find_by_#{field}") do |provided_value|
@@ -145,16 +152,70 @@ module Familia
145
152
  # No need to manually define it here
146
153
 
147
154
  # Generate method to rebuild the unique index for this parent instance
148
- define_method(:"rebuild_#{index_name}") do
149
- # Use declared field accessor instead of manual instantiation
150
- 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
151
175
 
152
- # Clear existing index using DataType method
153
- 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
154
188
 
155
- # Rebuild from all existing objects
156
- # This would need to scan through all objects belonging to this parent
157
- # 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
+ index_hashkey = send(index_name) # Get the index HashKey for serialization
195
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
196
+ self, # scope_instance (e.g., company)
197
+ indexed_class, # e.g., Employee
198
+ field, # e.g., :badge_number
199
+ :"add_to_#{scope_class_config}_#{index_name}", # e.g., :add_to_company_badge_index
200
+ collection,
201
+ index_config.cardinality, # :unique or :multi
202
+ index_hashkey, # Pass index for serialization
203
+ batch_size: batch_size,
204
+ &progress_block
205
+ )
206
+ else
207
+ # Strategy 3: Fall back to SCAN with filtering
208
+ index_hashkey = send(index_name) # Get the index HashKey for serialization
209
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_scan(
210
+ indexed_class,
211
+ field,
212
+ :"add_to_#{scope_class_config}_#{index_name}",
213
+ index_hashkey, # Pass index for serialization
214
+ scope_instance: self,
215
+ batch_size: batch_size,
216
+ &progress_block
217
+ )
218
+ end
158
219
  end
159
220
  end
160
221
  end
@@ -175,7 +236,7 @@ module Familia
175
236
  scope_class_config = scope_class.config_name
176
237
  indexed_class.class_eval do
177
238
  method_name = :"add_to_#{scope_class_config}_#{index_name}"
178
- Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
239
+ Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
179
240
 
180
241
  define_method(method_name) do |scope_instance|
181
242
  return unless scope_instance
@@ -209,7 +270,7 @@ module Familia
209
270
  # employee.guard_unique_company_badge_index!(company)
210
271
  #
211
272
  method_name = :"guard_unique_#{scope_class_config}_#{index_name}!"
212
- Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
273
+ Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
213
274
 
214
275
  define_method(method_name) do |scope_instance|
215
276
  return unless scope_instance
@@ -228,7 +289,7 @@ module Familia
228
289
  end
229
290
 
230
291
  method_name = :"remove_from_#{scope_class_config}_#{index_name}"
231
- Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
292
+ Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
232
293
 
233
294
  define_method(method_name) do |scope_instance|
234
295
  return unless scope_instance
@@ -244,7 +305,7 @@ module Familia
244
305
  end
245
306
 
246
307
  method_name = :"update_in_#{scope_class_config}_#{index_name}"
247
- Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
308
+ Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
248
309
 
249
310
  define_method(method_name) do |scope_instance, old_field_value = nil|
250
311
  return unless scope_instance
@@ -313,15 +374,30 @@ module Familia
313
374
  # No need to manually create it - Horreum handles this automatically
314
375
 
315
376
  # Generate method to rebuild the class-level index
316
- indexed_class.define_singleton_method(:"rebuild_#{index_name}") do
317
- index_hash = send(index_name) # Access the class-level hashkey DataType
318
-
319
- # Clear existing index using DataType method
320
- index_hash.clear
321
-
322
- # Rebuild from all existing objects
323
- # This would need to scan through all objects of this class
324
- # Implementation depends on how objects are stored/tracked
377
+ indexed_class.define_singleton_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
378
+ if respond_to?(:instances)
379
+ # Strategy 1: Use instances collection (fastest)
380
+ index_hashkey = send(index_name) # Get the index HashKey for serialization
381
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_instances(
382
+ self, # indexed_class (e.g., User)
383
+ field, # e.g., :email
384
+ :"add_to_class_#{index_name}", # e.g., :add_to_class_email_lookup
385
+ index_hashkey, # Pass index for serialization
386
+ batch_size: batch_size,
387
+ &progress_block
388
+ )
389
+ else
390
+ # Strategy 3: Fall back to SCAN
391
+ index_hashkey = send(index_name) # Get the index HashKey for serialization
392
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_scan(
393
+ self,
394
+ field,
395
+ :"add_to_class_#{index_name}",
396
+ index_hashkey, # Pass index for serialization
397
+ batch_size: batch_size,
398
+ &progress_block
399
+ )
400
+ end
325
401
  end
326
402
  end
327
403
 
@@ -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
@@ -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
@@ -29,7 +31,7 @@ module Familia
29
31
  :scope_class, # Class/Symbol - scope class for instance-scoped indexes (within:)
30
32
  :within, # Class/Symbol/nil - within: parameter (nil for class-level, Class for instance-scoped)
31
33
  :cardinality, # Symbol - :unique (1:1) or :multi (1:many)
32
- :query # Boolean - whether to generate query methods
34
+ :query, # Boolean - whether to generate query methods
33
35
  ) do
34
36
  #
35
37
  # Get the normalized config name for the scope class