familia 2.0.0.pre18 → 2.0.0.pre21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.rst +205 -88
  8. data/CLAUDE.md +62 -10
  9. data/Gemfile +3 -3
  10. data/Gemfile.lock +27 -62
  11. data/README.md +39 -0
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
  15. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
  16. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
  17. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
  18. data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
  19. data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
  20. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  21. data/docs/guides/encryption.md +486 -0
  22. data/docs/guides/feature-encrypted-fields.md +123 -7
  23. data/docs/guides/feature-expiration.md +177 -133
  24. data/docs/guides/feature-external-identifiers.md +415 -443
  25. data/docs/guides/feature-object-identifiers.md +400 -269
  26. data/docs/guides/feature-quantization.md +120 -6
  27. data/docs/guides/feature-relationships-indexing.md +318 -0
  28. data/docs/guides/feature-relationships-methods.md +146 -604
  29. data/docs/guides/feature-relationships-participation.md +263 -0
  30. data/docs/guides/feature-relationships.md +118 -136
  31. data/docs/guides/feature-system-devs.md +176 -693
  32. data/docs/guides/feature-system.md +119 -6
  33. data/docs/guides/feature-transient-fields.md +81 -0
  34. data/docs/guides/field-system.md +778 -0
  35. data/docs/guides/index.md +32 -15
  36. data/docs/guides/logging.md +187 -0
  37. data/docs/guides/optimized-loading.md +674 -0
  38. data/docs/guides/thread-safety-monitoring.md +61 -0
  39. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  40. data/docs/migrating/v2.0.0-pre19.md +197 -0
  41. data/docs/migrating/v2.0.0-pre22.md +241 -0
  42. data/docs/overview.md +7 -9
  43. data/docs/reference/api-technical.md +267 -320
  44. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  45. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  46. data/examples/autoloader/mega_customer.rb +2 -0
  47. data/examples/datatype_standalone.rb +282 -0
  48. data/examples/encrypted_fields.rb +2 -1
  49. data/examples/json_usage_patterns.rb +2 -0
  50. data/examples/relationships.rb +3 -0
  51. data/examples/safe_dump.rb +2 -1
  52. data/examples/sampling_demo.rb +53 -0
  53. data/examples/single_connection_transaction_confusions.rb +2 -1
  54. data/familia.gemspec +2 -1
  55. data/lib/familia/base.rb +2 -0
  56. data/lib/familia/connection/behavior.rb +254 -0
  57. data/lib/familia/connection/handlers.rb +97 -0
  58. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  59. data/lib/familia/connection/middleware.rb +34 -24
  60. data/lib/familia/connection/operation_core.rb +3 -1
  61. data/lib/familia/connection/operations.rb +2 -0
  62. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
  63. data/lib/familia/connection/transaction_core.rb +75 -9
  64. data/lib/familia/connection.rb +21 -5
  65. data/lib/familia/data_type/class_methods.rb +3 -1
  66. data/lib/familia/data_type/connection.rb +153 -7
  67. data/lib/familia/data_type/database_commands.rb +9 -4
  68. data/lib/familia/data_type/serialization.rb +10 -4
  69. data/lib/familia/data_type/settings.rb +2 -0
  70. data/lib/familia/data_type/types/counter.rb +2 -0
  71. data/lib/familia/data_type/types/hashkey.rb +8 -6
  72. data/lib/familia/data_type/types/listkey.rb +2 -0
  73. data/lib/familia/data_type/types/lock.rb +2 -0
  74. data/lib/familia/data_type/types/sorted_set.rb +2 -0
  75. data/lib/familia/data_type/types/stringkey.rb +2 -0
  76. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  77. data/lib/familia/data_type.rb +2 -0
  78. data/lib/familia/encryption/encrypted_data.rb +4 -2
  79. data/lib/familia/encryption/manager.rb +2 -0
  80. data/lib/familia/encryption/provider.rb +2 -0
  81. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  82. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  83. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  84. data/lib/familia/encryption/registry.rb +2 -0
  85. data/lib/familia/encryption/request_cache.rb +2 -0
  86. data/lib/familia/encryption.rb +9 -2
  87. data/lib/familia/errors.rb +53 -14
  88. data/lib/familia/features/autoloader.rb +2 -0
  89. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  90. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  91. data/lib/familia/features/encrypted_fields.rb +2 -2
  92. data/lib/familia/features/expiration/extensions.rb +11 -11
  93. data/lib/familia/features/expiration.rb +29 -21
  94. data/lib/familia/features/external_identifier.rb +33 -7
  95. data/lib/familia/features/object_identifier.rb +2 -0
  96. data/lib/familia/features/quantization.rb +3 -1
  97. data/lib/familia/features/relationships/README.md +3 -1
  98. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  99. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
  100. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  101. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
  102. data/lib/familia/features/relationships/indexing.rb +40 -42
  103. data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
  104. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  105. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  106. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  107. data/lib/familia/features/relationships/participation.rb +155 -69
  108. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  109. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  110. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  111. data/lib/familia/features/relationships.rb +5 -3
  112. data/lib/familia/features/safe_dump.rb +2 -0
  113. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  114. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  115. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  116. data/lib/familia/features/transient_fields.rb +2 -0
  117. data/lib/familia/features.rb +2 -0
  118. data/lib/familia/field_type.rb +5 -2
  119. data/lib/familia/horreum/connection.rb +28 -36
  120. data/lib/familia/horreum/database_commands.rb +131 -10
  121. data/lib/familia/horreum/definition.rb +18 -7
  122. data/lib/familia/horreum/management.rb +233 -57
  123. data/lib/familia/horreum/persistence.rb +314 -122
  124. data/lib/familia/horreum/related_fields.rb +2 -0
  125. data/lib/familia/horreum/serialization.rb +26 -4
  126. data/lib/familia/horreum/settings.rb +2 -0
  127. data/lib/familia/horreum/utils.rb +2 -8
  128. data/lib/familia/horreum.rb +46 -13
  129. data/lib/familia/identifier_extractor.rb +2 -0
  130. data/lib/familia/instrumentation.rb +156 -0
  131. data/lib/familia/json_serializer.rb +2 -0
  132. data/lib/familia/logging.rb +94 -37
  133. data/lib/familia/refinements/dear_json.rb +2 -0
  134. data/lib/familia/refinements/stylize_words.rb +2 -14
  135. data/lib/familia/refinements/time_literals.rb +2 -0
  136. data/lib/familia/refinements.rb +2 -0
  137. data/lib/familia/secure_identifier.rb +10 -2
  138. data/lib/familia/settings.rb +9 -7
  139. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  140. data/lib/familia/thread_safety/monitor.rb +328 -0
  141. data/lib/familia/utils.rb +13 -0
  142. data/lib/familia/verifiable_identifier.rb +3 -1
  143. data/lib/familia/version.rb +3 -1
  144. data/lib/familia.rb +31 -4
  145. data/lib/middleware/database_command_counter.rb +152 -0
  146. data/lib/middleware/database_logger.rb +325 -129
  147. data/lib/multi_result.rb +2 -0
  148. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  149. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  150. data/try/edge_cases/json_serialization_try.rb +2 -0
  151. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  152. data/try/edge_cases/race_conditions_try.rb +4 -0
  153. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  154. data/try/edge_cases/string_coercion_try.rb +6 -4
  155. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  156. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  157. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  158. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  159. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  160. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  161. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  162. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  164. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  165. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  166. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  167. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  168. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  169. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  170. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  171. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  172. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  173. data/try/features/encryption/config_persistence_try.rb +4 -0
  174. data/try/features/encryption/core_try.rb +4 -0
  175. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  176. data/try/features/encryption/module_loading_try.rb +4 -0
  177. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  178. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  179. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  180. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  181. data/try/features/expiration/expiration_try.rb +5 -1
  182. data/try/features/external_identifier/external_identifier_try.rb +171 -8
  183. data/try/features/feature_dependencies_try.rb +2 -0
  184. data/try/features/feature_improvements_try.rb +2 -0
  185. data/try/features/field_groups_try.rb +2 -0
  186. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  187. data/try/features/object_identifier/object_identifier_try.rb +2 -0
  188. data/try/features/quantization/quantization_try.rb +4 -0
  189. data/try/features/real_feature_integration_try.rb +2 -0
  190. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/indexing_rebuild_try.rb +600 -0
  192. data/try/features/relationships/indexing_try.rb +30 -4
  193. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  194. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  195. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  196. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  197. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  198. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  199. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  200. data/try/features/relationships/relationships_api_changes_try.rb +6 -4
  201. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  202. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  203. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  204. data/try/features/relationships/relationships_performance_try.rb +4 -0
  205. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  206. data/try/features/relationships/relationships_try.rb +6 -4
  207. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  208. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  209. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  210. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  211. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  212. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  213. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  214. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  215. data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
  216. data/try/integration/connection/handler_constraints_try.rb +4 -0
  217. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  218. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  219. data/try/integration/connection/operation_mode_guards_try.rb +5 -1
  220. data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
  221. data/try/integration/connection/pools_try.rb +4 -0
  222. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  223. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  224. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  225. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  226. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  227. data/try/integration/connection/transaction_modes_try.rb +4 -0
  228. data/try/integration/conventional_inheritance_try.rb +4 -0
  229. data/try/integration/create_method_try.rb +26 -22
  230. data/try/integration/cross_component_try.rb +4 -0
  231. data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
  232. data/try/integration/data_types/datatype_transactions_try.rb +251 -0
  233. data/try/integration/database_consistency_try.rb +4 -0
  234. data/try/integration/familia_extended_try.rb +4 -0
  235. data/try/integration/familia_members_methods_try.rb +4 -0
  236. data/try/integration/models/customer_safe_dump_try.rb +9 -1
  237. data/try/integration/models/customer_try.rb +4 -0
  238. data/try/integration/models/datatype_base_try.rb +4 -0
  239. data/try/integration/models/familia_object_try.rb +5 -1
  240. data/try/integration/persistence_operations_try.rb +166 -10
  241. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  242. data/try/integration/save_methods_consistency_try.rb +241 -0
  243. data/try/integration/scenarios_try.rb +4 -0
  244. data/try/integration/secure_identifier_try.rb +4 -0
  245. data/try/integration/transaction_safety_core_try.rb +176 -0
  246. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  247. data/try/integration/verifiable_identifier_try.rb +4 -0
  248. data/try/investigation/pipeline_routing/README.md +228 -0
  249. data/try/performance/benchmarks_try.rb +4 -0
  250. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  251. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  252. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  253. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  254. data/try/support/debugging/debug_aad_process.rb +3 -0
  255. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  256. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  257. data/try/support/debugging/debug_context_aad.rb +3 -0
  258. data/try/support/debugging/debug_context_simple.rb +3 -0
  259. data/try/support/debugging/debug_cross_context.rb +3 -0
  260. data/try/support/debugging/debug_database_load.rb +3 -0
  261. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  262. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  263. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  264. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  265. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  266. data/try/support/debugging/debug_load_path.rb +3 -0
  267. data/try/support/debugging/debug_method_definition.rb +3 -0
  268. data/try/support/debugging/debug_method_resolution.rb +3 -0
  269. data/try/support/debugging/debug_minimal.rb +3 -0
  270. data/try/support/debugging/debug_provider.rb +3 -0
  271. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  272. data/try/support/debugging/debug_string_class.rb +3 -0
  273. data/try/support/debugging/debug_test.rb +3 -0
  274. data/try/support/debugging/debug_test_design.rb +3 -0
  275. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  276. data/try/support/debugging/provider_diagnostics.rb +4 -0
  277. data/try/support/helpers/test_cleanup.rb +4 -0
  278. data/try/support/helpers/test_helpers.rb +5 -0
  279. data/try/support/memory/memory_basic_test.rb +4 -0
  280. data/try/support/memory/memory_detailed_test.rb +4 -0
  281. data/try/support/memory/memory_search_for_string.rb +4 -0
  282. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  283. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  284. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  285. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  286. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  287. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  288. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  289. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  290. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  291. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  292. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  293. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  294. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  295. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  296. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  297. data/try/thread_safety/README.md +496 -0
  298. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  299. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  300. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  301. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  302. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  303. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  304. data/try/thread_safety/field_registration_race_try.rb +222 -0
  305. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  306. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  307. data/try/thread_safety/module_config_race_try.rb +175 -0
  308. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  309. data/try/unit/core/autoloader_try.rb +4 -0
  310. data/try/unit/core/base_enhancements_try.rb +4 -0
  311. data/try/unit/core/connection_try.rb +4 -0
  312. data/try/unit/core/errors_try.rb +4 -0
  313. data/try/unit/core/extensions_try.rb +4 -0
  314. data/try/unit/core/familia_logger_try.rb +2 -0
  315. data/try/unit/core/familia_try.rb +4 -0
  316. data/try/unit/core/middleware_sampling_try.rb +335 -0
  317. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  318. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  319. data/try/unit/core/middleware_try.rb +4 -0
  320. data/try/unit/core/settings_try.rb +4 -0
  321. data/try/unit/core/time_utils_try.rb +4 -0
  322. data/try/unit/core/tools_try.rb +4 -0
  323. data/try/unit/core/utils_try.rb +37 -0
  324. data/try/unit/data_types/boolean_try.rb +5 -1
  325. data/try/unit/data_types/counter_try.rb +4 -0
  326. data/try/unit/data_types/datatype_base_try.rb +4 -0
  327. data/try/unit/data_types/hash_try.rb +4 -0
  328. data/try/unit/data_types/list_try.rb +4 -0
  329. data/try/unit/data_types/lock_try.rb +4 -0
  330. data/try/unit/data_types/sorted_set_try.rb +4 -0
  331. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  332. data/try/unit/data_types/string_try.rb +5 -1
  333. data/try/unit/data_types/unsortedset_try.rb +4 -0
  334. data/try/unit/familia_resolve_class_try.rb +116 -0
  335. data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
  336. data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
  337. data/try/unit/horreum/base_try.rb +5 -1
  338. data/try/unit/horreum/class_methods_try.rb +6 -2
  339. data/try/unit/horreum/commands_try.rb +4 -0
  340. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  341. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
  342. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  343. data/try/unit/horreum/field_categories_try.rb +4 -0
  344. data/try/unit/horreum/field_definition_try.rb +4 -0
  345. data/try/unit/horreum/initialization_try.rb +5 -1
  346. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  347. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  348. data/try/unit/horreum/relations_try.rb +8 -4
  349. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  350. data/try/unit/horreum/serialization_try.rb +6 -2
  351. data/try/unit/horreum/settings_try.rb +4 -0
  352. data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
  353. data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
  354. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  355. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  356. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  357. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  358. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  359. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  360. data/try/unit/thread_safety_monitor_try.rb +149 -0
  361. metadata +81 -14
  362. data/.github/workflows/code-quality.yml +0 -138
  363. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  364. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  365. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  366. data/docs/archive/README.md +0 -64
  367. data/docs/archive/api-reference.md +0 -333
  368. data/docs/guides/core-field-system.md +0 -806
  369. data/docs/guides/implementation.md +0 -276
  370. data/docs/guides/security-model.md +0 -183
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/external_identifier.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Features
@@ -12,8 +14,8 @@ module Familia
12
14
  base.extend ModelClassMethods
13
15
  base.include ModelInstanceMethods
14
16
 
15
- # Ensure default prefix is set in feature options
16
- base.add_feature_options(:external_identifier, prefix: 'ext')
17
+ # Ensure default format is set in feature options
18
+ base.add_feature_options(:external_identifier, format: 'ext_%{id}')
17
19
 
18
20
  # Add class-level mapping for extid -> id lookups
19
21
  base.class_hashkey :extid_lookup
@@ -35,10 +37,10 @@ module Familia
35
37
  # - Deterministic generation from objid ensures consistency
36
38
  # - Shorter than objid (128-bit vs 256-bit) for external use
37
39
  # - Base-36 encoding for URL-safe identifiers
38
- # - 'ext_' prefix for clear identification as external IDs
40
+ # - Customizable format template (default: 'ext_' prefix)
39
41
  # - Lazy generation preserves values from initialization
40
42
  #
41
- # @example Using external identifier fields
43
+ # @example Using external identifier fields with default format
42
44
  # class User < Familia::Horreum
43
45
  # feature :object_identifier
44
46
  # feature :external_identifier
@@ -51,6 +53,30 @@ module Familia
51
53
  # user2 = User.new(objid: user.objid, email: 'user@example.com')
52
54
  # user2.extid # => "ext_abc123def456ghi789" (identical to user.extid)
53
55
  #
56
+ # @example Using custom format template with hyphen separator
57
+ # class APIKey < Familia::Horreum
58
+ # feature :object_identifier
59
+ # feature :external_identifier, format: 'api-%{id}'
60
+ # end
61
+ # key = APIKey.new
62
+ # key.extid # => "api-abc123def456ghi789"
63
+ #
64
+ # @example Using custom format template with custom prefix
65
+ # class Customer < Familia::Horreum
66
+ # feature :object_identifier
67
+ # feature :external_identifier, format: 'cust_%{id}'
68
+ # end
69
+ # customer = Customer.new
70
+ # customer.extid # => "cust_abc123def456ghi789"
71
+ #
72
+ # @example Using format template without prefix
73
+ # class Resource < Familia::Horreum
74
+ # feature :object_identifier
75
+ # feature :external_identifier, format: 'v2/%{id}'
76
+ # end
77
+ # resource = Resource.new
78
+ # resource.extid # => "v2/abc123def456ghi789"
79
+ #
54
80
  class ExternalIdentifierFieldType < Familia::FieldType
55
81
  # Override getter to provide lazy generation from objid
56
82
  #
@@ -252,11 +278,11 @@ module Familia
252
278
  # 128 bits is approximately 25 characters in base36.
253
279
  external_part = random_bytes.unpack1('H*').to_i(16).to_s(36).rjust(25, '0')
254
280
 
255
- # Get prefix from feature options, default to "ext"
281
+ # Get format from feature options and interpolate the ID
256
282
  options = self.class.feature_options(:external_identifier)
257
- prefix = options[:prefix] || 'ext'
283
+ format = options[:format] || 'ext_%{id}'
258
284
 
259
- "#{prefix}_#{external_part}"
285
+ format % { id: external_part }
260
286
  end
261
287
 
262
288
  # Full-length alias for extid for clarity when needed
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/object_identifier.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Features
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/quantization.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Features
@@ -389,7 +391,7 @@ module Familia
389
391
  # user.quantized_identifier(1.hour) # => "123:1672531200"
390
392
  # user.quantized_identifier(1.hour, pattern: '%Y%m%d%H') # => "123:2023010114"
391
393
  #
392
- def quantized_identifier(quantum, pattern: nil, separator: ':')
394
+ def quantized_identifier(quantum, pattern: nil, separator: Familia.delim)
393
395
  timestamp = qstamp(quantum, pattern: pattern)
394
396
  base_id = respond_to?(:identifier) ? identifier : object_id
395
397
  "#{base_id}#{separator}#{timestamp}"
@@ -18,6 +18,7 @@ participates_in Organization, :members, score: :joined_at, bidirectional: true
18
18
 
19
19
  **unique_index** - Fast unique lookups ("find object by unique field value")
20
20
  ```ruby
21
+ unique_index :email, :email_index # Class-level: User.find_by_email()
21
22
  unique_index :email, :email_index, within: Organization # Scoped: org.find_by_email()
22
23
  ```
23
24
 
@@ -89,7 +90,8 @@ class Customer < Familia::Horreum
89
90
  feature :relationships
90
91
 
91
92
  participates_in Organization, :members # Customer belongs to org
92
- unique_index :email, :email_index, within: Organization # Find by unique email
93
+ unique_index :email, :email_index # Class-level: Customer.find_by_email()
94
+ multi_index :status, :status_index, within: Organization # Scoped: org.find_all_by_status()
93
95
  end
94
96
  ```
95
97
 
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/relationships/collection_operations.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Features
@@ -1,3 +1,5 @@
1
+ # lib/familia/features/relationships/indexing/multi_index_generators.rb
2
+ #
1
3
  # frozen_string_literal: true
2
4
 
3
5
  module Familia
@@ -32,29 +34,30 @@ module Familia
32
34
  # @param indexed_class [Class] The class being indexed (e.g., Employee)
33
35
  # @param field [Symbol] The field to index
34
36
  # @param index_name [Symbol] Name of the index
35
- # @param within [Class, Symbol] Parent class for instance-scoped index (required)
37
+ # @param within [Class, Symbol] Scope class for instance-scoped index (required)
36
38
  # @param query [Boolean] Whether to generate query methods
37
39
  def setup(indexed_class:, field:, index_name:, within:, query:)
38
- # Multi-index always requires a parent context
39
- target_class = within
40
- resolved_class = Familia.resolve_class(target_class)
40
+ # Multi-index always requires a scope context
41
+ scope_class = within
42
+ resolved_class = Familia.resolve_class(scope_class)
41
43
 
42
44
  # Store metadata for this indexing relationship
43
45
  indexed_class.indexing_relationships << IndexingRelationship.new(
44
46
  field: field,
45
- target_class: target_class,
47
+ scope_class: scope_class,
48
+ within: within,
46
49
  index_name: index_name,
47
50
  query: query,
48
51
  cardinality: :multi,
49
52
  )
50
53
 
51
54
  # Always generate the factory method - required by mutation methods
52
- if target_class.is_a?(Class)
55
+ if scope_class.is_a?(Class)
53
56
  generate_factory_method(resolved_class, index_name)
54
57
  end
55
58
 
56
- # Generate query methods on the parent class (optional)
57
- if query && target_class.is_a?(Class)
59
+ # Generate query methods on the scope class (optional)
60
+ if query && scope_class.is_a?(Class)
58
61
  generate_query_methods_destination(indexed_class, field, resolved_class, index_name)
59
62
  end
60
63
 
@@ -62,42 +65,45 @@ module Familia
62
65
  generate_mutation_methods_self(indexed_class, field, resolved_class, index_name)
63
66
  end
64
67
 
65
- # Generates the factory method ON THE PARENT CLASS (Company when within: Company):
68
+ # Generates the factory method ON THE SCOPE CLASS (Company when within: Company):
66
69
  # - company.index_name_for(field_value) - DataType factory (always needed)
67
70
  #
68
71
  # This method is required by mutation methods even when query: false
69
72
  #
70
- # @param target_class [Class] The parent class (e.g., Company)
73
+ # @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
71
74
  # @param index_name [Symbol] Name of the index (e.g., :dept_index)
72
- def generate_factory_method(target_class, index_name)
73
- actual_target_class = Familia.resolve_class(target_class)
75
+ def generate_factory_method(scope_class, index_name)
76
+ actual_scope_class = Familia.resolve_class(scope_class)
74
77
 
75
- actual_target_class.class_eval do
78
+ actual_scope_class.class_eval do
76
79
  # Helper method to get index set for a specific field value
77
80
  # This acts as a factory for field-value-specific DataTypes
78
81
  define_method(:"#{index_name}_for") do |field_value|
79
82
  # Return properly managed DataType instance with parameterized key
80
- index_key = "#{index_name}:#{field_value}"
83
+ index_key = Familia.join(index_name, field_value)
81
84
  Familia::UnsortedSet.new(index_key, parent: self)
82
85
  end
83
86
  end
84
87
  end
85
88
 
86
- # Generates query methods ON THE PARENT CLASS (Company when within: Company):
89
+ # Generates query methods ON THE SCOPE CLASS (Company when within: Company):
87
90
  # - company.sample_from_department(dept, count=1) - random sampling
88
91
  # - company.find_all_by_department(dept) - all objects
89
92
  # - company.rebuild_dept_index - rebuild index
90
93
  #
91
94
  # @param indexed_class [Class] The class being indexed (e.g., Employee)
92
95
  # @param field [Symbol] The field to index (e.g., :department)
93
- # @param target_class [Class] The parent class (e.g., Company)
96
+ # @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
94
97
  # @param index_name [Symbol] Name of the index (e.g., :dept_index)
95
- def generate_query_methods_destination(indexed_class, field, target_class, index_name)
96
- # Resolve target class using Familia pattern
97
- actual_target_class = Familia.resolve_class(target_class)
98
+ def generate_query_methods_destination(indexed_class, field, scope_class, index_name)
99
+ # Resolve scope class using Familia pattern
100
+ actual_scope_class = Familia.resolve_class(scope_class)
101
+
102
+ # Get scope_class_config for method naming (needed for rebuild methods)
103
+ scope_class_config = actual_scope_class.config_name
98
104
 
99
105
  # Generate instance sampling method (e.g., company.sample_from_department)
100
- actual_target_class.class_eval do
106
+ actual_scope_class.class_eval do
101
107
 
102
108
  define_method(:"sample_from_#{field}") do |field_value, count = 1|
103
109
  index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
@@ -117,11 +123,135 @@ module Familia
117
123
  index_set.members.map { |id| indexed_class.find_by_identifier(id) }
118
124
  end
119
125
 
120
- # Generate method to rebuild the index for this parent instance
121
- define_method(:"rebuild_#{index_name}") do
122
- # This would need to be implemented based on how you track which
123
- # objects belong to this parent instance
124
- # For now, just a placeholder
126
+ # Generate method to rebuild the multi-value index for this parent instance
127
+ #
128
+ # Multi-indexes create separate sets for each field value, requiring a three-phase approach:
129
+ # 1. Loading: Load all objects once and cache them (discovers field values simultaneously)
130
+ # 2. Clearing: Remove all existing index sets using SCAN
131
+ # 3. Rebuilding: Rebuild index from cached objects (no reload needed)
132
+ #
133
+ # @param batch_size [Integer] Number of identifiers to process per batch
134
+ # @yield [progress] Optional block called with progress updates
135
+ # @yieldparam progress [Hash] Progress information with keys:
136
+ # - :phase [Symbol] Current phase (:loading, :clearing, :rebuilding)
137
+ # - :current [Integer] Current item count
138
+ # - :total [Integer] Total items (when known)
139
+ # - :field_value [String] Current field value being processed
140
+ #
141
+ # @example Basic rebuild
142
+ # company.rebuild_dept_index
143
+ #
144
+ # @example With progress monitoring
145
+ # company.rebuild_dept_index do |progress|
146
+ # puts "#{progress[:phase]}: #{progress[:current]}/#{progress[:total]}"
147
+ # end
148
+ #
149
+ # @example Memory-conscious rebuild for large collections
150
+ # # Process in smaller batches to reduce memory footprint
151
+ # company.rebuild_dept_index(batch_size: 50)
152
+ #
153
+ # @note Memory Considerations:
154
+ # This method caches all objects in memory during rebuild to avoid duplicate
155
+ # database loads. For very large collections (>100k objects), monitor memory usage
156
+ # and consider processing in chunks or using a streaming approach if memory
157
+ # constraints are encountered. The batch_size parameter controls Redis I/O
158
+ # batching but does not affect memory usage since all objects are cached.
159
+ #
160
+ define_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
161
+ # PHASE 1: Find the collection containing the indexed objects
162
+ # Look for a participation relationship where indexed_class participates in this scope_class
163
+ collection_name = nil
164
+
165
+ # Check if indexed_class has participation to this scope_class
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
+ collection_name = participation&.collection_name if participation
171
+ end
172
+
173
+ # Get the collection DataType if we found a participation relationship
174
+ collection = collection_name ? send(collection_name) : nil
175
+
176
+ if collection
177
+ # PHASE 2: Load objects once and cache them for both discovery and rebuilding
178
+ # This avoids duplicate load_multi calls (previous approach loaded twice)
179
+ progress_block&.call(phase: :loading, current: 0, total: collection.size)
180
+
181
+ field_values = Set.new
182
+ cached_objects = []
183
+ processed = 0
184
+
185
+ collection.members.each_slice(batch_size) do |identifiers|
186
+ # Load objects in batches - SINGLE LOAD for both phases
187
+ objects = indexed_class.load_multi(identifiers).compact
188
+ cached_objects.concat(objects)
189
+
190
+ objects.each do |obj|
191
+ value = obj.send(field)
192
+ # Only track non-nil, non-empty field values
193
+ field_values << value.to_s if value && !value.to_s.strip.empty?
194
+ end
195
+
196
+ processed += identifiers.size
197
+ progress_block&.call(phase: :loading, current: processed, total: collection.size)
198
+ end
199
+
200
+ # PHASE 3: Clear all existing field-value-specific index sets
201
+ # Use SCAN to find all existing index keys (including orphaned ones from deleted field values)
202
+ progress_block&.call(phase: :clearing, current: 0, total: field_values.size)
203
+
204
+ # Get the base pattern for this index by creating a sample index set
205
+ # The "*" creates a wildcard pattern like "company:123:dept_index:*" for SCAN
206
+ sample_index = send(:"#{index_name}_for", "*")
207
+ index_pattern = sample_index.dbkey
208
+
209
+ # Find all existing index keys using SCAN
210
+ cleared_count = 0
211
+ dbclient.scan_each(match: index_pattern) do |key|
212
+ dbclient.del(key)
213
+ cleared_count += 1
214
+ progress_block&.call(phase: :clearing, current: cleared_count, total: field_values.size, key: key)
215
+ end
216
+
217
+ # PHASE 4: Rebuild index from cached objects (no reload needed)
218
+ progress_block&.call(phase: :rebuilding, current: 0, total: cached_objects.size)
219
+
220
+ processed = 0
221
+ cached_objects.each_slice(batch_size) do |objects|
222
+ transaction do |_tx|
223
+ objects.each do |obj|
224
+ # Use the generated add_to method to maintain consistency
225
+ # This ensures the same logic is used as during normal operation
226
+ obj.send(:"add_to_#{scope_class_config}_#{index_name}", self)
227
+ end
228
+ end
229
+
230
+ processed += objects.size
231
+ progress_block&.call(phase: :rebuilding, current: processed, total: cached_objects.size)
232
+ end
233
+
234
+ Familia.info "[Rebuild] Multi-index #{index_name} rebuilt: #{field_values.size} field values, #{processed} objects"
235
+
236
+ processed # Return count of processed objects
237
+
238
+ else
239
+ # No participation relationship found - warn and suggest alternative
240
+ Familia.warn <<~WARNING
241
+ [Rebuild] Cannot rebuild multi-index #{index_name}: no participation relationship found
242
+
243
+ Multi-index rebuild requires a participation relationship to find objects.
244
+ Add a participation relationship to #{indexed_class.name}:
245
+
246
+ class #{indexed_class.name} < Familia::Horreum
247
+ participates_in #{self.class.name}, :collection_name, score: :field
248
+ end
249
+
250
+ Then access the collection via: #{self.class.config_name}.collection_name
251
+ WARNING
252
+
253
+ nil
254
+ end
125
255
  end
126
256
  end
127
257
  end
@@ -133,62 +263,62 @@ module Familia
133
263
  #
134
264
  # @param indexed_class [Class] The class being indexed (e.g., Employee)
135
265
  # @param field [Symbol] The field to index (e.g., :department)
136
- # @param target_class [Class] The parent class (e.g., Company)
266
+ # @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
137
267
  # @param index_name [Symbol] Name of the index (e.g., :dept_index)
138
- def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
139
- target_class_config = target_class.config_name
268
+ def generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
269
+ scope_class_config = scope_class.config_name
140
270
  indexed_class.class_eval do
141
- method_name = :"add_to_#{target_class_config}_#{index_name}"
142
- Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
271
+ method_name = :"add_to_#{scope_class_config}_#{index_name}"
272
+ Familia.debug("[MultiIndexGenerators] #{name} method #{method_name}")
143
273
 
144
- define_method(method_name) do |target_instance|
145
- return unless target_instance
274
+ define_method(method_name) do |scope_instance|
275
+ return unless scope_instance
146
276
 
147
277
  field_value = send(field)
148
278
  return unless field_value
149
279
 
150
- # Use helper method on target instance instead of manual instantiation
151
- index_set = target_instance.send("#{index_name}_for", field_value)
280
+ # Use helper method on scope instance instead of manual instantiation
281
+ index_set = scope_instance.send("#{index_name}_for", field_value)
152
282
 
153
283
  # Use UnsortedSet DataType method (no scoring)
154
284
  index_set.add(identifier)
155
285
  end
156
286
 
157
- method_name = :"remove_from_#{target_class_config}_#{index_name}"
158
- Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
287
+ method_name = :"remove_from_#{scope_class_config}_#{index_name}"
288
+ Familia.debug("[MultiIndexGenerators] #{name} method #{method_name}")
159
289
 
160
- define_method(method_name) do |target_instance|
161
- return unless target_instance
290
+ define_method(method_name) do |scope_instance|
291
+ return unless scope_instance
162
292
 
163
293
  field_value = send(field)
164
294
  return unless field_value
165
295
 
166
- # Use helper method on target instance instead of manual instantiation
167
- index_set = target_instance.send("#{index_name}_for", field_value)
296
+ # Use helper method on scope instance instead of manual instantiation
297
+ index_set = scope_instance.send("#{index_name}_for", field_value)
168
298
 
169
299
  # Remove using UnsortedSet DataType method
170
300
  index_set.remove(identifier)
171
301
  end
172
302
 
173
- method_name = :"update_in_#{target_class_config}_#{index_name}"
174
- Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
303
+ method_name = :"update_in_#{scope_class_config}_#{index_name}"
304
+ Familia.debug("[MultiIndexGenerators] #{name} method #{method_name}")
175
305
 
176
- define_method(method_name) do |target_instance, old_field_value = nil|
177
- return unless target_instance
306
+ define_method(method_name) do |scope_instance, old_field_value = nil|
307
+ return unless scope_instance
178
308
 
179
309
  new_field_value = send(field)
180
310
 
181
311
  # Use Familia's transaction method for atomicity with DataType abstraction
182
- target_instance.transaction do |_tx|
312
+ scope_instance.transaction do |_tx|
183
313
  # Remove from old index if provided - use helper method
184
314
  if old_field_value
185
- old_index_set = target_instance.send("#{index_name}_for", old_field_value)
315
+ old_index_set = scope_instance.send("#{index_name}_for", old_field_value)
186
316
  old_index_set.remove(identifier)
187
317
  end
188
318
 
189
319
  # Add to new index if present - use helper method
190
320
  if new_field_value
191
- new_index_set = target_instance.send("#{index_name}_for", new_field_value)
321
+ new_index_set = scope_instance.send("#{index_name}_for", new_field_value)
192
322
  new_index_set.add(identifier)
193
323
  end
194
324
  end