familia 2.0.0.pre18 → 2.0.0.pre21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.rst +205 -88
  8. data/CLAUDE.md +62 -10
  9. data/Gemfile +3 -3
  10. data/Gemfile.lock +27 -62
  11. data/README.md +39 -0
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
  15. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
  16. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
  17. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
  18. data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
  19. data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
  20. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  21. data/docs/guides/encryption.md +486 -0
  22. data/docs/guides/feature-encrypted-fields.md +123 -7
  23. data/docs/guides/feature-expiration.md +177 -133
  24. data/docs/guides/feature-external-identifiers.md +415 -443
  25. data/docs/guides/feature-object-identifiers.md +400 -269
  26. data/docs/guides/feature-quantization.md +120 -6
  27. data/docs/guides/feature-relationships-indexing.md +318 -0
  28. data/docs/guides/feature-relationships-methods.md +146 -604
  29. data/docs/guides/feature-relationships-participation.md +263 -0
  30. data/docs/guides/feature-relationships.md +118 -136
  31. data/docs/guides/feature-system-devs.md +176 -693
  32. data/docs/guides/feature-system.md +119 -6
  33. data/docs/guides/feature-transient-fields.md +81 -0
  34. data/docs/guides/field-system.md +778 -0
  35. data/docs/guides/index.md +32 -15
  36. data/docs/guides/logging.md +187 -0
  37. data/docs/guides/optimized-loading.md +674 -0
  38. data/docs/guides/thread-safety-monitoring.md +61 -0
  39. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  40. data/docs/migrating/v2.0.0-pre19.md +197 -0
  41. data/docs/migrating/v2.0.0-pre22.md +241 -0
  42. data/docs/overview.md +7 -9
  43. data/docs/reference/api-technical.md +267 -320
  44. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  45. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  46. data/examples/autoloader/mega_customer.rb +2 -0
  47. data/examples/datatype_standalone.rb +282 -0
  48. data/examples/encrypted_fields.rb +2 -1
  49. data/examples/json_usage_patterns.rb +2 -0
  50. data/examples/relationships.rb +3 -0
  51. data/examples/safe_dump.rb +2 -1
  52. data/examples/sampling_demo.rb +53 -0
  53. data/examples/single_connection_transaction_confusions.rb +2 -1
  54. data/familia.gemspec +2 -1
  55. data/lib/familia/base.rb +2 -0
  56. data/lib/familia/connection/behavior.rb +254 -0
  57. data/lib/familia/connection/handlers.rb +97 -0
  58. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  59. data/lib/familia/connection/middleware.rb +34 -24
  60. data/lib/familia/connection/operation_core.rb +3 -1
  61. data/lib/familia/connection/operations.rb +2 -0
  62. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
  63. data/lib/familia/connection/transaction_core.rb +75 -9
  64. data/lib/familia/connection.rb +21 -5
  65. data/lib/familia/data_type/class_methods.rb +3 -1
  66. data/lib/familia/data_type/connection.rb +153 -7
  67. data/lib/familia/data_type/database_commands.rb +9 -4
  68. data/lib/familia/data_type/serialization.rb +10 -4
  69. data/lib/familia/data_type/settings.rb +2 -0
  70. data/lib/familia/data_type/types/counter.rb +2 -0
  71. data/lib/familia/data_type/types/hashkey.rb +8 -6
  72. data/lib/familia/data_type/types/listkey.rb +2 -0
  73. data/lib/familia/data_type/types/lock.rb +2 -0
  74. data/lib/familia/data_type/types/sorted_set.rb +2 -0
  75. data/lib/familia/data_type/types/stringkey.rb +2 -0
  76. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  77. data/lib/familia/data_type.rb +2 -0
  78. data/lib/familia/encryption/encrypted_data.rb +4 -2
  79. data/lib/familia/encryption/manager.rb +2 -0
  80. data/lib/familia/encryption/provider.rb +2 -0
  81. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  82. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  83. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  84. data/lib/familia/encryption/registry.rb +2 -0
  85. data/lib/familia/encryption/request_cache.rb +2 -0
  86. data/lib/familia/encryption.rb +9 -2
  87. data/lib/familia/errors.rb +53 -14
  88. data/lib/familia/features/autoloader.rb +2 -0
  89. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  90. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  91. data/lib/familia/features/encrypted_fields.rb +2 -2
  92. data/lib/familia/features/expiration/extensions.rb +11 -11
  93. data/lib/familia/features/expiration.rb +29 -21
  94. data/lib/familia/features/external_identifier.rb +33 -7
  95. data/lib/familia/features/object_identifier.rb +2 -0
  96. data/lib/familia/features/quantization.rb +3 -1
  97. data/lib/familia/features/relationships/README.md +3 -1
  98. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  99. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
  100. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  101. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
  102. data/lib/familia/features/relationships/indexing.rb +40 -42
  103. data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
  104. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  105. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  106. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  107. data/lib/familia/features/relationships/participation.rb +155 -69
  108. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  109. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  110. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  111. data/lib/familia/features/relationships.rb +5 -3
  112. data/lib/familia/features/safe_dump.rb +2 -0
  113. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  114. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  115. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  116. data/lib/familia/features/transient_fields.rb +2 -0
  117. data/lib/familia/features.rb +2 -0
  118. data/lib/familia/field_type.rb +5 -2
  119. data/lib/familia/horreum/connection.rb +28 -36
  120. data/lib/familia/horreum/database_commands.rb +131 -10
  121. data/lib/familia/horreum/definition.rb +18 -7
  122. data/lib/familia/horreum/management.rb +233 -57
  123. data/lib/familia/horreum/persistence.rb +314 -122
  124. data/lib/familia/horreum/related_fields.rb +2 -0
  125. data/lib/familia/horreum/serialization.rb +26 -4
  126. data/lib/familia/horreum/settings.rb +2 -0
  127. data/lib/familia/horreum/utils.rb +2 -8
  128. data/lib/familia/horreum.rb +46 -13
  129. data/lib/familia/identifier_extractor.rb +2 -0
  130. data/lib/familia/instrumentation.rb +156 -0
  131. data/lib/familia/json_serializer.rb +2 -0
  132. data/lib/familia/logging.rb +94 -37
  133. data/lib/familia/refinements/dear_json.rb +2 -0
  134. data/lib/familia/refinements/stylize_words.rb +2 -14
  135. data/lib/familia/refinements/time_literals.rb +2 -0
  136. data/lib/familia/refinements.rb +2 -0
  137. data/lib/familia/secure_identifier.rb +10 -2
  138. data/lib/familia/settings.rb +9 -7
  139. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  140. data/lib/familia/thread_safety/monitor.rb +328 -0
  141. data/lib/familia/utils.rb +13 -0
  142. data/lib/familia/verifiable_identifier.rb +3 -1
  143. data/lib/familia/version.rb +3 -1
  144. data/lib/familia.rb +31 -4
  145. data/lib/middleware/database_command_counter.rb +152 -0
  146. data/lib/middleware/database_logger.rb +325 -129
  147. data/lib/multi_result.rb +2 -0
  148. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  149. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  150. data/try/edge_cases/json_serialization_try.rb +2 -0
  151. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  152. data/try/edge_cases/race_conditions_try.rb +4 -0
  153. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  154. data/try/edge_cases/string_coercion_try.rb +6 -4
  155. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  156. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  157. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  158. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  159. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  160. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  161. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  162. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  164. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  165. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  166. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  167. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  168. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  169. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  170. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  171. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  172. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  173. data/try/features/encryption/config_persistence_try.rb +4 -0
  174. data/try/features/encryption/core_try.rb +4 -0
  175. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  176. data/try/features/encryption/module_loading_try.rb +4 -0
  177. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  178. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  179. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  180. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  181. data/try/features/expiration/expiration_try.rb +5 -1
  182. data/try/features/external_identifier/external_identifier_try.rb +171 -8
  183. data/try/features/feature_dependencies_try.rb +2 -0
  184. data/try/features/feature_improvements_try.rb +2 -0
  185. data/try/features/field_groups_try.rb +2 -0
  186. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  187. data/try/features/object_identifier/object_identifier_try.rb +2 -0
  188. data/try/features/quantization/quantization_try.rb +4 -0
  189. data/try/features/real_feature_integration_try.rb +2 -0
  190. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/indexing_rebuild_try.rb +600 -0
  192. data/try/features/relationships/indexing_try.rb +30 -4
  193. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  194. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  195. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  196. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  197. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  198. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  199. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  200. data/try/features/relationships/relationships_api_changes_try.rb +6 -4
  201. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  202. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  203. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  204. data/try/features/relationships/relationships_performance_try.rb +4 -0
  205. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  206. data/try/features/relationships/relationships_try.rb +6 -4
  207. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  208. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  209. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  210. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  211. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  212. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  213. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  214. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  215. data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
  216. data/try/integration/connection/handler_constraints_try.rb +4 -0
  217. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  218. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  219. data/try/integration/connection/operation_mode_guards_try.rb +5 -1
  220. data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
  221. data/try/integration/connection/pools_try.rb +4 -0
  222. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  223. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  224. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  225. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  226. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  227. data/try/integration/connection/transaction_modes_try.rb +4 -0
  228. data/try/integration/conventional_inheritance_try.rb +4 -0
  229. data/try/integration/create_method_try.rb +26 -22
  230. data/try/integration/cross_component_try.rb +4 -0
  231. data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
  232. data/try/integration/data_types/datatype_transactions_try.rb +251 -0
  233. data/try/integration/database_consistency_try.rb +4 -0
  234. data/try/integration/familia_extended_try.rb +4 -0
  235. data/try/integration/familia_members_methods_try.rb +4 -0
  236. data/try/integration/models/customer_safe_dump_try.rb +9 -1
  237. data/try/integration/models/customer_try.rb +4 -0
  238. data/try/integration/models/datatype_base_try.rb +4 -0
  239. data/try/integration/models/familia_object_try.rb +5 -1
  240. data/try/integration/persistence_operations_try.rb +166 -10
  241. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  242. data/try/integration/save_methods_consistency_try.rb +241 -0
  243. data/try/integration/scenarios_try.rb +4 -0
  244. data/try/integration/secure_identifier_try.rb +4 -0
  245. data/try/integration/transaction_safety_core_try.rb +176 -0
  246. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  247. data/try/integration/verifiable_identifier_try.rb +4 -0
  248. data/try/investigation/pipeline_routing/README.md +228 -0
  249. data/try/performance/benchmarks_try.rb +4 -0
  250. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  251. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  252. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  253. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  254. data/try/support/debugging/debug_aad_process.rb +3 -0
  255. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  256. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  257. data/try/support/debugging/debug_context_aad.rb +3 -0
  258. data/try/support/debugging/debug_context_simple.rb +3 -0
  259. data/try/support/debugging/debug_cross_context.rb +3 -0
  260. data/try/support/debugging/debug_database_load.rb +3 -0
  261. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  262. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  263. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  264. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  265. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  266. data/try/support/debugging/debug_load_path.rb +3 -0
  267. data/try/support/debugging/debug_method_definition.rb +3 -0
  268. data/try/support/debugging/debug_method_resolution.rb +3 -0
  269. data/try/support/debugging/debug_minimal.rb +3 -0
  270. data/try/support/debugging/debug_provider.rb +3 -0
  271. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  272. data/try/support/debugging/debug_string_class.rb +3 -0
  273. data/try/support/debugging/debug_test.rb +3 -0
  274. data/try/support/debugging/debug_test_design.rb +3 -0
  275. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  276. data/try/support/debugging/provider_diagnostics.rb +4 -0
  277. data/try/support/helpers/test_cleanup.rb +4 -0
  278. data/try/support/helpers/test_helpers.rb +5 -0
  279. data/try/support/memory/memory_basic_test.rb +4 -0
  280. data/try/support/memory/memory_detailed_test.rb +4 -0
  281. data/try/support/memory/memory_search_for_string.rb +4 -0
  282. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  283. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  284. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  285. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  286. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  287. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  288. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  289. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  290. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  291. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  292. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  293. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  294. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  295. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  296. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  297. data/try/thread_safety/README.md +496 -0
  298. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  299. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  300. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  301. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  302. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  303. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  304. data/try/thread_safety/field_registration_race_try.rb +222 -0
  305. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  306. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  307. data/try/thread_safety/module_config_race_try.rb +175 -0
  308. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  309. data/try/unit/core/autoloader_try.rb +4 -0
  310. data/try/unit/core/base_enhancements_try.rb +4 -0
  311. data/try/unit/core/connection_try.rb +4 -0
  312. data/try/unit/core/errors_try.rb +4 -0
  313. data/try/unit/core/extensions_try.rb +4 -0
  314. data/try/unit/core/familia_logger_try.rb +2 -0
  315. data/try/unit/core/familia_try.rb +4 -0
  316. data/try/unit/core/middleware_sampling_try.rb +335 -0
  317. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  318. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  319. data/try/unit/core/middleware_try.rb +4 -0
  320. data/try/unit/core/settings_try.rb +4 -0
  321. data/try/unit/core/time_utils_try.rb +4 -0
  322. data/try/unit/core/tools_try.rb +4 -0
  323. data/try/unit/core/utils_try.rb +37 -0
  324. data/try/unit/data_types/boolean_try.rb +5 -1
  325. data/try/unit/data_types/counter_try.rb +4 -0
  326. data/try/unit/data_types/datatype_base_try.rb +4 -0
  327. data/try/unit/data_types/hash_try.rb +4 -0
  328. data/try/unit/data_types/list_try.rb +4 -0
  329. data/try/unit/data_types/lock_try.rb +4 -0
  330. data/try/unit/data_types/sorted_set_try.rb +4 -0
  331. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  332. data/try/unit/data_types/string_try.rb +5 -1
  333. data/try/unit/data_types/unsortedset_try.rb +4 -0
  334. data/try/unit/familia_resolve_class_try.rb +116 -0
  335. data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
  336. data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
  337. data/try/unit/horreum/base_try.rb +5 -1
  338. data/try/unit/horreum/class_methods_try.rb +6 -2
  339. data/try/unit/horreum/commands_try.rb +4 -0
  340. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  341. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
  342. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  343. data/try/unit/horreum/field_categories_try.rb +4 -0
  344. data/try/unit/horreum/field_definition_try.rb +4 -0
  345. data/try/unit/horreum/initialization_try.rb +5 -1
  346. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  347. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  348. data/try/unit/horreum/relations_try.rb +8 -4
  349. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  350. data/try/unit/horreum/serialization_try.rb +6 -2
  351. data/try/unit/horreum/settings_try.rb +4 -0
  352. data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
  353. data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
  354. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  355. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  356. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  357. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  358. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  359. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  360. data/try/unit/thread_safety_monitor_try.rb +149 -0
  361. metadata +81 -14
  362. data/.github/workflows/code-quality.yml +0 -138
  363. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  364. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  365. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  366. data/docs/archive/README.md +0 -64
  367. data/docs/archive/api-reference.md +0 -333
  368. data/docs/guides/core-field-system.md +0 -806
  369. data/docs/guides/implementation.md +0 -276
  370. data/docs/guides/security-model.md +0 -183
@@ -0,0 +1,69 @@
1
+ # lib/familia/features/relationships/participation_membership.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ module Familia
6
+ module Features
7
+ module Relationships
8
+ #
9
+ # ParticipationMembership
10
+ #
11
+ # Represents runtime snapshot of a participant's membership in a target collection.
12
+ # Returned by current_participations to provide a type-safe, structured view
13
+ # of actual participation state.
14
+ #
15
+ # @note This represents what currently exists in Redis, not just configuration.
16
+ # See ParticipationRelationship for static configuration metadata.
17
+ #
18
+ # @example
19
+ # membership = user.current_participations.first
20
+ # membership.target_class # => "Team"
21
+ # membership.target_id # => "team123"
22
+ # membership.collection_name # => :members
23
+ # membership.type # => :sorted_set
24
+ # membership.score # => 1762554020.05
25
+ #
26
+ ParticipationMembership = Data.define(
27
+ :target_class, # String - class name (e.g., "Customer")
28
+ :target_id, # String - target instance identifier
29
+ :collection_name, # Symbol - collection name (e.g., :domains)
30
+ :type, # Symbol - collection type (:sorted_set, :set, :list)
31
+ :score, # Float - optional, for sorted_set only
32
+ :decoded_score, # Hash - optional, decoded score data
33
+ :position # Integer - optional, for list only
34
+ ) do
35
+ # Check if this membership is a sorted set
36
+ # @return [Boolean]
37
+ def sorted_set?
38
+ type == :sorted_set
39
+ end
40
+
41
+ # Check if this membership is a set
42
+ # @return [Boolean]
43
+ def set?
44
+ type == :set
45
+ end
46
+
47
+ # Check if this membership is a list
48
+ # @return [Boolean]
49
+ def list?
50
+ type == :list
51
+ end
52
+
53
+ # Get the target instance (requires loading from database)
54
+ # @return [Familia::Horreum, nil] the loaded target instance
55
+ def target_instance
56
+ return nil unless target_class
57
+
58
+ # Resolve class from string name
59
+ # Only rescue NameError (class doesn't exist), not all exceptions
60
+ klass = Object.const_get(target_class)
61
+ klass.find_by_id(target_id)
62
+ rescue NameError
63
+ # Target class doesn't exist or isn't loaded
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/relationships/participation_relationship.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Features
@@ -10,20 +12,46 @@ module Familia
10
12
  # Used to configure code generation and runtime behavior for participates_in
11
13
  # and class_participates_in declarations.
12
14
  #
15
+ # @note target_class is resolved once at definition time for performance.
16
+ # Use _original_target for debugging/introspection to see what was passed.
17
+ #
13
18
  ParticipationRelationship = Data.define(
14
- :target_class, # Class object that owns the collection
19
+ :_original_target, # Original Symbol/String/Class as passed to participates_in
20
+ :target_class, # Resolved Class object (e.g., User class, not :User symbol)
15
21
  :collection_name, # Symbol name of the collection (e.g., :members, :domains)
16
22
  :score, # Proc/Symbol/nil - score calculator for sorted sets
17
23
  :type, # Symbol - collection type (:sorted_set, :set, :list)
18
- :bidirectional, # Boolean - whether to generate reverse methods
24
+ :bidirectional, # Boolean/Symbol - whether to generate reverse methods
19
25
  ) do
26
+ # Get a unique key for this participation relationship
27
+ # Useful for comparisons and hash keys
20
28
  #
21
- # Get the normalized config name for the target class
29
+ # @return [String] unique identifier in format "TargetClass:collection_name"
30
+ def unique_key
31
+ Familia.join(target_class_base, collection_name)
32
+ end
33
+
34
+ # Get the base class name without namespace
35
+ # Handles anonymous class wrappers like "#<Class:0x123>::SymbolResolutionCustomer"
22
36
  #
23
- # @return [String] The config name (e.g., "user", "perf_test_customer")
37
+ # @return [String] base class name (e.g., "Customer")
38
+ def target_class_base
39
+ target_class.name.split('::').last
40
+ end
41
+
42
+ # Check if this relationship matches the given target and collection
43
+ # Handles namespace-agnostic class comparison
24
44
  #
25
- def target_class_config_name
26
- target_class.config_name
45
+ # @param comparison_target [Class, String, Symbol] target to compare against
46
+ # @param comparison_collection [Symbol, String] collection name to compare
47
+ # @return [Boolean] true if both target and collection match
48
+ def matches?(comparison_target, comparison_collection)
49
+ # Normalize comparison target to base class name
50
+ comparison_target = comparison_target.name if comparison_target.is_a?(Class)
51
+ comparison_target_base = comparison_target.to_s.split('::').last
52
+
53
+ target_class_base == comparison_target_base &&
54
+ collection_name == comparison_collection.to_sym
27
55
  end
28
56
  end
29
57
  end
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/relationships/score_encoding.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Features
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/relationships.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require 'securerandom'
4
6
  require_relative 'relationships/score_encoding'
@@ -84,7 +86,7 @@ module Familia
84
86
 
85
87
  # Feature initialization
86
88
  def self.included(base)
87
- Familia.ld "[#{base}] Relationships included"
89
+ Familia.debug "[#{base}] Relationships included"
88
90
  base.extend ModelClassMethods
89
91
  base.include ModelInstanceMethods
90
92
 
@@ -157,7 +159,7 @@ module Familia
157
159
  def create_temp_key(base_name, ttl = 300)
158
160
  timestamp = Familia.now.to_i
159
161
  random_suffix = SecureRandom.hex(3)
160
- temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
162
+ temp_key = Familia.join('temp', base_name, timestamp, random_suffix)
161
163
 
162
164
  # UnsortedSet immediate expiry to ensure cleanup even if operation fails
163
165
  if respond_to?(:dbclient)
@@ -256,7 +258,7 @@ module Familia
256
258
  def create_temp_key(base_name, ttl = 300)
257
259
  timestamp = Familia.now.to_i
258
260
  random_suffix = SecureRandom.hex(3)
259
- temp_key = "temp:#{base_name}:#{timestamp}:#{random_suffix}"
261
+ temp_key = Familia.join('temp', base_name, timestamp, random_suffix)
260
262
 
261
263
  # UnsortedSet immediate expiry to ensure cleanup even if operation fails
262
264
  dbclient.expire(temp_key, ttl)
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/safe_dump.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  #
4
6
  # Class instance variables are used here for configuration. These are set
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/transient_fields/redacted_string.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # RedactedString
4
6
  #
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/transient_fields/single_use_redacted_string.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'redacted_string'
4
6
 
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/transient_fields/transient_field_type.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require 'familia/field_type'
4
6
 
@@ -80,7 +82,7 @@ module Familia
80
82
  #
81
83
  def define_fast_writer(_klass)
82
84
  # No fast writer for transient fields since they're not persisted
83
- Familia.ld "[TransientFieldType] Skipping fast writer for transient field: #{@name}"
85
+ Familia.debug "[TransientFieldType] Skipping fast writer for transient field: #{@name}"
84
86
  nil
85
87
  end
86
88
 
@@ -117,7 +119,7 @@ module Familia
117
119
  #
118
120
  def serialize(_value, _record = nil)
119
121
  # Transient fields should never be serialized
120
- Familia.ld "[TransientFieldType] WARNING: serialize called on transient field #{@name}"
122
+ Familia.debug "[TransientFieldType] WARNING: serialize called on transient field #{@name}"
121
123
  nil
122
124
  end
123
125
 
@@ -132,7 +134,7 @@ module Familia
132
134
  #
133
135
  def deserialize(_value, _record = nil)
134
136
  # Transient fields should never be deserialized
135
- Familia.ld "[TransientFieldType] WARNING: deserialize called on transient field #{@name}"
137
+ Familia.debug "[TransientFieldType] WARNING: deserialize called on transient field #{@name}"
136
138
  nil
137
139
  end
138
140
  end
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/transient_fields.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'transient_fields/redacted_string'
4
6
  require_relative 'transient_fields/transient_field_type'
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # Load the Autoloader first, then use it to load all other features
4
6
  require_relative 'features/autoloader'
@@ -1,4 +1,6 @@
1
1
  # lib/familia/field_type.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  # Base class for all field types in Familia
@@ -143,7 +145,8 @@ module Familia
143
145
  val = args.first
144
146
 
145
147
  # If no value provided, return current stored value
146
- return hget(field_name) if val.nil?
148
+ # Handle Redis::Future objects during transactions
149
+ return hget(field_name) if val.nil? || val.is_a?(Redis::Future)
147
150
 
148
151
  begin
149
152
  # Trace the operation if debugging is enabled
@@ -151,7 +154,7 @@ module Familia
151
154
 
152
155
  # Convert value for database storage
153
156
  prepared = serialize_value(val)
154
- Familia.ld "[FieldType#define_fast_writer] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
157
+ Familia.debug "[FieldType#define_fast_writer] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
155
158
 
156
159
  # Use the setter method to update instance variable
157
160
  send(:"#{method_name}=", val) if method_name
@@ -1,4 +1,6 @@
1
1
  # lib/familia/horreum/connection.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class Horreum
@@ -6,40 +8,16 @@ module Familia
6
8
  # Provides connection handling, transactions, and URI normalization for both
7
9
  # class-level operations (e.g., Customer.dbclient) and instance-level operations
8
10
  # (e.g., customer.dbclient)
11
+ #
12
+ # Includes shared connection behavior from Familia::Connection::Behavior, providing:
13
+ # - URI normalization (normalize_uri)
14
+ # - Connection creation (create_dbclient)
15
+ # - Transaction method signatures
16
+ # - Pipeline method signatures
9
17
  module Connection
10
- attr_reader :uri
11
-
12
- # Normalizes various URI formats to a consistent URI object
13
- # Considers the class/instance logical_database when uri is nil or Integer
14
- def normalize_uri(uri)
15
- case uri
16
- when Integer
17
- new_uri = Familia.uri.dup
18
- new_uri.db = uri
19
- new_uri
20
- when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
21
- URI.parse(uri)
22
- when URI
23
- uri
24
- when nil
25
- # Use logical_database if available, otherwise fall back to Familia.uri
26
- if respond_to?(:logical_database) && logical_database
27
- new_uri = Familia.uri.dup
28
- new_uri.db = logical_database
29
- new_uri
30
- else
31
- Familia.uri
32
- end
33
- else
34
- raise ArgumentError, "Invalid URI type: #{uri.class.name}"
35
- end
36
- end
18
+ include Familia::Connection::Behavior
37
19
 
38
- # Creates a new Database connection instance using the class/instance configuration
39
- def create_dbclient(uri = nil)
40
- parsed_uri = normalize_uri(uri)
41
- Familia.create_dbclient(parsed_uri)
42
- end
20
+ attr_reader :uri
43
21
 
44
22
  # Returns the Database connection for the class using Chain of Responsibility pattern.
45
23
  #
@@ -48,10 +26,19 @@ module Familia
48
26
  # 2. DefaultConnectionHandler - Horreum model class-level @dbclient
49
27
  # 3. GlobalFallbackHandler - Familia.dbclient(uri || logical_database) (global fallback)
50
28
  #
29
+ # Thread-safe lazy initialization using double-checked locking to ensure
30
+ # only a single connection chain is built even under high concurrent load.
31
+ #
51
32
  # @return [Redis] the Database connection instance.
52
33
  #
53
34
  def dbclient(uri = nil)
54
- @class_connection_chain ||= build_connection_chain
35
+ # Fast path: return existing chain if already initialized
36
+ return @class_connection_chain.handle(uri) if @class_connection_chain
37
+
38
+ # Slow path: thread-safe initialization
39
+ @class_connection_chain_mutex.synchronize do
40
+ @class_connection_chain ||= build_connection_chain
41
+ end
55
42
  @class_connection_chain.handle(uri)
56
43
  end
57
44
 
@@ -277,9 +264,9 @@ module Familia
277
264
  @provider_connection_handler ||= Familia::Connection::ProviderConnectionHandler.new
278
265
 
279
266
  # Determine the appropriate class context
280
- # When called from instance: self is instance, self.class is the model class
281
- # When called from class: self is the model class
282
- klass = self.is_a?(Class) ? self : self.class
267
+ # When called from instance: self is instance, use the model class connection
268
+ # When called from class: we'll use our own connection
269
+ klass = is_a?(Class) ? self : self.class
283
270
 
284
271
  # Always check class first for @dbclient since instance-level connections were removed
285
272
  @cached_connection_handler ||= Familia::Connection::CachedConnectionHandler.new(klass)
@@ -292,6 +279,11 @@ module Familia
292
279
  .add_handler(@cached_connection_handler)
293
280
  .add_handler(@create_connection_handler)
294
281
  end
282
+
283
+ # Thread-safe mutex initialization when module is extended
284
+ def self.extended(base)
285
+ base.instance_variable_set(:@class_connection_chain_mutex, Mutex.new)
286
+ end
295
287
  end
296
288
  end
297
289
  end
@@ -1,4 +1,6 @@
1
1
  # lib/familia/horreum/database_commands.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  # Familia::Horreum
@@ -16,6 +18,10 @@ module Familia
16
18
  # just load the object again.
17
19
  #
18
20
  module DatabaseCommands
21
+ # Moves the object's key to a different logical database.
22
+ #
23
+ # @param logical_database [Integer] The target database number
24
+ # @return [Boolean] true if the key was moved successfully
19
25
  def move(logical_database)
20
26
  dbclient.move dbkey, logical_database
21
27
  end
@@ -40,7 +46,18 @@ module Familia
40
46
  key_exists = self.class.exists?(identifier)
41
47
  return key_exists unless check_size
42
48
 
43
- key_exists && !size.zero?
49
+ # Handle Redis::Future in transactions - skip size check
50
+ if key_exists.is_a?(Redis::Future)
51
+ return key_exists
52
+ end
53
+
54
+ current_size = size
55
+ # Handle Redis::Future from size call too
56
+ if current_size.is_a?(Redis::Future)
57
+ return current_size
58
+ end
59
+
60
+ key_exists && !current_size.zero?
44
61
  end
45
62
 
46
63
  # Returns the number of fields in the main object hash
@@ -55,6 +72,8 @@ module Familia
55
72
  # automatically be deleted. Returns 1 if the timeout was set, 0 if key
56
73
  # does not exist or the timeout could not be set.
57
74
  #
75
+ # @param default_expiration [Integer] TTL in seconds (uses class default if nil)
76
+ # @return [Integer] 1 if timeout was set, 0 otherwise
58
77
  def expire(default_expiration = nil)
59
78
  default_expiration ||= self.class.default_expiration
60
79
  Familia.trace :EXPIRE, nil, default_expiration if Familia.debug?
@@ -69,7 +88,7 @@ module Familia
69
88
  # @return [Integer] The TTL of the key in seconds. Returns -1 if the key does not exist
70
89
  # or has no associated expire time.
71
90
  def current_expiration
72
- Familia.trace :CURRENT_EXPIRATION, nil, uri if Familia.debug?
91
+ Familia.trace :CURRENT_EXPIRATION, nil, self.class.uri if Familia.debug?
73
92
  dbclient.ttl dbkey
74
93
  end
75
94
 
@@ -83,24 +102,38 @@ module Familia
83
102
  end
84
103
  alias remove remove_field # deprecated
85
104
 
105
+ # Returns the Redis data type of the key.
106
+ #
107
+ # @return [String] The data type (e.g., 'hash', 'string', 'list')
86
108
  def data_type
87
- Familia.trace :DATATYPE, nil, uri if Familia.debug?
109
+ Familia.trace :DATATYPE, nil, self.class.uri if Familia.debug?
88
110
  dbclient.type dbkey(suffix)
89
111
  end
90
112
 
91
- # For parity with DataType#hgetall
113
+ # Returns all fields and values in the hash.
114
+ #
115
+ # @return [Hash] All field-value pairs in the hash
116
+ # @note For parity with DataType#hgetall
92
117
  def hgetall
93
- Familia.trace :HGETALL, nil, uri if Familia.debug?
118
+ Familia.trace :HGETALL, nil, self.class.uri if Familia.debug?
94
119
  dbclient.hgetall dbkey(suffix)
95
120
  end
96
121
  alias all hgetall
97
122
 
123
+ # Gets the value of a hash field.
124
+ #
125
+ # @param field [String] The field name
126
+ # @return [String, nil] The value of the field, or nil if field doesn't exist
98
127
  def hget(field)
99
128
  Familia.trace :HGET, nil, field if Familia.debug?
100
129
  dbclient.hget dbkey(suffix), field
101
130
  end
102
131
 
103
- # @return The number of fields that were added to the hash. If the
132
+ # Sets the value of a hash field.
133
+ #
134
+ # @param field [String] The field name
135
+ # @param value [String] The value to set
136
+ # @return [Integer] The number of fields that were added to the hash. If the
104
137
  # field already exists, this will return 0.
105
138
  def hset(field, value)
106
139
  Familia.trace :HSET, nil, field if Familia.debug?
@@ -120,51 +153,92 @@ module Familia
120
153
  dbclient.hsetnx dbkey, field, value
121
154
  end
122
155
 
156
+ # Sets multiple hash fields to multiple values.
157
+ #
158
+ # @param hsh [Hash] Hash of field-value pairs to set
159
+ # @return [String] 'OK' on success
123
160
  def hmset(hsh = {})
124
161
  hsh ||= to_h_for_storage
125
162
  Familia.trace :HMSET, nil, hsh if Familia.debug?
126
163
  dbclient.hmset dbkey(suffix), hsh
127
164
  end
128
165
 
166
+ # Returns all field names in the hash.
167
+ #
168
+ # @return [Array<String>] Array of field names
129
169
  def hkeys
130
- Familia.trace :HKEYS, nil, 'uri' if Familia.debug?
170
+ Familia.trace :HKEYS, nil, self.class.uri if Familia.debug?
131
171
  dbclient.hkeys dbkey(suffix)
132
172
  end
133
173
 
174
+ # Returns all values in the hash.
175
+ #
176
+ # @return [Array<String>] Array of values
134
177
  def hvals
135
178
  dbclient.hvals dbkey(suffix)
136
179
  end
137
180
 
181
+ # Increments the integer value of a hash field by 1.
182
+ #
183
+ # @param field [String] The field name
184
+ # @return [Integer] The value after incrementing
138
185
  def incr(field)
139
186
  dbclient.hincrby dbkey(suffix), field, 1
140
187
  end
141
188
  alias increment incr
142
189
 
190
+ # Increments the integer value of a hash field by the given amount.
191
+ #
192
+ # @param field [String] The field name
193
+ # @param increment [Integer] The increment value
194
+ # @return [Integer] The value after incrementing
143
195
  def incrby(field, increment)
144
196
  dbclient.hincrby dbkey(suffix), field, increment
145
197
  end
146
198
  alias incrementby incrby
147
199
 
200
+ # Increments the float value of a hash field by the given amount.
201
+ #
202
+ # @param field [String] The field name
203
+ # @param increment [Float] The increment value
204
+ # @return [Float] The value after incrementing
148
205
  def incrbyfloat(field, increment)
149
206
  dbclient.hincrbyfloat dbkey(suffix), field, increment
150
207
  end
151
208
  alias incrementbyfloat incrbyfloat
152
209
 
210
+ # Decrements the integer value of a hash field by the given amount.
211
+ #
212
+ # @param field [String] The field name
213
+ # @param decrement [Integer] The decrement value
214
+ # @return [Integer] The value after decrementing
153
215
  def decrby(field, decrement)
154
216
  dbclient.decrby dbkey(suffix), field, decrement
155
217
  end
156
218
  alias decrementby decrby
157
219
 
220
+ # Decrements the integer value of a hash field by 1.
221
+ #
222
+ # @param field [String] The field name
223
+ # @return [Integer] The value after decrementing
158
224
  def decr(field)
159
225
  dbclient.hdecr field
160
226
  end
161
227
  alias decrement decr
162
228
 
229
+ # Returns the string length of the value associated with field in the hash.
230
+ #
231
+ # @param field [String] The field name
232
+ # @return [Integer] The string length of the field value, or 0 if field doesn't exist
163
233
  def hstrlen(field)
164
234
  dbclient.hstrlen dbkey(suffix), field
165
235
  end
166
236
  alias hstrlength hstrlen
167
237
 
238
+ # Determines if a hash field exists.
239
+ #
240
+ # @param field [String] The field name
241
+ # @return [Boolean] true if the field exists, false otherwise
168
242
  def key?(field)
169
243
  dbclient.hexists dbkey(suffix), field
170
244
  end
@@ -176,14 +250,61 @@ module Familia
176
250
  #
177
251
  # @return [Boolean] true if the key was deleted, false otherwise
178
252
  def delete!
179
- Familia.trace :DELETE!, nil, uri if Familia.debug?
253
+ Familia.trace :DELETE!, nil, self.class.uri if Familia.debug?
180
254
 
181
255
  # Delete the main object key
182
- ret = dbclient.del dbkey
183
- ret.positive?
256
+ dbclient.del dbkey
184
257
  end
185
258
  alias clear delete!
186
259
 
260
+ # Watches the key for changes during a MULTI/EXEC transaction.
261
+ #
262
+ # Decision Matrix:
263
+ #
264
+ # | Scenario | Use | Why |
265
+ # |----------|-----|-----|
266
+ # | Check if exists, then create | WATCH | Must prevent duplicate creation |
267
+ # | Read value, update conditionally | WATCH | Decision depends on current state |
268
+ # | Compare-and-swap operations | WATCH | Need optimistic locking |
269
+ # | Version-based updates | WATCH | Must detect concurrent changes |
270
+ # | Batch field updates | MULTI only | No conditional logic |
271
+ # | Increment + timestamp together | MULTI only | Concurrent increments OK |
272
+ # | Save object atomically | MULTI only | Just need atomicity |
273
+ # | Update indexes with save | MULTI only | No state checking needed |
274
+ #
275
+ # @param suffix_override [String, nil] Optional suffix override
276
+ # @return [String] 'OK' on success
277
+ def watch(...)
278
+ raise ArgumentError, 'Block required' unless block_given?
279
+
280
+ # Forward all arguments including the block to the watch command
281
+ dbclient.watch(dbkey, ...)
282
+
283
+ rescue Redis::BaseError => e
284
+ raise OptimisticLockError, "Redis error: #{e.message}"
285
+ end
286
+
287
+ # Flushes all the previously watched keys for a transaction.
288
+ #
289
+ # If a transaction completes successfully or discard is called, there's
290
+ # no need to manually call unwatch.
291
+ #
292
+ # NOTE: This command operates on the connection itself; not a specific key
293
+ #
294
+ # @return [String] 'OK' always, regardless of whether the key was watched or not
295
+ def unwatch(...) = dbclient.unwatch(...)
296
+
297
+ # Flushes all previously queued commands in a transaction and all watched keys
298
+ #
299
+ # NOTE: This command operates on the connection itself; not a specific key
300
+ #
301
+ # @return [String] 'OK' always
302
+ def discard(...) = dbclient.discard(...)
303
+
304
+ # Echoes a message through the Redis connection.
305
+ #
306
+ # @param args [Array] Arguments to join and echo
307
+ # @return [String] The echoed message
187
308
  def echo(*args)
188
309
  dbclient.echo "[#{self.class}] #{args.join(' ')}"
189
310
  end