familia 2.0.0.pre19 → 2.0.0.pre22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/.talismanrc +5 -1
  8. data/CHANGELOG.rst +220 -112
  9. data/CLAUDE.md +28 -1
  10. data/Gemfile +1 -1
  11. data/Gemfile.lock +20 -17
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  15. data/docs/guides/encryption.md +486 -0
  16. data/docs/guides/feature-encrypted-fields.md +123 -7
  17. data/docs/guides/feature-expiration.md +161 -117
  18. data/docs/guides/feature-external-identifiers.md +415 -443
  19. data/docs/guides/feature-object-identifiers.md +400 -269
  20. data/docs/guides/feature-quantization.md +120 -6
  21. data/docs/guides/feature-relationships-indexing.md +318 -0
  22. data/docs/guides/feature-relationships-methods.md +146 -604
  23. data/docs/guides/feature-relationships-participation.md +263 -0
  24. data/docs/guides/feature-relationships.md +118 -136
  25. data/docs/guides/feature-system-devs.md +176 -693
  26. data/docs/guides/feature-system.md +119 -6
  27. data/docs/guides/feature-transient-fields.md +81 -0
  28. data/docs/guides/field-system.md +778 -0
  29. data/docs/guides/index.md +32 -15
  30. data/docs/guides/logging.md +187 -0
  31. data/docs/guides/optimized-loading.md +674 -0
  32. data/docs/guides/thread-safety-monitoring.md +61 -0
  33. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  34. data/docs/migrating/v2.0.0-pre22.md +241 -0
  35. data/docs/overview.md +7 -9
  36. data/docs/reference/api-technical.md +267 -320
  37. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  38. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  39. data/examples/autoloader/mega_customer.rb +2 -0
  40. data/examples/datatype_standalone.rb +4 -3
  41. data/examples/encrypted_fields.rb +2 -1
  42. data/examples/json_usage_patterns.rb +2 -0
  43. data/examples/relationships.rb +3 -0
  44. data/examples/safe_dump.rb +2 -1
  45. data/examples/sampling_demo.rb +53 -0
  46. data/examples/single_connection_transaction_confusions.rb +2 -1
  47. data/familia.gemspec +2 -1
  48. data/lib/familia/base.rb +2 -0
  49. data/lib/familia/connection/behavior.rb +2 -0
  50. data/lib/familia/connection/handlers.rb +2 -0
  51. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  52. data/lib/familia/connection/middleware.rb +34 -24
  53. data/lib/familia/connection/operation_core.rb +3 -2
  54. data/lib/familia/connection/operations.rb +2 -0
  55. data/lib/familia/connection/pipelined_core.rb +3 -3
  56. data/lib/familia/connection/transaction_core.rb +69 -2
  57. data/lib/familia/connection.rb +18 -3
  58. data/lib/familia/data_type/class_methods.rb +3 -1
  59. data/lib/familia/data_type/connection.rb +2 -0
  60. data/lib/familia/data_type/database_commands.rb +2 -0
  61. data/lib/familia/data_type/serialization.rb +79 -52
  62. data/lib/familia/data_type/settings.rb +2 -0
  63. data/lib/familia/data_type/types/counter.rb +2 -0
  64. data/lib/familia/data_type/types/hashkey.rb +7 -5
  65. data/lib/familia/data_type/types/listkey.rb +2 -0
  66. data/lib/familia/data_type/types/lock.rb +2 -0
  67. data/lib/familia/data_type/types/sorted_set.rb +7 -10
  68. data/lib/familia/data_type/types/stringkey.rb +24 -0
  69. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  70. data/lib/familia/data_type.rb +2 -0
  71. data/lib/familia/encryption/encrypted_data.rb +4 -2
  72. data/lib/familia/encryption/manager.rb +2 -0
  73. data/lib/familia/encryption/provider.rb +2 -0
  74. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  75. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  76. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  77. data/lib/familia/encryption/registry.rb +2 -0
  78. data/lib/familia/encryption/request_cache.rb +2 -0
  79. data/lib/familia/encryption.rb +9 -2
  80. data/lib/familia/errors.rb +2 -0
  81. data/lib/familia/features/autoloader.rb +2 -0
  82. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  83. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  84. data/lib/familia/features/encrypted_fields.rb +2 -2
  85. data/lib/familia/features/expiration/extensions.rb +3 -1
  86. data/lib/familia/features/expiration.rb +12 -4
  87. data/lib/familia/features/external_identifier.rb +62 -7
  88. data/lib/familia/features/object_identifier.rb +49 -0
  89. data/lib/familia/features/quantization.rb +3 -1
  90. data/lib/familia/features/relationships/README.md +3 -1
  91. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  92. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +138 -9
  93. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +97 -21
  95. data/lib/familia/features/relationships/indexing.rb +3 -0
  96. data/lib/familia/features/relationships/indexing_relationship.rb +3 -1
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  98. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  100. data/lib/familia/features/relationships/participation.rb +155 -69
  101. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  102. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  103. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  104. data/lib/familia/features/relationships.rb +5 -3
  105. data/lib/familia/features/safe_dump.rb +2 -0
  106. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  107. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  108. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  109. data/lib/familia/features/transient_fields.rb +2 -0
  110. data/lib/familia/features.rb +2 -0
  111. data/lib/familia/field_type.rb +3 -1
  112. data/lib/familia/horreum/connection.rb +17 -1
  113. data/lib/familia/horreum/database_commands.rb +8 -1
  114. data/lib/familia/horreum/definition.rb +16 -6
  115. data/lib/familia/horreum/management.rb +353 -52
  116. data/lib/familia/horreum/persistence.rb +179 -108
  117. data/lib/familia/horreum/related_fields.rb +2 -0
  118. data/lib/familia/horreum/serialization.rb +23 -4
  119. data/lib/familia/horreum/settings.rb +2 -0
  120. data/lib/familia/horreum/utils.rb +2 -0
  121. data/lib/familia/horreum.rb +15 -1
  122. data/lib/familia/identifier_extractor.rb +3 -1
  123. data/lib/familia/instrumentation.rb +156 -0
  124. data/lib/familia/json_serializer.rb +2 -0
  125. data/lib/familia/logging.rb +92 -32
  126. data/lib/familia/refinements/dear_json.rb +2 -0
  127. data/lib/familia/refinements/stylize_words.rb +2 -14
  128. data/lib/familia/refinements/time_literals.rb +2 -0
  129. data/lib/familia/refinements.rb +2 -0
  130. data/lib/familia/secure_identifier.rb +10 -2
  131. data/lib/familia/settings.rb +2 -0
  132. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  133. data/lib/familia/thread_safety/monitor.rb +328 -0
  134. data/lib/familia/utils.rb +13 -0
  135. data/lib/familia/verifiable_identifier.rb +3 -1
  136. data/lib/familia/version.rb +3 -1
  137. data/lib/familia.rb +31 -4
  138. data/lib/middleware/database_command_counter.rb +152 -0
  139. data/lib/middleware/database_logger.rb +295 -170
  140. data/lib/multi_result.rb +61 -31
  141. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  142. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  143. data/try/edge_cases/json_serialization_try.rb +2 -0
  144. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  145. data/try/edge_cases/race_conditions_try.rb +4 -0
  146. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  147. data/try/edge_cases/string_coercion_try.rb +2 -0
  148. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  149. data/try/features/count_any_edge_cases_try.rb +486 -0
  150. data/try/features/count_any_methods_try.rb +197 -0
  151. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  152. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  153. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  154. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  155. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  156. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  157. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  158. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  159. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  160. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  161. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  162. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  164. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  165. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  166. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  167. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  168. data/try/features/encryption/config_persistence_try.rb +4 -0
  169. data/try/features/encryption/core_try.rb +4 -0
  170. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  171. data/try/features/encryption/module_loading_try.rb +4 -0
  172. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  173. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  174. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  175. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  176. data/try/features/expiration/expiration_try.rb +4 -0
  177. data/try/features/external_identifier/external_identifier_try.rb +305 -8
  178. data/try/features/feature_dependencies_try.rb +2 -0
  179. data/try/features/feature_improvements_try.rb +2 -0
  180. data/try/features/field_groups_try.rb +2 -0
  181. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  182. data/try/features/object_identifier/object_identifier_try.rb +140 -0
  183. data/try/features/quantization/quantization_try.rb +4 -0
  184. data/try/features/real_feature_integration_try.rb +2 -0
  185. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  186. data/try/features/relationships/indexing_rebuild_try.rb +606 -0
  187. data/try/features/relationships/indexing_try.rb +2 -0
  188. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  189. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  190. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  192. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  193. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  194. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  195. data/try/features/relationships/relationships_api_changes_try.rb +2 -0
  196. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  197. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  198. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  199. data/try/features/relationships/relationships_performance_try.rb +4 -0
  200. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  201. data/try/features/relationships/relationships_try.rb +6 -4
  202. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  203. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  204. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  205. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  206. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  207. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  208. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  209. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  210. data/try/integration/connection/fiber_context_preservation_try.rb +4 -0
  211. data/try/integration/connection/handler_constraints_try.rb +4 -0
  212. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  213. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  214. data/try/integration/connection/operation_mode_guards_try.rb +4 -0
  215. data/try/integration/connection/pipeline_fallback_integration_try.rb +3 -0
  216. data/try/integration/connection/pools_try.rb +4 -0
  217. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  218. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  219. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  220. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  221. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  222. data/try/integration/connection/transaction_modes_try.rb +4 -0
  223. data/try/integration/conventional_inheritance_try.rb +4 -0
  224. data/try/integration/create_method_try.rb +4 -0
  225. data/try/integration/cross_component_try.rb +4 -0
  226. data/try/integration/data_types/datatype_pipelines_try.rb +9 -3
  227. data/try/integration/data_types/datatype_transactions_try.rb +17 -7
  228. data/try/integration/database_consistency_try.rb +4 -0
  229. data/try/integration/familia_extended_try.rb +4 -0
  230. data/try/integration/familia_members_methods_try.rb +4 -0
  231. data/try/integration/models/customer_safe_dump_try.rb +4 -0
  232. data/try/integration/models/customer_try.rb +7 -3
  233. data/try/integration/models/datatype_base_try.rb +4 -0
  234. data/try/integration/models/familia_object_try.rb +4 -0
  235. data/try/integration/persistence_operations_try.rb +4 -0
  236. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  237. data/try/integration/save_methods_consistency_try.rb +241 -0
  238. data/try/integration/scenarios_try.rb +4 -0
  239. data/try/integration/secure_identifier_try.rb +4 -0
  240. data/try/integration/transaction_safety_core_try.rb +176 -0
  241. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  242. data/try/integration/verifiable_identifier_try.rb +4 -0
  243. data/try/investigation/pipeline_routing/README.md +228 -0
  244. data/try/performance/benchmarks_try.rb +4 -0
  245. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  246. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  247. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  248. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  249. data/try/support/debugging/debug_aad_process.rb +3 -0
  250. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  251. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  252. data/try/support/debugging/debug_context_aad.rb +3 -0
  253. data/try/support/debugging/debug_context_simple.rb +3 -0
  254. data/try/support/debugging/debug_cross_context.rb +3 -0
  255. data/try/support/debugging/debug_database_load.rb +3 -0
  256. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  257. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  258. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  259. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  260. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  261. data/try/support/debugging/debug_load_path.rb +3 -0
  262. data/try/support/debugging/debug_method_definition.rb +3 -0
  263. data/try/support/debugging/debug_method_resolution.rb +3 -0
  264. data/try/support/debugging/debug_minimal.rb +3 -0
  265. data/try/support/debugging/debug_provider.rb +3 -0
  266. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  267. data/try/support/debugging/debug_string_class.rb +3 -0
  268. data/try/support/debugging/debug_test.rb +3 -0
  269. data/try/support/debugging/debug_test_design.rb +3 -0
  270. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  271. data/try/support/debugging/provider_diagnostics.rb +4 -0
  272. data/try/support/helpers/test_cleanup.rb +4 -0
  273. data/try/support/helpers/test_helpers.rb +5 -0
  274. data/try/support/memory/memory_basic_test.rb +4 -0
  275. data/try/support/memory/memory_detailed_test.rb +4 -0
  276. data/try/support/memory/memory_search_for_string.rb +4 -0
  277. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  278. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  279. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  280. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  281. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  282. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  283. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  284. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  285. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  286. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  287. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  288. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  289. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  290. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  291. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  292. data/try/thread_safety/README.md +496 -0
  293. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  294. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  295. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  296. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  297. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  298. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  299. data/try/thread_safety/field_registration_race_try.rb +222 -0
  300. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  301. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  302. data/try/thread_safety/module_config_race_try.rb +175 -0
  303. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  304. data/try/unit/core/autoloader_try.rb +4 -0
  305. data/try/unit/core/base_enhancements_try.rb +4 -0
  306. data/try/unit/core/connection_try.rb +4 -0
  307. data/try/unit/core/errors_try.rb +4 -0
  308. data/try/unit/core/extensions_try.rb +4 -0
  309. data/try/unit/core/familia_logger_try.rb +2 -0
  310. data/try/unit/core/familia_try.rb +4 -0
  311. data/try/unit/core/middleware_sampling_try.rb +335 -0
  312. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  313. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  314. data/try/unit/core/middleware_try.rb +4 -0
  315. data/try/unit/core/settings_try.rb +4 -0
  316. data/try/unit/core/time_utils_try.rb +4 -0
  317. data/try/unit/core/tools_try.rb +4 -0
  318. data/try/unit/core/utils_try.rb +37 -0
  319. data/try/unit/data_types/boolean_try.rb +39 -22
  320. data/try/unit/data_types/counter_try.rb +4 -0
  321. data/try/unit/data_types/datatype_base_try.rb +4 -0
  322. data/try/unit/data_types/hash_try.rb +6 -2
  323. data/try/unit/data_types/list_try.rb +4 -0
  324. data/try/unit/data_types/lock_try.rb +4 -0
  325. data/try/unit/data_types/serialization_try.rb +386 -0
  326. data/try/unit/data_types/sorted_set_try.rb +4 -0
  327. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  328. data/try/unit/data_types/string_try.rb +4 -0
  329. data/try/unit/data_types/unsortedset_try.rb +4 -0
  330. data/try/unit/familia_resolve_class_try.rb +116 -0
  331. data/try/unit/horreum/auto_indexing_on_save_try.rb +5 -1
  332. data/try/unit/horreum/automatic_index_validation_try.rb +2 -0
  333. data/try/unit/horreum/base_try.rb +4 -0
  334. data/try/unit/horreum/class_methods_try.rb +4 -0
  335. data/try/unit/horreum/commands_try.rb +4 -0
  336. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  337. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +6 -1
  338. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  339. data/try/unit/horreum/field_categories_try.rb +4 -0
  340. data/try/unit/horreum/field_definition_try.rb +4 -0
  341. data/try/unit/horreum/initialization_try.rb +4 -0
  342. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  343. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  344. data/try/unit/horreum/relations_try.rb +4 -0
  345. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  346. data/try/unit/horreum/serialization_try.rb +4 -0
  347. data/try/unit/horreum/settings_try.rb +4 -0
  348. data/try/unit/horreum/unique_index_edge_cases_try.rb +4 -0
  349. data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -0
  350. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  351. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  352. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  353. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  354. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  355. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  356. data/try/unit/thread_safety_monitor_try.rb +149 -0
  357. metadata +69 -17
  358. data/.github/workflows/code-quality.yml +0 -138
  359. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +0 -91
  360. data/changelog.d/20251011_203905_delano_next.rst +0 -30
  361. data/changelog.d/20251011_212633_delano_next.rst +0 -13
  362. data/changelog.d/20251011_221253_delano_next.rst +0 -26
  363. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  364. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  365. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  366. data/docs/archive/README.md +0 -64
  367. data/docs/archive/api-reference.md +0 -333
  368. data/docs/guides/core-field-system.md +0 -806
  369. data/docs/guides/implementation.md +0 -276
  370. data/docs/guides/security-model.md +0 -183
@@ -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
@@ -104,56 +106,71 @@ module Familia
104
106
  # key.
105
107
  #
106
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).
107
112
  # @return [Object, nil] An instance of the class if the key exists, nil
108
113
  # otherwise.
109
114
  # @raise [ArgumentError] If the provided key is empty.
110
115
  #
111
- # This method performs a two-step process to safely retrieve and
112
- # instantiate objects:
116
+ # This method can operate in two modes:
113
117
  #
114
- # 1. It first checks if the key exists in the database. This is crucial because:
115
- # - It provides a definitive answer about the object's existence.
116
- # - It prevents ambiguity that could arise from `hgetall` returning an
117
- # empty hash for non-existent keys, which could lead to the creation
118
- # 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)
119
124
  #
120
- # 2. If the key exists, it retrieves the object's data and instantiates
121
- # 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
122
132
  #
123
- # This approach ensures that we only attempt to instantiate objects that
124
- # actually exist in Valkey/Redis, improving reliability and simplifying
125
- # debugging.
133
+ # @example Safe mode (default)
134
+ # User.find_by_key("user:123") # 2 commands: EXISTS + HGETALL
126
135
  #
127
- # @example
128
- # User.find_by_key("user:123") # Returns a User instance if it exists,
129
- # 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).
130
141
  #
131
- def find_by_dbkey(objkey)
142
+ def find_by_dbkey(objkey, check_exists: true)
132
143
  raise ArgumentError, 'Empty key' if objkey.to_s.empty?
133
144
 
134
- # We use a lower-level method here b/c we're working with the
135
- # full key and not just the identifier.
136
- does_exist = dbclient.exists(objkey).positive?
137
-
138
- Familia.ld "[find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
139
- Familia.trace :FIND_BY_DBKEY_KEY, nil, objkey
140
-
141
- # This is the reason for calling exists first. We want to definitively
142
- # and without any ambiguity know if the object exists in the database. If it
143
- # doesn't, we return nil. If it does, we proceed to load the object.
144
- # Otherwise, hgetall will return an empty hash, which will be passed to
145
- # the constructor, which will then be annoying to debug.
146
- 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
147
165
 
148
166
  obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
149
167
  Familia.trace :FIND_BY_DBKEY_INSPECT, nil, "#{objkey}: #{obj.inspect}"
150
168
 
151
- # Create instance and deserialize fields using existing helper method
152
- # This avoids duplicating deserialization logic and keeps field-by-field processing
153
- instance = allocate
154
- instance.send(:initialize_relatives)
155
- instance.send(:initialize_with_keyword_args_deserialize_value, **obj)
156
- 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)
157
174
  end
158
175
  alias find_by_key find_by_dbkey
159
176
 
@@ -161,8 +178,10 @@ module Familia
161
178
  #
162
179
  # @param identifier [String, Integer] The unique identifier for the
163
180
  # object.
164
- # @param suffix [Symbol] The suffix to use in the dbkey (default:
165
- # :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.
166
185
  # @return [Object, nil] An instance of the class if found, nil otherwise.
167
186
  #
168
187
  # This method constructs the full dbkey using the provided identifier
@@ -173,23 +192,153 @@ module Familia
173
192
  # making it easier to retrieve objects when you only have their
174
193
  # identifier.
175
194
  #
176
- # @example
177
- # 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
178
200
  #
179
- 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)
180
205
  suffix ||= self.suffix
181
206
  return nil if identifier.to_s.empty?
182
207
 
183
208
  objkey = dbkey(identifier, suffix)
184
209
 
185
- Familia.ld "[find_by_id] #{self} from key #{objkey})"
210
+ Familia.debug "[find_by_id] #{self} from key #{objkey})"
186
211
  Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
187
- find_by_dbkey objkey
212
+ find_by_dbkey objkey, check_exists: check_exists
188
213
  end
189
214
  alias find_by_id find_by_identifier
190
215
  alias find find_by_id
191
216
  alias load find_by_id
192
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
+
193
342
  # Checks if an object with the given identifier exists in the database.
194
343
  #
195
344
  # @param identifier [String, Integer] The unique identifier for the object.
@@ -234,7 +383,7 @@ module Familia
234
383
  #
235
384
  def destroy!(identifier, suffix = nil)
236
385
  suffix ||= self.suffix
237
- return MultiResult.new(false, []) if identifier.to_s.empty?
386
+ raise Familia::NoIdentifier, "#{self} requires non-empty identifier" if identifier.to_s.empty?
238
387
 
239
388
  objkey = dbkey identifier, suffix
240
389
 
@@ -301,22 +450,174 @@ module Familia
301
450
  def all(suffix = nil)
302
451
  suffix ||= self.suffix
303
452
  # objects that could not be parsed will be nil
304
- keys(suffix).filter_map { |k| find_by_key(k) }
453
+ find_keys(suffix).filter_map { |k| find_by_key(k) }
305
454
  end
306
455
 
307
- def any?(filter = '*')
308
- matching_keys_count(filter).positive?
456
+ # Returns the number of tracked instances (fast, from instances sorted set).
457
+ #
458
+ # This method provides O(1) performance by querying the `instances` sorted set,
459
+ # which is automatically maintained when objects are created/destroyed through
460
+ # Familia. However, objects deleted outside Familia (e.g., direct Redis commands)
461
+ # may leave stale entries.
462
+ #
463
+ # @return [Integer] Number of instances in the instances sorted set
464
+ #
465
+ # @example
466
+ # User.create(email: 'test@example.com')
467
+ # User.count #=> 1
468
+ #
469
+ # @note For authoritative count, use {#scan_count} (production-safe) or {#keys_count} (blocking)
470
+ # @see #scan_count Production-safe authoritative count via SCAN
471
+ # @see #keys_count Blocking authoritative count via KEYS
472
+ # @see #instances The underlying sorted set
473
+ #
474
+ def count
475
+ instances.count
309
476
  end
477
+ alias size count
478
+ alias length count
310
479
 
311
- # Returns the number of dbkeys matching the given filter pattern
312
- # @param filter [String] dbkey pattern to match (default: '*')
313
- # @return [Integer] Number of matching keys
480
+ # Returns authoritative count using blocking KEYS command (production-dangerous).
481
+ #
482
+ # ⚠️ WARNING: This method uses the KEYS command which blocks Redis during execution.
483
+ # It scans ALL keys in the database and should NEVER be used in production.
314
484
  #
315
- def matching_keys_count(filter = '*')
485
+ # @param filter [String] Key pattern to match (default: '*')
486
+ # @return [Integer] Number of matching keys in Redis
487
+ #
488
+ # @example
489
+ # User.keys_count #=> 1 (all User objects)
490
+ # User.keys_count('a*') #=> 1 (Users with IDs starting with 'a')
491
+ #
492
+ # @note For production-safe authoritative count, use {#scan_count}
493
+ # @see #scan_count Production-safe alternative using SCAN
494
+ # @see #count Fast count from instances sorted set
495
+ #
496
+ def keys_count(filter = '*')
316
497
  dbclient.keys(dbkey(filter)).compact.size
317
498
  end
318
- alias size matching_keys_count
319
- alias length matching_keys_count
499
+
500
+ # Returns authoritative count using non-blocking SCAN command (production-safe).
501
+ #
502
+ # This method uses cursor-based SCAN iteration to count matching keys without
503
+ # blocking Redis. Safe for production use as it processes keys in chunks.
504
+ #
505
+ # @param filter [String] Key pattern to match (default: '*')
506
+ # @return [Integer] Number of matching keys in Redis
507
+ #
508
+ # @example
509
+ # User.scan_count #=> 1 (all User objects)
510
+ # User.scan_count('a*') #=> 1 (Users with IDs starting with 'a')
511
+ #
512
+ # @note For fast count (potentially stale), use {#count}
513
+ # @see #count Fast count from instances sorted set
514
+ # @see #keys_count Blocking alternative (production-dangerous)
515
+ #
516
+ def scan_count(filter = '*')
517
+ pattern = dbkey(filter)
518
+ count = 0
519
+ cursor = "0"
520
+
521
+ loop do
522
+ cursor, keys = dbclient.scan(cursor, match: pattern, count: 1000)
523
+ count += keys.size
524
+ break if cursor == "0"
525
+ end
526
+
527
+ count
528
+ end
529
+ alias count! scan_count
530
+
531
+ # Checks if any tracked instances exist (fast, from instances sorted set).
532
+ #
533
+ # This method provides O(1) performance by querying the `instances` sorted set.
534
+ # However, objects deleted outside Familia may leave stale entries.
535
+ #
536
+ # @return [Boolean] true if instances sorted set is non-empty
537
+ #
538
+ # @example
539
+ # User.create(email: 'test@example.com')
540
+ # User.any? #=> true
541
+ #
542
+ # @note For authoritative check, use {#scan_any?} (production-safe) or {#keys_any?} (blocking)
543
+ # @see #scan_any? Production-safe authoritative check via SCAN
544
+ # @see #keys_any? Blocking authoritative check via KEYS
545
+ # @see #count Fast count of instances
546
+ #
547
+ def any?
548
+ count.positive?
549
+ end
550
+
551
+ # Checks if any objects exist using blocking KEYS command (production-dangerous).
552
+ #
553
+ # ⚠️ WARNING: This method uses the KEYS command which blocks Redis during execution.
554
+ # It scans ALL keys in the database and should NEVER be used in production.
555
+ #
556
+ # @param filter [String] Key pattern to match (default: '*')
557
+ # @return [Boolean] true if any matching keys exist in Redis
558
+ #
559
+ # @example
560
+ # User.keys_any? #=> true (any User objects)
561
+ # User.keys_any?('a*') #=> true (Users with IDs starting with 'a')
562
+ #
563
+ # @note For production-safe authoritative check, use {#scan_any?}
564
+ # @see #scan_any? Production-safe alternative using SCAN
565
+ # @see #any? Fast existence check from instances sorted set
566
+ #
567
+ def keys_any?(filter = '*')
568
+ keys_count(filter).positive?
569
+ end
570
+
571
+ # Checks if any objects exist using non-blocking SCAN command (production-safe).
572
+ #
573
+ # This method uses cursor-based SCAN iteration to check for matching keys without
574
+ # blocking Redis. Safe for production use and returns early on first match.
575
+ #
576
+ # @param filter [String] Key pattern to match (default: '*')
577
+ # @return [Boolean] true if any matching keys exist in Redis
578
+ #
579
+ # @example
580
+ # User.scan_any? #=> true (any User objects)
581
+ # User.scan_any?('a*') #=> true (Users with IDs starting with 'a')
582
+ #
583
+ # @note For fast check (potentially stale), use {#any?}
584
+ # @see #any? Fast existence check from instances sorted set
585
+ # @see #keys_any? Blocking alternative (production-dangerous)
586
+ #
587
+ def scan_any?(filter = '*')
588
+ pattern = dbkey(filter)
589
+ cursor = "0"
590
+
591
+ loop do
592
+ cursor, keys = dbclient.scan(cursor, match: pattern, count: 100)
593
+ return true unless keys.empty?
594
+ break if cursor == "0"
595
+ end
596
+
597
+ false
598
+ end
599
+ alias any! scan_any?
600
+
601
+ # Instantiates an object from a hash of field values.
602
+ #
603
+ # This is an internal helper method used by find_by_dbkey, load_multi, and
604
+ # load_multi_by_keys to eliminate code duplication. Not intended for direct use.
605
+ #
606
+ # @param obj_hash [Hash] Hash of field names to serialized values from Redis
607
+ # @return [Object] Instantiated object with deserialized fields
608
+ #
609
+ # @note This method:
610
+ # 1. Allocates a new instance without calling initialize
611
+ # 2. Initializes related DataType fields
612
+ # 3. Deserializes and assigns field values from the hash
613
+ #
614
+ # @api private
615
+ def instantiate_from_hash(obj_hash)
616
+ instance = allocate
617
+ instance.send(:initialize_relatives)
618
+ instance.send(:initialize_with_keyword_args_deserialize_value, **obj_hash)
619
+ instance
620
+ end
320
621
  end
321
622
  end
322
623
  end