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/horreum/definition.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'settings'
4
6
 
@@ -98,7 +100,7 @@ module Familia
98
100
  @current_field_group = nil
99
101
  end
100
102
  else
101
- Familia.ld "[field_group] Created field group :#{name} but no block given" if Familia.debug?
103
+ Familia.debug "[field_group] Created field group :#{name} but no block given"
102
104
  end
103
105
 
104
106
  field_groups[name.to_sym]
@@ -124,7 +126,10 @@ module Familia
124
126
  # ]
125
127
  #
126
128
  def field_groups
127
- @field_groups ||= {}
129
+ @field_groups_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('field_groups')
130
+ @field_groups || @field_groups_mutex.synchronize do
131
+ @field_groups ||= {}
132
+ end
128
133
  end
129
134
 
130
135
  # Sets or retrieves the unique identifier field for the class.
@@ -215,8 +220,10 @@ module Familia
215
220
  # Returns the list of field names defined for the class in the order
216
221
  # that they were defined. i.e. `field :a; field :b; fields => [:a, :b]`.
217
222
  def fields
218
- @fields ||= []
219
- @fields
223
+ @fields_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('fields')
224
+ @fields || @fields_mutex.synchronize do
225
+ @fields ||= []
226
+ end
220
227
  end
221
228
 
222
229
  def class_related_fields
@@ -235,7 +242,10 @@ module Familia
235
242
 
236
243
  # Storage for field type instances
237
244
  def field_types
238
- @field_types ||= {}
245
+ @field_types_mutex ||= Familia::ThreadSafety::InstrumentedMutex.new('field_types')
246
+ @field_types || @field_types_mutex.synchronize do
247
+ @field_types ||= {}
248
+ end
239
249
  end
240
250
 
241
251
  # Returns a hash mapping field names to method names for backward compatibility
@@ -467,7 +477,8 @@ module Familia
467
477
 
468
478
  # If no value is provided to this fast attribute method, make a call
469
479
  # to the db to return the current stored value of the hash field.
470
- return hget field_name if val.nil?
480
+ # Handle Redis::Future objects during transactions
481
+ return hget field_name if val.nil? || val.is_a?(Redis::Future)
471
482
 
472
483
  begin
473
484
  # Trace the operation if debugging is enabled.
@@ -475,7 +486,7 @@ module Familia
475
486
 
476
487
  # Convert the provided value to a format suitable for Database storage.
477
488
  prepared = serialize_value(val)
478
- Familia.ld "[define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
489
+ Familia.debug "[define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
479
490
 
480
491
  # Use the existing accessor method to set the attribute value.
481
492
  send :"#{method_name}=", val
@@ -1,4 +1,6 @@
1
1
  # lib/familia/horreum/management.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class Horreum
@@ -23,7 +25,7 @@ module Familia
23
25
  # to the constructor.
24
26
  # @param kwargs [Hash] Keyword arguments to be passed to the constructor.
25
27
  # @return [Object] The newly created and persisted instance.
26
- # @raise [Familia::Problem] If an instance with the same identifier already
28
+ # @raise [Familia::RecordExistsError] If an instance with the same identifier already
27
29
  # exists.
28
30
  #
29
31
  # This method serves as a factory method for creating and persisting new
@@ -35,7 +37,7 @@ module Familia
35
37
  # - Keyword arguments (**kwargs) are passed as a hash to the constructor.
36
38
  #
37
39
  # After instantiation, the method checks if an object with the same
38
- # identifier already exists. If it does, a Familia::Problem exception is
40
+ # identifier already exists. If it does, a Familia::RecordExistsError exception is
39
41
  # raised to prevent overwriting existing data.
40
42
  #
41
43
  # Finally, the method saves the new instance returns it.
@@ -52,24 +54,27 @@ module Familia
52
54
  # @see #new
53
55
  # @see #exists?
54
56
  # @see #save
55
- #
56
- def create(*, **)
57
- fobj = new(*, **)
58
- fobj.save_if_not_exists
59
- fobj
57
+ def create!(...)
58
+ hobj = new(...)
59
+ hobj.save_if_not_exists!
60
+
61
+ # If a block is given, yield the created object
62
+ # This allows for additional operations on successful creation
63
+ yield hobj if block_given?
64
+
65
+ hobj
60
66
  end
61
67
 
62
- def multiget(*ids)
63
- ids = rawmultiget(*ids)
64
- ids.filter_map { |json| from_json(json) }
68
+ def multiget(...)
69
+ rawmultiget(...).filter_map { |json| Familia::JsonSerializer.parse(json) }
65
70
  end
66
71
 
67
- def rawmultiget(*ids)
68
- ids.collect! { |objid| dbkey(objid) }
69
- return [] if ids.compact.empty?
72
+ def rawmultiget(*hids)
73
+ hids.collect! { |hobjid| dbkey(hobjid) }
74
+ return [] if hids.compact.empty?
70
75
 
71
- Familia.trace :MULTIGET, nil, "#{ids.size}: #{ids}" if Familia.debug?
72
- dbclient.mget(*ids)
76
+ Familia.trace :MULTIGET, nil, "#{hids.size}: #{hids}" if Familia.debug?
77
+ dbclient.mget(*hids)
73
78
  end
74
79
 
75
80
  # Converts the class name into a string that can be used to look up
@@ -101,56 +106,71 @@ module Familia
101
106
  # key.
102
107
  #
103
108
  # @param objkey [String] The full dbkey for the object.
109
+ # @param check_exists [Boolean] Whether to check key existence before HGETALL
110
+ # (default: true). When false, skips EXISTS check for better performance
111
+ # but still returns nil for non-existent keys (detected via empty hash).
104
112
  # @return [Object, nil] An instance of the class if the key exists, nil
105
113
  # otherwise.
106
114
  # @raise [ArgumentError] If the provided key is empty.
107
115
  #
108
- # This method performs a two-step process to safely retrieve and
109
- # instantiate objects:
116
+ # This method can operate in two modes:
110
117
  #
111
- # 1. It first checks if the key exists in the database. This is crucial because:
112
- # - It provides a definitive answer about the object's existence.
113
- # - It prevents ambiguity that could arise from `hgetall` returning an
114
- # empty hash for non-existent keys, which could lead to the creation
115
- # of "empty" objects.
118
+ # **Safe mode (check_exists: true, default):**
119
+ # 1. First checks if the key exists with EXISTS command
120
+ # 2. Returns nil immediately if key doesn't exist
121
+ # 3. If exists, retrieves data with HGETALL and instantiates object
122
+ # - Best for: Single object lookups, defensive code
123
+ # - Commands: 2 per object (EXISTS + HGETALL)
116
124
  #
117
- # 2. If the key exists, it retrieves the object's data and instantiates
118
- # it.
125
+ # **Optimized mode (check_exists: false):**
126
+ # 1. Directly calls HGETALL without EXISTS check
127
+ # 2. Returns nil if HGETALL returns empty hash (key doesn't exist)
128
+ # 3. Otherwise instantiates object with returned data
129
+ # - Best for: Bulk operations, performance-critical paths, when keys likely exist
130
+ # - Commands: 1 per object (HGETALL only)
131
+ # - Reduction: 50% fewer Redis commands
119
132
  #
120
- # This approach ensures that we only attempt to instantiate objects that
121
- # actually exist in Valkey/Redis, improving reliability and simplifying
122
- # debugging.
133
+ # @example Safe mode (default)
134
+ # User.find_by_key("user:123") # 2 commands: EXISTS + HGETALL
123
135
  #
124
- # @example
125
- # User.find_by_key("user:123") # Returns a User instance if it exists,
126
- # nil otherwise
136
+ # @example Optimized mode (skip existence check)
137
+ # User.find_by_key("user:123", check_exists: false) # 1 command: HGETALL
138
+ #
139
+ # @note When check_exists: false, HGETALL on non-existent keys returns {}
140
+ # which we detect and return nil (not an empty object instance).
127
141
  #
128
- def find_by_dbkey(objkey)
142
+ def find_by_dbkey(objkey, check_exists: true)
129
143
  raise ArgumentError, 'Empty key' if objkey.to_s.empty?
130
144
 
131
- # We use a lower-level method here b/c we're working with the
132
- # full key and not just the identifier.
133
- does_exist = dbclient.exists(objkey).positive?
134
-
135
- Familia.ld "[find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
136
- Familia.trace :FIND_BY_DBKEY_KEY, nil, objkey
137
-
138
- # This is the reason for calling exists first. We want to definitively
139
- # and without any ambiguity know if the object exists in the database. If it
140
- # doesn't, we return nil. If it does, we proceed to load the object.
141
- # Otherwise, hgetall will return an empty hash, which will be passed to
142
- # the constructor, which will then be annoying to debug.
143
- return unless does_exist
145
+ if check_exists
146
+ # Safe mode: Check existence first (original behavior)
147
+ # We use a lower-level method here b/c we're working with the
148
+ # full key and not just the identifier.
149
+ does_exist = dbclient.exists(objkey).positive?
150
+
151
+ Familia.debug "[find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
152
+ Familia.trace :FIND_BY_DBKEY_KEY, nil, objkey
153
+
154
+ # This is the reason for calling exists first. We want to definitively
155
+ # and without any ambiguity know if the object exists in the database. If it
156
+ # doesn't, we return nil. If it does, we proceed to load the object.
157
+ # Otherwise, hgetall will return an empty hash, which will be passed to
158
+ # the constructor, which will then be annoying to debug.
159
+ return unless does_exist
160
+ else
161
+ # Optimized mode: Skip existence check
162
+ Familia.debug "[find_by_key] #{self} from key #{objkey} (check_exists: false)"
163
+ Familia.trace :FIND_BY_DBKEY_KEY, nil, objkey
164
+ end
144
165
 
145
166
  obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
146
167
  Familia.trace :FIND_BY_DBKEY_INSPECT, nil, "#{objkey}: #{obj.inspect}"
147
168
 
148
- # Create instance and deserialize fields using existing helper method
149
- # This avoids duplicating deserialization logic and keeps field-by-field processing
150
- instance = allocate
151
- instance.send(:initialize_relatives)
152
- instance.send(:initialize_with_keyword_args_deserialize_value, **obj)
153
- instance
169
+ # If we skipped existence check and got empty hash, key doesn't exist
170
+ return nil if !check_exists && obj.empty?
171
+
172
+ # Create instance and deserialize fields using shared helper method
173
+ instantiate_from_hash(obj)
154
174
  end
155
175
  alias find_by_key find_by_dbkey
156
176
 
@@ -158,8 +178,10 @@ module Familia
158
178
  #
159
179
  # @param identifier [String, Integer] The unique identifier for the
160
180
  # object.
161
- # @param suffix [Symbol] The suffix to use in the dbkey (default:
162
- # :object).
181
+ # @param suffix [Symbol, nil] The suffix to use in the dbkey (default:
182
+ # class suffix). Keyword parameter for consistency with check_exists.
183
+ # @param check_exists [Boolean] Whether to check key existence before HGETALL
184
+ # (default: true). See find_by_dbkey for details.
163
185
  # @return [Object, nil] An instance of the class if found, nil otherwise.
164
186
  #
165
187
  # This method constructs the full dbkey using the provided identifier
@@ -170,23 +192,153 @@ module Familia
170
192
  # making it easier to retrieve objects when you only have their
171
193
  # identifier.
172
194
  #
173
- # @example
174
- # User.find_by_id(123) # Equivalent to User.find_by_key("user:123:object")
195
+ # @example Safe mode (default)
196
+ # User.find_by_id(123) # 2 commands: EXISTS + HGETALL
197
+ #
198
+ # @example Optimized mode
199
+ # User.find_by_id(123, check_exists: false) # 1 command: HGETALL
175
200
  #
176
- def find_by_identifier(identifier, suffix = nil)
201
+ # @example Custom suffix
202
+ # Session.find_by_id('abc', suffix: :session)
203
+ #
204
+ def find_by_identifier(identifier, suffix: nil, check_exists: true)
177
205
  suffix ||= self.suffix
178
206
  return nil if identifier.to_s.empty?
179
207
 
180
208
  objkey = dbkey(identifier, suffix)
181
209
 
182
- Familia.ld "[find_by_id] #{self} from key #{objkey})"
210
+ Familia.debug "[find_by_id] #{self} from key #{objkey})"
183
211
  Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
184
- find_by_dbkey objkey
212
+ find_by_dbkey objkey, check_exists: check_exists
185
213
  end
186
214
  alias find_by_id find_by_identifier
187
215
  alias find find_by_id
188
216
  alias load find_by_id
189
217
 
218
+ # Loads multiple objects by their identifiers using pipelined HGETALL commands.
219
+ #
220
+ # This method provides significant performance improvements for bulk loading by:
221
+ # 1. Batching all HGETALL commands into a single Redis pipeline
222
+ # 2. Eliminating network round-trip overhead
223
+ # 3. Skipping individual EXISTS checks (like check_exists: false)
224
+ #
225
+ # @param identifiers [Array<String, Integer>] Array of identifiers to load
226
+ # @param suffix [Symbol, nil] The suffix to use in dbkeys (default: class suffix)
227
+ # @return [Array<Object>] Array of instantiated objects (nils for non-existent)
228
+ #
229
+ # Performance characteristics:
230
+ # - Standard approach: N objects × 2 commands (EXISTS + HGETALL) = 2N round trips
231
+ # - check_exists: false: N objects × 1 command (HGETALL) = N round trips
232
+ # - load_multi: 1 pipeline with N commands = 1 round trip
233
+ # - Improvement: Up to 2N× faster for bulk operations
234
+ #
235
+ # @example Load multiple users efficiently
236
+ # users = User.load_multi([123, 456, 789])
237
+ # # 1 pipeline with 3 HGETALL commands instead of 6 individual commands
238
+ #
239
+ # @example Filter out nils
240
+ # existing_users = User.load_multi(ids).compact
241
+ #
242
+ # @note Returns nil for non-existent keys (maintains same contract as find_by_id)
243
+ # @note Objects are returned in the same order as input identifiers
244
+ # @note Empty/nil identifiers are skipped and return nil in result array
245
+ #
246
+ def load_multi(identifiers, suffix = nil)
247
+ suffix ||= self.suffix
248
+ return [] if identifiers.empty?
249
+
250
+ # Build list of valid keys and track their original positions
251
+ valid_keys = []
252
+ valid_positions = []
253
+
254
+ identifiers.each_with_index do |identifier, idx|
255
+ next if identifier.to_s.empty?
256
+
257
+ valid_keys << dbkey(identifier, suffix)
258
+ valid_positions << idx
259
+ end
260
+
261
+ Familia.trace :LOAD_MULTI, nil, "Loading #{identifiers.size} objects" if Familia.debug?
262
+
263
+ # Pipeline all HGETALL commands
264
+ multi_result = pipelined do |pipeline|
265
+ valid_keys.each do |objkey|
266
+ pipeline.hgetall(objkey)
267
+ end
268
+ end
269
+
270
+ # Extract results array from MultiResult
271
+ results = multi_result.results
272
+
273
+ # Map results back to original positions
274
+ objects = Array.new(identifiers.size)
275
+ valid_positions.each_with_index do |pos, result_idx|
276
+ obj_hash = results[result_idx]
277
+
278
+ # Skip empty hashes (non-existent keys)
279
+ next if obj_hash.nil? || obj_hash.empty?
280
+
281
+ # Instantiate object using shared helper method
282
+ objects[pos] = instantiate_from_hash(obj_hash)
283
+ end
284
+
285
+ objects
286
+ end
287
+ alias load_batch load_multi
288
+
289
+ # Loads multiple objects by their full dbkeys using pipelined HGETALL commands.
290
+ #
291
+ # This is a lower-level variant of load_multi that works directly with dbkeys
292
+ # instead of identifiers. Useful when you already have the full keys.
293
+ #
294
+ # @param objkeys [Array<String>] Array of full dbkeys to load
295
+ # @return [Array<Object>] Array of instantiated objects (nils for non-existent)
296
+ #
297
+ # @example Load objects by full keys
298
+ # keys = ["user:123:object", "user:456:object"]
299
+ # users = User.load_multi_by_keys(keys)
300
+ #
301
+ # @note Returns nil for empty/nil keys, maintaining position alignment with input array
302
+ #
303
+ # @see load_multi For loading by identifiers
304
+ #
305
+ def load_multi_by_keys(objkeys)
306
+ return [] if objkeys.empty?
307
+
308
+ Familia.trace :LOAD_MULTI_BY_KEYS, nil, "Loading #{objkeys.size} objects" if Familia.debug?
309
+
310
+ # Track which positions have valid keys to maintain result array alignment
311
+ valid_positions = []
312
+ objkeys.each_with_index do |objkey, idx|
313
+ valid_positions << idx unless objkey.to_s.empty?
314
+ end
315
+
316
+ # Pipeline all HGETALL commands for valid keys
317
+ multi_result = pipelined do |pipeline|
318
+ objkeys.each do |objkey|
319
+ next if objkey.to_s.empty?
320
+ pipeline.hgetall(objkey)
321
+ end
322
+ end
323
+
324
+ # Extract results array from MultiResult
325
+ results = multi_result.results
326
+
327
+ # Map results back to original positions
328
+ objects = Array.new(objkeys.size)
329
+ valid_positions.each_with_index do |pos, result_idx|
330
+ obj_hash = results[result_idx]
331
+
332
+ # Skip empty hashes (non-existent keys)
333
+ next if obj_hash.nil? || obj_hash.empty?
334
+
335
+ # Instantiate object using shared helper method
336
+ objects[pos] = instantiate_from_hash(obj_hash)
337
+ end
338
+
339
+ objects
340
+ end
341
+
190
342
  # Checks if an object with the given identifier exists in the database.
191
343
  #
192
344
  # @param identifier [String, Integer] The unique identifier for the object.
@@ -209,6 +361,9 @@ module Familia
209
361
  ret = dbclient.exists objkey
210
362
  Familia.trace :EXISTS, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
211
363
 
364
+ # Handle Redis::Future objects during transactions
365
+ return ret if ret.is_a?(Redis::Future)
366
+
212
367
  ret.positive? # differs from Valkey API but I think it's okay bc `exists?` is a predicate method.
213
368
  end
214
369
 
@@ -311,6 +466,27 @@ module Familia
311
466
  end
312
467
  alias size matching_keys_count
313
468
  alias length matching_keys_count
469
+
470
+ # Instantiates an object from a hash of field values.
471
+ #
472
+ # This is an internal helper method used by find_by_dbkey, load_multi, and
473
+ # load_multi_by_keys to eliminate code duplication. Not intended for direct use.
474
+ #
475
+ # @param obj_hash [Hash] Hash of field names to serialized values from Redis
476
+ # @return [Object] Instantiated object with deserialized fields
477
+ #
478
+ # @note This method:
479
+ # 1. Allocates a new instance without calling initialize
480
+ # 2. Initializes related DataType fields
481
+ # 3. Deserializes and assigns field values from the hash
482
+ #
483
+ # @api private
484
+ def instantiate_from_hash(obj_hash)
485
+ instance = allocate
486
+ instance.send(:initialize_relatives)
487
+ instance.send(:initialize_with_keyword_args_deserialize_value, **obj_hash)
488
+ instance
489
+ end
314
490
  end
315
491
  end
316
492
  end