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
  # examples/autoloader/mega_customer/features/deprecated_fields.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # Extend the MegaCustomer class to organize all of the deprecated fields into one place
4
6
  class MegaCustomer < Familia::Horreum
@@ -1,4 +1,6 @@
1
1
  # examples/autoloader/mega_customer/safe_dump_fields.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # Extend the MegaCustomer class to add safe dump fields
4
6
  class MegaCustomer < Familia::Horreum
@@ -1,4 +1,6 @@
1
1
  # examples/autoloader/mega_customer.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative '../../lib/familia'
4
6
 
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/datatype_standalone.rb
3
+ #
4
+ # frozen_string_literal: true
5
+
6
+ # Demonstration: Familia::StringKey for Session Storage with Atomic Transactions
7
+ # This example shows how to use Familia's DataType classes independently
8
+ # without inheriting from Familia::Horreum. It implements a Rack-compatible
9
+ # session store using Familia::StringKey for secure, TTL-managed storage.
10
+ #
11
+ # Key Familia Features Demonstrated:
12
+ # - Standalone DataType usage (no parent model required)
13
+ # - Atomic transactions for multi-operation consistency
14
+ # - TTL management for automatic expiration
15
+ # - JSON serialization for complex data structures
16
+ # - Direct Redis access through DataType objects
17
+
18
+ require 'rack/session/abstract/id'
19
+ require 'securerandom'
20
+
21
+ require 'base64'
22
+ require 'openssl'
23
+
24
+ # Load local development version of Familia (not the gem)
25
+ begin
26
+ require_relative '../lib/familia'
27
+ rescue LoadError
28
+ # Fall back to installed gem
29
+ require 'familia'
30
+ end
31
+
32
+ # SecureSessionStore - a rack-session compatible session store using Familia::StringKey
33
+ #
34
+ # Usage:
35
+ # ruby examples/datatype_standalone.rb
36
+ # # Or in your Rack app:
37
+ # use SecureSessionStore, secret: 'your-secret-key', expire_after: 3600
38
+ #
39
+ # @see https://raw.githubusercontent.com/rack/rack-session/dadcfe60f193e8/lib/rack/session/abstract/id.rb
40
+ # @see https://raw.githubusercontent.com/rack/rack-session/dadcfe60f193e8/lib/rack/session/encryptor.rb
41
+ #
42
+ class SecureSessionStore < Rack::Session::Abstract::PersistedSecure
43
+ unless defined?(DEFAULT_OPTIONS)
44
+ DEFAULT_OPTIONS = {
45
+ key: 'project.session',
46
+ expire_after: 86_400, # 24 hours default
47
+ namespace: 'session',
48
+ sidbits: 256, # Required by Rack::Session::Abstract::Persisted
49
+ dbclient: nil,
50
+ }.freeze
51
+ end
52
+
53
+ attr_reader :dbclient
54
+
55
+ def initialize(app, options = {})
56
+ # Require a secret for security
57
+ raise ArgumentError, 'Secret required for secure sessions' unless options[:secret]
58
+
59
+ # Merge options with defaults
60
+ options = DEFAULT_OPTIONS.merge(options)
61
+
62
+ # Configure Familia connection if redis_uri provided
63
+ @dbclient = options[:dbclient] || Familia.dbclient
64
+
65
+ super
66
+
67
+ @secret = options[:secret]
68
+ @expire_after = options[:expire_after]
69
+ @namespace = options[:namespace] || 'session'
70
+
71
+ # Derive different keys for different purposes
72
+ @hmac_key = derive_key('hmac')
73
+ @encryption_key = derive_key('encryption')
74
+ end
75
+
76
+ private
77
+
78
+ # Create a StringKey instance for a session ID
79
+ def get_stringkey(sid)
80
+ return nil if sid.to_s.empty?
81
+
82
+ key = Familia.join(@namespace, sid)
83
+ Familia::StringKey.new(key,
84
+ ttl: @expire_after,
85
+ default: nil)
86
+ end
87
+
88
+ def delete_session(_request, sid, _options)
89
+ # Extract string ID from SessionId object if needed
90
+ sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
91
+
92
+ get_stringkey(sid_string)&.del
93
+
94
+ generate_sid
95
+ end
96
+
97
+ def valid_session_id?(sid)
98
+ return false if sid.to_s.empty?
99
+ return false unless sid.match?(/\A[a-f0-9]{64,}\z/)
100
+
101
+ # Additional security checks could go here
102
+ true
103
+ end
104
+
105
+ def valid_hmac?(data, hmac)
106
+ expected = compute_hmac(data)
107
+ return false unless hmac.is_a?(String) && expected.is_a?(String) && hmac.bytesize == expected.bytesize
108
+
109
+ Rack::Utils.secure_compare(expected, hmac)
110
+ end
111
+
112
+ def derive_key(purpose)
113
+ OpenSSL::HMAC.hexdigest('SHA256', @secret, "session-#{purpose}")
114
+ end
115
+
116
+ def compute_hmac(data)
117
+ OpenSSL::HMAC.hexdigest('SHA256', @hmac_key, data)
118
+ end
119
+
120
+ def find_session(_request, sid)
121
+ # Parent class already extracts sid from cookies
122
+ # sid may be a SessionId object or nil
123
+ sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
124
+
125
+ # Only generate new sid if none provided or invalid
126
+ return [generate_sid, {}] unless sid_string && valid_session_id?(sid_string)
127
+
128
+ begin
129
+ stringkey = get_stringkey(sid_string)
130
+ stored_data = stringkey.value if stringkey
131
+
132
+ # If no data stored, return empty session
133
+ return [sid, {}] unless stored_data
134
+
135
+ # Verify HMAC before deserializing
136
+ data, hmac = stored_data.split('--', 2)
137
+
138
+ # If no HMAC or invalid format, create new session
139
+ unless hmac && valid_hmac?(data, hmac)
140
+ # Session tampered with - create new session
141
+ return [generate_sid, {}]
142
+ end
143
+
144
+ # Decode and parse the session data
145
+ session_data = Familia::JsonSerializer.parse(Base64.decode64(data))
146
+
147
+ [sid, session_data]
148
+ rescue Familia::PersistenceError => e
149
+ # Log error in development/debugging
150
+ Familia.debug "[Session] Error reading session #{sid_string}: #{e.message}"
151
+
152
+ # Return new session on any error
153
+ [generate_sid, {}]
154
+ end
155
+ end
156
+
157
+ def write_session(_request, sid, session_data, _options)
158
+ # Extract string ID from SessionId object if needed
159
+ sid_string = sid.respond_to?(:public_id) ? sid.public_id : sid
160
+
161
+ # Serialize and sign the data
162
+ encoded = Base64.encode64(Familia::JsonSerializer.dump(session_data)).delete("\n")
163
+ hmac = compute_hmac(encoded)
164
+ signed_data = "#{encoded}--#{hmac}"
165
+
166
+ # Get or create StringKey for this session
167
+ stringkey = get_stringkey(sid_string)
168
+
169
+ # ATOMIC TRANSACTION: Ensures both operations succeed or both fail
170
+ #
171
+ # Before DataType transaction support (PR #160), these operations were not atomic:
172
+ # stringkey.set(signed_data)
173
+ # stringkey.update_expiration(expiration: @expire_after)
174
+ #
175
+ # With transaction support, we guarantee atomicity - critical for session storage
176
+ # where partial writes could lead to sessions without TTL (memory leaks) or
177
+ # expired sessions with stale data (security issues).
178
+ #
179
+ # RECOMMENDED PATTERN: Use DataType methods inside transaction blocks
180
+ # The transaction block automatically handles the atomic MULTI/EXEC wrapping.
181
+ # DataType methods handle key generation and provide clean, expressive syntax.
182
+ stringkey.transaction do
183
+ stringkey.set(signed_data)
184
+ stringkey.update_expiration(expiration: @expire_after) if @expire_after&.positive?
185
+ end
186
+
187
+ # ADVANCED: The block yields the Redis connection for low-level access when needed
188
+ # This is useful for operations that require direct Redis command access or
189
+ # when working with multiple DataTypes in a single transaction.
190
+ #
191
+ # stringkey.transaction do |conn|
192
+ # conn.set(stringkey.dbkey, signed_data)
193
+ # conn.expire(stringkey.dbkey, @expire_after) if @expire_after&.positive?
194
+ # end
195
+
196
+ # Return the original sid (may be SessionId object)
197
+ sid
198
+ rescue Familia::PersistenceError => e
199
+ # Log error in development/debugging
200
+ Familia.debug "[Session] Error writing session #{sid_string}: #{e.message}"
201
+
202
+ # Return false to indicate failure
203
+ false
204
+ end
205
+
206
+ # Clean up expired sessions (optional, can be called periodically)
207
+ def cleanup_expired_sessions
208
+ # This would typically be handled by Redis TTL automatically
209
+ # but you could implement manual cleanup if needed
210
+ end
211
+ end
212
+
213
+ # Demo application showing session store in action
214
+ class DemoApp
215
+ def initialize
216
+ @store = SecureSessionStore.new(
217
+ proc { |_env| [200, {}, ['Demo App']] },
218
+ secret: 'demo-secret-key-change-in-production',
219
+ expire_after: 300, # 5 minutes for demo
220
+ )
221
+ end
222
+
223
+ def call(env)
224
+ puts "\n=== Familia::StringKey Session Demo ==="
225
+
226
+ # Mock Rack environment
227
+ env['rack.session'] ||= {}
228
+ env['HTTP_COOKIE'] ||= ''
229
+
230
+ # Simulate session operations
231
+ session_id = SecureRandom.hex(32)
232
+ session_data = {
233
+ 'user_id' => '12345',
234
+ 'username' => 'demo_user',
235
+ 'login_time' => Time.now.to_i,
236
+ 'preferences' => { 'theme' => 'dark', 'lang' => 'en' },
237
+ }
238
+
239
+ puts 'Writing session data...'
240
+ result = @store.send(:write_session, nil, session_id, session_data, {})
241
+ puts " Result: #{result ? 'Success' : 'Failed'}"
242
+
243
+ puts "\nReading session data..."
244
+ found_id, found_data = @store.send(:find_session, nil, session_id)
245
+ puts " Session ID: #{found_id}"
246
+ puts " Data: #{found_data}"
247
+
248
+ puts "\nDeleting session..."
249
+ @store.send(:delete_session, nil, session_id, {})
250
+
251
+ puts "\nVerifying deletion..."
252
+ deleted_id, deleted_data = @store.send(:find_session, nil, session_id)
253
+ puts " Data after deletion: #{deleted_data}"
254
+ puts " New session ID: #{deleted_id == session_id ? 'Same' : 'Generated'}"
255
+
256
+ puts "\n✅ Demo complete!"
257
+ puts "\nKey Familia Features Used:"
258
+ puts '• Familia::StringKey for typed Redis storage'
259
+ puts '• Automatic TTL management'
260
+ puts '• Direct Redis operations (set, get, del)'
261
+ puts '• JSON serialization support'
262
+ puts '• No Horreum inheritance required'
263
+
264
+ [200, { 'Content-Type' => 'text/plain' }, ['Familia StringKey Demo - Check console output']]
265
+ end
266
+ end
267
+
268
+ # Run demo if executed directly
269
+ if __FILE__ == $0
270
+ # Ensure Redis is available
271
+ begin
272
+ Familia.dbclient.ping
273
+ rescue Familia::PersistenceError => e
274
+ puts "❌ Redis connection failed: #{e.message}"
275
+ puts ' Please ensure Redis is running on localhost:6379'
276
+ exit 1
277
+ end
278
+
279
+ # Run the demo
280
+ app = DemoApp.new
281
+ app.call({})
282
+ end
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
-
3
2
  # examples/encrypted_fields.rb
4
3
  #
4
+ # frozen_string_literal: true
5
+
5
6
  # Demonstrates the EncryptedFields feature for protecting sensitive data.
6
7
  # This feature provides transparent encryption/decryption of sensitive fields
7
8
  # using strong cryptographic algorithms with field-specific key derivation.
@@ -1,5 +1,7 @@
1
1
  # examples/json_usage_patterns.rb
2
2
  #
3
+ # frozen_string_literal: true
4
+
3
5
  # This file demonstrates the JSON serialization patterns available in Familia,
4
6
  # showing both the secure defaults and optional developer convenience features.
5
7
 
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
+ # examples/relationships.rb
3
+ #
4
+ # frozen_string_literal: true
2
5
 
3
6
  # Basic Relationships Example
4
7
  # This example demonstrates the core features of Familia's relationships system
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
-
3
2
  # examples/safe_dump.rb
4
3
  #
4
+ # frozen_string_literal: true
5
+
5
6
  # Demonstrates the SafeDump feature with the new DSL methods.
6
7
  # SafeDump allows you to control which fields are exposed when
7
8
  # serializing objects, preventing accidental exposure of sensitive data.
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/sampling_demo.rb
3
+ #
4
+ # frozen_string_literal: true
5
+
6
+ # Demonstrates DatabaseLogger sampling to reduce log volume in high-traffic scenarios.
7
+ # Run with: bundle exec ruby examples/sampling_demo.rb
8
+
9
+ require_relative '../lib/familia'
10
+ require 'logger'
11
+
12
+ # Enable database command logging (middleware registered automatically)
13
+ Familia.enable_database_logging = true
14
+ DatabaseLogger.logger = Familia::FamiliaLogger.new($stdout)
15
+ DatabaseLogger.logger.level = Familia::FamiliaLogger::TRACE
16
+
17
+ # Scenario 1: No sampling (default) - logs every command
18
+ puts "\n=== Scenario 1: No Sampling (logs all 100 commands) ==="
19
+ DatabaseLogger.sample_rate = nil
20
+ 100.times { |i| Familia.dbclient.set("key_#{i}", "value_#{i}") }
21
+ puts "Commands captured: #{DatabaseLogger.commands.size}"
22
+ puts "(Check output above - should see ~100 log lines)"
23
+
24
+ # Scenario 2: 10% sampling - logs ~10 commands
25
+ puts "\n=== Scenario 2: 10% Sampling (logs ~10 of 100 commands) ==="
26
+ DatabaseLogger.clear_commands
27
+ DatabaseLogger.sample_rate = 0.1
28
+ 100.times { |i| Familia.dbclient.set("sampled_10_#{i}", "value_#{i}") }
29
+ puts "Commands captured: #{DatabaseLogger.commands.size}"
30
+ puts "(Check output above - should see ~10 log lines)"
31
+
32
+ # Scenario 3: 1% sampling - logs ~1 command (production-friendly)
33
+ puts "\n=== Scenario 3: 1% Sampling (logs ~1 of 100 commands) ==="
34
+ DatabaseLogger.clear_commands
35
+ DatabaseLogger.sample_rate = 0.01
36
+ 100.times { |i| Familia.dbclient.set("sampled_1_#{i}", "value_#{i}") }
37
+ puts "Commands captured: #{DatabaseLogger.commands.size}"
38
+ puts "(Check output above - should see ~1 log line)"
39
+
40
+ # Scenario 4: Sampling with structured logging
41
+ puts "\n=== Scenario 4: 10% Sampling + Structured Logging ==="
42
+ DatabaseLogger.clear_commands
43
+ DatabaseLogger.sample_rate = 0.1
44
+ DatabaseLogger.structured_logging = true
45
+ 100.times { |i| Familia.dbclient.set("structured_#{i}", "value_#{i}") }
46
+ puts "Commands captured: #{DatabaseLogger.commands.size}"
47
+ puts "(Check structured output above)"
48
+
49
+ puts "\n=== Key Insights ==="
50
+ puts "✓ Command capture is unaffected (always 100 commands captured)"
51
+ puts "✓ Only logger output is sampled (reduces log volume)"
52
+ puts "✓ Tests can verify commands while production logs stay clean"
53
+ puts "✓ Deterministic sampling (every Nth command) ensures consistency"
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # examples/single_connection_transaction_confusions.rb
3
+ #
4
+ # frozen_string_literal: true
3
5
 
4
6
  # Redis Single Connection Mode Confusions
5
- #
6
7
  # This file demonstrates why mixing Redis operation modes on a single connection
7
8
  # causes subtle but critical failures in production applications.
8
9
  #
data/familia.gemspec CHANGED
@@ -17,9 +17,10 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
18
  spec.require_paths = ['lib']
19
19
 
20
- spec.required_ruby_version = Gem::Requirement.new('>= 3.4')
20
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.3.6')
21
21
 
22
22
  spec.add_dependency 'benchmark', '~> 0.4'
23
+ spec.add_dependency 'concurrent-ruby', '~> 1.3'
23
24
  spec.add_dependency 'connection_pool', '~> 2.5'
24
25
  spec.add_dependency 'csv', '~> 3.3'
25
26
  spec.add_dependency 'logger', '~> 1.7'
data/lib/familia/base.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # lib/familia/base.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  # A common module for Familia::DataType and Familia::Horreum to include.
@@ -0,0 +1,254 @@
1
+ # lib/familia/connection/behavior.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # lib/familia/connection/behavior.rb
6
+
7
+ module Familia
8
+ module Connection
9
+ # Shared connection behavior for both Horreum and DataType classes
10
+ #
11
+ # This module extracts common connection management functionality that was
12
+ # previously duplicated between Horreum::Connection and DataType::Connection.
13
+ # It provides:
14
+ #
15
+ # * URI normalization with logical_database support
16
+ # * Connection creation methods
17
+ # * Transaction and pipeline execution methods
18
+ # * Consistent connection API across object types
19
+ #
20
+ # Classes including this module must implement:
21
+ # * `dbclient(uri = nil)` - Connection resolution method
22
+ # * `build_connection_chain` (private) - Chain of Responsibility setup
23
+ #
24
+ # @example Basic usage in a class
25
+ # class MyDataStore
26
+ # include Familia::Connection::Behavior
27
+ #
28
+ # def dbclient(uri = nil)
29
+ # @connection_chain ||= build_connection_chain
30
+ # @connection_chain.handle(uri)
31
+ # end
32
+ #
33
+ # private
34
+ #
35
+ # def build_connection_chain
36
+ # # ... handler setup ...
37
+ # end
38
+ # end
39
+ #
40
+ module Behavior
41
+ def self.included(base)
42
+ base.class_eval do
43
+ attr_writer :dbclient
44
+ attr_reader :uri
45
+ end
46
+ end
47
+
48
+ # Normalizes various URI formats to a consistent URI object
49
+ #
50
+ # Handles multiple input types and considers the logical_database setting
51
+ # when uri is nil or Integer. This method is public so connection handlers
52
+ # can use it for consistent URI processing.
53
+ #
54
+ # @param uri [Integer, String, URI, nil] The URI to normalize
55
+ # @return [URI] Normalized URI object
56
+ # @raise [ArgumentError] If URI type is invalid
57
+ #
58
+ # @example Integer database number
59
+ # normalize_uri(2) # => URI with db=2 on default server
60
+ #
61
+ # @example String URI
62
+ # normalize_uri('redis://localhost:6379/1')
63
+ #
64
+ # @example nil with logical_database
65
+ # class MyModel
66
+ # include Familia::Connection::Behavior
67
+ # attr_accessor :logical_database
68
+ # end
69
+ # model = MyModel.new
70
+ # model.logical_database = 3
71
+ # model.normalize_uri(nil) # => URI with db=3
72
+ #
73
+ def normalize_uri(uri)
74
+ case uri
75
+ when Integer
76
+ new_uri = Familia.uri.dup
77
+ new_uri.db = uri
78
+ new_uri
79
+ when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
80
+ URI.parse(uri)
81
+ when URI
82
+ uri
83
+ when nil
84
+ # Use logical_database if available, otherwise fall back to Familia.uri
85
+ if respond_to?(:logical_database) && logical_database
86
+ new_uri = Familia.uri.dup
87
+ new_uri.db = logical_database
88
+ new_uri
89
+ else
90
+ Familia.uri
91
+ end
92
+ else
93
+ raise ArgumentError, "Invalid URI type: #{uri.class.name}"
94
+ end
95
+ end
96
+
97
+ # Creates a new Database connection instance
98
+ #
99
+ # This method always creates a fresh connection and does not use caching.
100
+ # Each call returns a new Redis client instance that you are responsible
101
+ # for managing and closing when done.
102
+ #
103
+ # @param uri [String, URI, Integer, nil] The URI of the Database server
104
+ # @return [Redis] A new Database client connection
105
+ #
106
+ # @example Creating a new connection
107
+ # client = create_dbclient('redis://localhost:6379/1')
108
+ # client.ping
109
+ # client.close
110
+ #
111
+ def create_dbclient(uri = nil)
112
+ parsed_uri = normalize_uri(uri)
113
+ Familia.create_dbclient(parsed_uri)
114
+ end
115
+
116
+ # Alias for create_dbclient (backward compatibility)
117
+ def connect(*)
118
+ create_dbclient(*)
119
+ end
120
+
121
+ # Sets the URI for this object's database connection
122
+ #
123
+ # @param uri [String, URI, Integer] The new URI
124
+ # @return [URI] The normalized URI
125
+ #
126
+ def uri=(uri)
127
+ @uri = normalize_uri(uri)
128
+ end
129
+
130
+ # Alias for uri (backward compatibility)
131
+ def url
132
+ uri
133
+ end
134
+
135
+ # Alias for uri= (backward compatibility)
136
+ def url=(uri)
137
+ self.uri = uri
138
+ end
139
+
140
+ # Executes a Redis transaction (MULTI/EXEC) using this object's connection context
141
+ #
142
+ # Provides atomic execution of multiple Redis commands with automatic connection
143
+ # management and operation mode enforcement. Uses the object's database and
144
+ # connection settings. Returns a MultiResult object for consistency.
145
+ #
146
+ # @yield [Redis] conn The Redis connection configured for transaction mode
147
+ # @return [MultiResult] Result object with success status and command results
148
+ #
149
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
150
+ #
151
+ # @example Basic transaction
152
+ # obj.transaction do |conn|
153
+ # conn.set('key1', 'value1')
154
+ # conn.set('key2', 'value2')
155
+ # conn.get('key1')
156
+ # end
157
+ #
158
+ # @example Reentrant behavior
159
+ # obj.transaction do |conn|
160
+ # conn.set('outer', 'value')
161
+ #
162
+ # # Nested transaction reuses same connection
163
+ # obj.transaction do |inner_conn|
164
+ # inner_conn.set('inner', 'value')
165
+ # end
166
+ # end
167
+ #
168
+ # @note Connection Inheritance:
169
+ # - Uses object's logical_database setting if configured
170
+ # - Inherits class-level database settings
171
+ # - Falls back to instance-level dbclient if set
172
+ # - Uses global connection chain as final fallback
173
+ #
174
+ # @note Transaction Context:
175
+ # - When called outside global transaction: Creates local MultiResult
176
+ # - When called inside global transaction: Yields to existing transaction
177
+ # - Maintains proper Fiber-local state for nested calls
178
+ #
179
+ # @see Familia.transaction For global transaction method
180
+ # @see MultiResult For details on the return value structure
181
+ #
182
+ def transaction(&)
183
+ ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
184
+ Familia::Connection::TransactionCore.execute_transaction(-> { dbclient }, &)
185
+ end
186
+
187
+ # Alias for transaction (alternate naming)
188
+ def multi(&)
189
+ transaction(&)
190
+ end
191
+
192
+ # Executes Redis commands in a pipeline using this object's connection context
193
+ #
194
+ # Batches multiple Redis commands together and sends them in a single network
195
+ # round-trip for improved performance. Uses the object's database and connection
196
+ # settings. Returns a MultiResult object for consistency.
197
+ #
198
+ # @yield [Redis] conn The Redis connection configured for pipelined mode
199
+ # @return [MultiResult] Result object with success status and command results
200
+ #
201
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
202
+ #
203
+ # @example Basic pipeline
204
+ # obj.pipelined do |conn|
205
+ # conn.set('key1', 'value1')
206
+ # conn.incr('counter')
207
+ # conn.get('key1')
208
+ # end
209
+ #
210
+ # @example Performance optimization
211
+ # # Instead of multiple round-trips:
212
+ # obj.save # Round-trip 1
213
+ # obj.increment_count # Round-trip 2
214
+ # obj.update_timestamp # Round-trip 3
215
+ #
216
+ # # Use pipeline for single round-trip:
217
+ # obj.pipelined do |conn|
218
+ # conn.hmset(obj.dbkey, obj.to_h)
219
+ # conn.hincrby(obj.dbkey, 'count', 1)
220
+ # conn.hset(obj.dbkey, 'updated_at', Time.now.to_i)
221
+ # end
222
+ #
223
+ # @note Connection Inheritance:
224
+ # - Uses object's logical_database setting if configured
225
+ # - Inherits class-level database settings
226
+ # - Falls back to instance-level dbclient if set
227
+ # - Uses global connection chain as final fallback
228
+ #
229
+ # @note Pipeline Context:
230
+ # - When called outside global pipeline: Creates local MultiResult
231
+ # - When called inside global pipeline: Yields to existing pipeline
232
+ # - Maintains proper Fiber-local state for nested calls
233
+ #
234
+ # @note Performance Considerations:
235
+ # - Best for multiple independent operations
236
+ # - Reduces network latency by batching commands
237
+ # - Commands execute independently (some may succeed, others fail)
238
+ #
239
+ # @see Familia.pipelined For global pipeline method
240
+ # @see MultiResult For details on the return value structure
241
+ # @see #transaction For atomic command execution
242
+ #
243
+ def pipelined(&block)
244
+ ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
245
+ Familia::Connection::PipelineCore.execute_pipeline(-> { dbclient }, &block)
246
+ end
247
+
248
+ # Alias for pipelined (alternate naming)
249
+ def pipeline(&block)
250
+ pipelined(&block)
251
+ end
252
+ end
253
+ end
254
+ end