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
@@ -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, 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 membersraw to get raw identifiers without deserialization
108
+ instances.membersraw.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 directly into temp key
121
+ tx.hset(temp_key, value.to_s, obj.identifier.to_s)
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, 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 membersraw to get raw identifiers
226
+ collection.membersraw.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 identifier
237
+ # For multi-index: SADD temp_key:field_value identifier
238
+ tx.hset(temp_key, value.to_s, obj.identifier.to_s)
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, 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, 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, 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, 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, obj.identifier.to_s)
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