familia 2.0.0.pre19 → 2.0.0.pre22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/.talismanrc +5 -1
  8. data/CHANGELOG.rst +220 -112
  9. data/CLAUDE.md +28 -1
  10. data/Gemfile +1 -1
  11. data/Gemfile.lock +20 -17
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  15. data/docs/guides/encryption.md +486 -0
  16. data/docs/guides/feature-encrypted-fields.md +123 -7
  17. data/docs/guides/feature-expiration.md +161 -117
  18. data/docs/guides/feature-external-identifiers.md +415 -443
  19. data/docs/guides/feature-object-identifiers.md +400 -269
  20. data/docs/guides/feature-quantization.md +120 -6
  21. data/docs/guides/feature-relationships-indexing.md +318 -0
  22. data/docs/guides/feature-relationships-methods.md +146 -604
  23. data/docs/guides/feature-relationships-participation.md +263 -0
  24. data/docs/guides/feature-relationships.md +118 -136
  25. data/docs/guides/feature-system-devs.md +176 -693
  26. data/docs/guides/feature-system.md +119 -6
  27. data/docs/guides/feature-transient-fields.md +81 -0
  28. data/docs/guides/field-system.md +778 -0
  29. data/docs/guides/index.md +32 -15
  30. data/docs/guides/logging.md +187 -0
  31. data/docs/guides/optimized-loading.md +674 -0
  32. data/docs/guides/thread-safety-monitoring.md +61 -0
  33. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  34. data/docs/migrating/v2.0.0-pre22.md +241 -0
  35. data/docs/overview.md +7 -9
  36. data/docs/reference/api-technical.md +267 -320
  37. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  38. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  39. data/examples/autoloader/mega_customer.rb +2 -0
  40. data/examples/datatype_standalone.rb +4 -3
  41. data/examples/encrypted_fields.rb +2 -1
  42. data/examples/json_usage_patterns.rb +2 -0
  43. data/examples/relationships.rb +3 -0
  44. data/examples/safe_dump.rb +2 -1
  45. data/examples/sampling_demo.rb +53 -0
  46. data/examples/single_connection_transaction_confusions.rb +2 -1
  47. data/familia.gemspec +2 -1
  48. data/lib/familia/base.rb +2 -0
  49. data/lib/familia/connection/behavior.rb +2 -0
  50. data/lib/familia/connection/handlers.rb +2 -0
  51. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  52. data/lib/familia/connection/middleware.rb +34 -24
  53. data/lib/familia/connection/operation_core.rb +3 -2
  54. data/lib/familia/connection/operations.rb +2 -0
  55. data/lib/familia/connection/pipelined_core.rb +3 -3
  56. data/lib/familia/connection/transaction_core.rb +69 -2
  57. data/lib/familia/connection.rb +18 -3
  58. data/lib/familia/data_type/class_methods.rb +3 -1
  59. data/lib/familia/data_type/connection.rb +2 -0
  60. data/lib/familia/data_type/database_commands.rb +2 -0
  61. data/lib/familia/data_type/serialization.rb +79 -52
  62. data/lib/familia/data_type/settings.rb +2 -0
  63. data/lib/familia/data_type/types/counter.rb +2 -0
  64. data/lib/familia/data_type/types/hashkey.rb +7 -5
  65. data/lib/familia/data_type/types/listkey.rb +2 -0
  66. data/lib/familia/data_type/types/lock.rb +2 -0
  67. data/lib/familia/data_type/types/sorted_set.rb +7 -10
  68. data/lib/familia/data_type/types/stringkey.rb +24 -0
  69. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  70. data/lib/familia/data_type.rb +2 -0
  71. data/lib/familia/encryption/encrypted_data.rb +4 -2
  72. data/lib/familia/encryption/manager.rb +2 -0
  73. data/lib/familia/encryption/provider.rb +2 -0
  74. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  75. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  76. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  77. data/lib/familia/encryption/registry.rb +2 -0
  78. data/lib/familia/encryption/request_cache.rb +2 -0
  79. data/lib/familia/encryption.rb +9 -2
  80. data/lib/familia/errors.rb +2 -0
  81. data/lib/familia/features/autoloader.rb +2 -0
  82. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  83. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  84. data/lib/familia/features/encrypted_fields.rb +2 -2
  85. data/lib/familia/features/expiration/extensions.rb +3 -1
  86. data/lib/familia/features/expiration.rb +12 -4
  87. data/lib/familia/features/external_identifier.rb +62 -7
  88. data/lib/familia/features/object_identifier.rb +49 -0
  89. data/lib/familia/features/quantization.rb +3 -1
  90. data/lib/familia/features/relationships/README.md +3 -1
  91. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  92. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +138 -9
  93. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +97 -21
  95. data/lib/familia/features/relationships/indexing.rb +3 -0
  96. data/lib/familia/features/relationships/indexing_relationship.rb +3 -1
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  98. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  100. data/lib/familia/features/relationships/participation.rb +155 -69
  101. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  102. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  103. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  104. data/lib/familia/features/relationships.rb +5 -3
  105. data/lib/familia/features/safe_dump.rb +2 -0
  106. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  107. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  108. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  109. data/lib/familia/features/transient_fields.rb +2 -0
  110. data/lib/familia/features.rb +2 -0
  111. data/lib/familia/field_type.rb +3 -1
  112. data/lib/familia/horreum/connection.rb +17 -1
  113. data/lib/familia/horreum/database_commands.rb +8 -1
  114. data/lib/familia/horreum/definition.rb +16 -6
  115. data/lib/familia/horreum/management.rb +353 -52
  116. data/lib/familia/horreum/persistence.rb +179 -108
  117. data/lib/familia/horreum/related_fields.rb +2 -0
  118. data/lib/familia/horreum/serialization.rb +23 -4
  119. data/lib/familia/horreum/settings.rb +2 -0
  120. data/lib/familia/horreum/utils.rb +2 -0
  121. data/lib/familia/horreum.rb +15 -1
  122. data/lib/familia/identifier_extractor.rb +3 -1
  123. data/lib/familia/instrumentation.rb +156 -0
  124. data/lib/familia/json_serializer.rb +2 -0
  125. data/lib/familia/logging.rb +92 -32
  126. data/lib/familia/refinements/dear_json.rb +2 -0
  127. data/lib/familia/refinements/stylize_words.rb +2 -14
  128. data/lib/familia/refinements/time_literals.rb +2 -0
  129. data/lib/familia/refinements.rb +2 -0
  130. data/lib/familia/secure_identifier.rb +10 -2
  131. data/lib/familia/settings.rb +2 -0
  132. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  133. data/lib/familia/thread_safety/monitor.rb +328 -0
  134. data/lib/familia/utils.rb +13 -0
  135. data/lib/familia/verifiable_identifier.rb +3 -1
  136. data/lib/familia/version.rb +3 -1
  137. data/lib/familia.rb +31 -4
  138. data/lib/middleware/database_command_counter.rb +152 -0
  139. data/lib/middleware/database_logger.rb +295 -170
  140. data/lib/multi_result.rb +61 -31
  141. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  142. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  143. data/try/edge_cases/json_serialization_try.rb +2 -0
  144. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  145. data/try/edge_cases/race_conditions_try.rb +4 -0
  146. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  147. data/try/edge_cases/string_coercion_try.rb +2 -0
  148. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  149. data/try/features/count_any_edge_cases_try.rb +486 -0
  150. data/try/features/count_any_methods_try.rb +197 -0
  151. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  152. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  153. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  154. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  155. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  156. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  157. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  158. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  159. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  160. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  161. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  162. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  164. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  165. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  166. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  167. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  168. data/try/features/encryption/config_persistence_try.rb +4 -0
  169. data/try/features/encryption/core_try.rb +4 -0
  170. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  171. data/try/features/encryption/module_loading_try.rb +4 -0
  172. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  173. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  174. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  175. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  176. data/try/features/expiration/expiration_try.rb +4 -0
  177. data/try/features/external_identifier/external_identifier_try.rb +305 -8
  178. data/try/features/feature_dependencies_try.rb +2 -0
  179. data/try/features/feature_improvements_try.rb +2 -0
  180. data/try/features/field_groups_try.rb +2 -0
  181. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  182. data/try/features/object_identifier/object_identifier_try.rb +140 -0
  183. data/try/features/quantization/quantization_try.rb +4 -0
  184. data/try/features/real_feature_integration_try.rb +2 -0
  185. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  186. data/try/features/relationships/indexing_rebuild_try.rb +606 -0
  187. data/try/features/relationships/indexing_try.rb +2 -0
  188. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  189. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  190. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  192. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  193. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  194. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  195. data/try/features/relationships/relationships_api_changes_try.rb +2 -0
  196. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  197. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  198. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  199. data/try/features/relationships/relationships_performance_try.rb +4 -0
  200. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  201. data/try/features/relationships/relationships_try.rb +6 -4
  202. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  203. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  204. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  205. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  206. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  207. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  208. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  209. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  210. data/try/integration/connection/fiber_context_preservation_try.rb +4 -0
  211. data/try/integration/connection/handler_constraints_try.rb +4 -0
  212. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  213. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  214. data/try/integration/connection/operation_mode_guards_try.rb +4 -0
  215. data/try/integration/connection/pipeline_fallback_integration_try.rb +3 -0
  216. data/try/integration/connection/pools_try.rb +4 -0
  217. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  218. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  219. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  220. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  221. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  222. data/try/integration/connection/transaction_modes_try.rb +4 -0
  223. data/try/integration/conventional_inheritance_try.rb +4 -0
  224. data/try/integration/create_method_try.rb +4 -0
  225. data/try/integration/cross_component_try.rb +4 -0
  226. data/try/integration/data_types/datatype_pipelines_try.rb +9 -3
  227. data/try/integration/data_types/datatype_transactions_try.rb +17 -7
  228. data/try/integration/database_consistency_try.rb +4 -0
  229. data/try/integration/familia_extended_try.rb +4 -0
  230. data/try/integration/familia_members_methods_try.rb +4 -0
  231. data/try/integration/models/customer_safe_dump_try.rb +4 -0
  232. data/try/integration/models/customer_try.rb +7 -3
  233. data/try/integration/models/datatype_base_try.rb +4 -0
  234. data/try/integration/models/familia_object_try.rb +4 -0
  235. data/try/integration/persistence_operations_try.rb +4 -0
  236. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  237. data/try/integration/save_methods_consistency_try.rb +241 -0
  238. data/try/integration/scenarios_try.rb +4 -0
  239. data/try/integration/secure_identifier_try.rb +4 -0
  240. data/try/integration/transaction_safety_core_try.rb +176 -0
  241. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  242. data/try/integration/verifiable_identifier_try.rb +4 -0
  243. data/try/investigation/pipeline_routing/README.md +228 -0
  244. data/try/performance/benchmarks_try.rb +4 -0
  245. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  246. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  247. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  248. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  249. data/try/support/debugging/debug_aad_process.rb +3 -0
  250. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  251. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  252. data/try/support/debugging/debug_context_aad.rb +3 -0
  253. data/try/support/debugging/debug_context_simple.rb +3 -0
  254. data/try/support/debugging/debug_cross_context.rb +3 -0
  255. data/try/support/debugging/debug_database_load.rb +3 -0
  256. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  257. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  258. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  259. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  260. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  261. data/try/support/debugging/debug_load_path.rb +3 -0
  262. data/try/support/debugging/debug_method_definition.rb +3 -0
  263. data/try/support/debugging/debug_method_resolution.rb +3 -0
  264. data/try/support/debugging/debug_minimal.rb +3 -0
  265. data/try/support/debugging/debug_provider.rb +3 -0
  266. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  267. data/try/support/debugging/debug_string_class.rb +3 -0
  268. data/try/support/debugging/debug_test.rb +3 -0
  269. data/try/support/debugging/debug_test_design.rb +3 -0
  270. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  271. data/try/support/debugging/provider_diagnostics.rb +4 -0
  272. data/try/support/helpers/test_cleanup.rb +4 -0
  273. data/try/support/helpers/test_helpers.rb +5 -0
  274. data/try/support/memory/memory_basic_test.rb +4 -0
  275. data/try/support/memory/memory_detailed_test.rb +4 -0
  276. data/try/support/memory/memory_search_for_string.rb +4 -0
  277. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  278. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  279. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  280. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  281. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  282. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  283. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  284. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  285. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  286. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  287. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  288. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  289. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  290. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  291. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  292. data/try/thread_safety/README.md +496 -0
  293. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  294. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  295. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  296. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  297. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  298. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  299. data/try/thread_safety/field_registration_race_try.rb +222 -0
  300. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  301. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  302. data/try/thread_safety/module_config_race_try.rb +175 -0
  303. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  304. data/try/unit/core/autoloader_try.rb +4 -0
  305. data/try/unit/core/base_enhancements_try.rb +4 -0
  306. data/try/unit/core/connection_try.rb +4 -0
  307. data/try/unit/core/errors_try.rb +4 -0
  308. data/try/unit/core/extensions_try.rb +4 -0
  309. data/try/unit/core/familia_logger_try.rb +2 -0
  310. data/try/unit/core/familia_try.rb +4 -0
  311. data/try/unit/core/middleware_sampling_try.rb +335 -0
  312. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  313. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  314. data/try/unit/core/middleware_try.rb +4 -0
  315. data/try/unit/core/settings_try.rb +4 -0
  316. data/try/unit/core/time_utils_try.rb +4 -0
  317. data/try/unit/core/tools_try.rb +4 -0
  318. data/try/unit/core/utils_try.rb +37 -0
  319. data/try/unit/data_types/boolean_try.rb +39 -22
  320. data/try/unit/data_types/counter_try.rb +4 -0
  321. data/try/unit/data_types/datatype_base_try.rb +4 -0
  322. data/try/unit/data_types/hash_try.rb +6 -2
  323. data/try/unit/data_types/list_try.rb +4 -0
  324. data/try/unit/data_types/lock_try.rb +4 -0
  325. data/try/unit/data_types/serialization_try.rb +386 -0
  326. data/try/unit/data_types/sorted_set_try.rb +4 -0
  327. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  328. data/try/unit/data_types/string_try.rb +4 -0
  329. data/try/unit/data_types/unsortedset_try.rb +4 -0
  330. data/try/unit/familia_resolve_class_try.rb +116 -0
  331. data/try/unit/horreum/auto_indexing_on_save_try.rb +5 -1
  332. data/try/unit/horreum/automatic_index_validation_try.rb +2 -0
  333. data/try/unit/horreum/base_try.rb +4 -0
  334. data/try/unit/horreum/class_methods_try.rb +4 -0
  335. data/try/unit/horreum/commands_try.rb +4 -0
  336. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  337. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +6 -1
  338. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  339. data/try/unit/horreum/field_categories_try.rb +4 -0
  340. data/try/unit/horreum/field_definition_try.rb +4 -0
  341. data/try/unit/horreum/initialization_try.rb +4 -0
  342. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  343. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  344. data/try/unit/horreum/relations_try.rb +4 -0
  345. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  346. data/try/unit/horreum/serialization_try.rb +4 -0
  347. data/try/unit/horreum/settings_try.rb +4 -0
  348. data/try/unit/horreum/unique_index_edge_cases_try.rb +4 -0
  349. data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -0
  350. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  351. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  352. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  353. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  354. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  355. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  356. data/try/unit/thread_safety_monitor_try.rb +149 -0
  357. metadata +69 -17
  358. data/.github/workflows/code-quality.yml +0 -138
  359. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +0 -91
  360. data/changelog.d/20251011_203905_delano_next.rst +0 -30
  361. data/changelog.d/20251011_212633_delano_next.rst +0 -13
  362. data/changelog.d/20251011_221253_delano_next.rst +0 -26
  363. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  364. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  365. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  366. data/docs/archive/README.md +0 -64
  367. data/docs/archive/api-reference.md +0 -333
  368. data/docs/guides/core-field-system.md +0 -806
  369. data/docs/guides/implementation.md +0 -276
  370. data/docs/guides/security-model.md +0 -183
@@ -1,46 +1,117 @@
1
1
  # lib/middleware/database_logger.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require 'concurrent-ruby'
4
6
 
5
- # DatabaseLogger is Valkey/RedisClient middleware.
7
+ # DatabaseLogger is redis-rb middleware for command logging and capture.
8
+ #
9
+ # Provides detailed Redis command logging for development and debugging.
10
+ # Familia uses the redis-rb gem (v4.8.1 to <6.0), which internally uses
11
+ # RedisClient infrastructure for middleware. Users work with Redis.new
12
+ # connections and Redis:: exceptions - RedisClient is an implementation detail.
13
+ #
14
+ # ## User-Facing API
15
+ #
16
+ # Enable via Familia configuration:
17
+ # Familia.enable_database_logging = true
18
+ #
19
+ # Familia automatically calls RedisClient.register(DatabaseLogger) internally.
20
+ #
21
+ # ## Critical: Uses `super` not `yield` for middleware chaining
22
+ # @see https://github.com/redis-rb/redis-client#instrumentation-and-middlewares
23
+ # ## Internal: RedisClient Middleware Architecture
24
+ #
25
+ # RedisClient middlewares are modules that are `include`d into the
26
+ # `RedisClient::Middlewares` class, which inherits from `BasicMiddleware`.
27
+ # The middleware chain works through Ruby's method lookup and `super`.
28
+ #
29
+ # ### Middleware Chain Flow (Internal)
30
+ #
31
+ # ```ruby
32
+ # # Internal registration order (last registered is called first):
33
+ # RedisClient.register(DatabaseLogger) # Called second (internal)
34
+ # RedisClient.register(DatabaseCommandCounter) # Called first (internal)
35
+ #
36
+ # # Execution flow when client.call('SET', 'key', 'value') is invoked:
37
+ # DatabaseCommandCounter.call(cmd, config) { |result| ... }
38
+ # └─> super # Implicitly passes block to next middleware
39
+ # └─> DatabaseLogger.call(cmd, config)
40
+ # └─> super # Implicitly passes block to next middleware
41
+ # └─> BasicMiddleware.call(cmd, config)
42
+ # └─> yield command # Executes actual Redis command
43
+ # └─> Returns result
44
+ # ← result flows back up
45
+ # ← result flows back up
46
+ # ← result flows back up
47
+ # ← result flows back up
48
+ # ```
49
+ #
50
+ # ### Critical Implementation Detail: `super` vs `yield`
6
51
  #
7
- # This middleware addresses the need for detailed Database command logging, which
8
- # was removed from the redis-rb gem due to performance concerns. However, in
9
- # many development and debugging scenarios, the ability to log Database commands
10
- # can be invaluable.
52
+ # **MUST use `super`** to properly chain middlewares. Using `yield` breaks
53
+ # the chain because it executes the original block directly, bypassing other
54
+ # middlewares in the chain.
11
55
  #
12
- # @example Enable Database command logging
13
- # DatabaseLogger.logger = Logger.new(STDOUT)
14
- # RedisClient.register(DatabaseLogger)
56
+ # ```ruby
57
+ # # ✅ CORRECT - Chains to next middleware
58
+ # def call(command, config)
59
+ # result = super # Calls next middleware, block passes implicitly
60
+ # result
61
+ # end
62
+ #
63
+ # # ❌ WRONG - Breaks middleware chain
64
+ # def call(command, config)
65
+ # result = yield # Executes block directly, skips other middlewares!
66
+ # result
67
+ # end
68
+ # ```
69
+ #
70
+ # When `super` is called:
71
+ # 1. Ruby automatically passes the block to the next method in the chain
72
+ # 2. The next middleware's `call` method executes
73
+ # 3. Eventually reaches `BasicMiddleware.call` which does `yield command`
74
+ # 4. The actual Redis command executes
75
+ # 5. Results flow back up through each middleware
76
+ #
77
+ # ## Usage Examples
78
+ #
79
+ # @example Enable Redis command logging (recommended user-facing API)
80
+ # Familia.enable_database_logging = true
15
81
  #
16
82
  # @example Capture commands for testing
17
83
  # commands = DatabaseLogger.capture_commands do
18
84
  # redis.set('key', 'value')
19
85
  # redis.get('key')
20
86
  # end
21
- # puts commands.first[:command] # => ["SET", "key", "value"]
87
+ # puts commands.first.command # => "SET key value"
22
88
  #
23
- # @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
89
+ # @example Use with DatabaseCommandCounter
90
+ # Familia.enable_database_logging = true
91
+ # Familia.enable_database_counter = true
92
+ # # Both middlewares registered automatically and execute correctly in sequence
24
93
  #
25
- # @note While there were concerns about the performance impact of logging in
26
- # the redis-rb gem, this middleware is designed to be optional and can be
27
- # easily enabled or disabled as needed. The performance impact is minimal
28
- # when logging is disabled, and the benefits during development and debugging
29
- # often outweigh the slight performance cost when enabled.
94
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
30
95
  module DatabaseLogger
31
- @logger = nil
32
- @commands = Concurrent::Array.new
33
- @max_commands = 10_000
34
- @process_start = Time.now.to_f.freeze
35
-
96
+ # Data structure for captured command metadata
36
97
  CommandMessage = Data.define(:command, :μs, :timeline) do
37
98
  alias_method :to_a, :deconstruct
99
+
38
100
  def inspect
39
101
  cmd, duration, timeline = to_a
40
102
  format('%.6f %4dμs > %s', timeline, duration, cmd)
41
103
  end
42
104
  end
43
105
 
106
+ @logger = nil
107
+ @commands = Concurrent::Array.new
108
+ @max_commands = 10_000
109
+ @process_start = Time.now.to_f.freeze
110
+ @structured_logging = false
111
+ @sample_rate = nil # nil = log everything, 0.1 = 10%, 0.01 = 1%
112
+ @sample_counter = Concurrent::AtomicFixnum.new(0)
113
+ @commands_mutex = Mutex.new # Protects compound operations on @commands
114
+
44
115
  class << self
45
116
  # Gets/sets the logger instance used by DatabaseLogger.
46
117
  # @return [Logger, nil] The current logger instance or nil if not set.
@@ -50,8 +121,41 @@ module DatabaseLogger
50
121
  # @return [Integer] The maximum number of commands to capture.
51
122
  attr_accessor :max_commands
52
123
 
124
+ # Gets/sets structured logging mode.
125
+ # When enabled, outputs Redis commands with structured key=value context
126
+ # instead of formatted string output.
127
+ #
128
+ # @return [Boolean] Whether structured logging is enabled
129
+ #
130
+ # @example Enable structured logging
131
+ # DatabaseLogger.structured_logging = true
132
+ # # Outputs: "Redis command cmd=SET args=[key, value] duration_ms=0.42 db=0"
133
+ #
134
+ # @example Disable (default formatted output)
135
+ # DatabaseLogger.structured_logging = false
136
+ # # Outputs: "[123] 0.001234 567μs > SET key value"
137
+ attr_accessor :structured_logging
138
+
139
+ # Gets/sets the sampling rate for logging.
140
+ # Controls what percentage of commands are logged to reduce noise.
141
+ #
142
+ # @return [Float, nil] Sample rate (0.0-1.0) or nil for no sampling
143
+ #
144
+ # @example Log 10% of commands
145
+ # DatabaseLogger.sample_rate = 0.1
146
+ #
147
+ # @example Log 1% of commands (high-traffic production)
148
+ # DatabaseLogger.sample_rate = 0.01
149
+ #
150
+ # @example Disable sampling (log everything)
151
+ # DatabaseLogger.sample_rate = nil
152
+ #
153
+ # @note Command capture is unaffected - only logger output is sampled.
154
+ # This means tests can still verify commands while production logs stay clean.
155
+ attr_accessor :sample_rate
156
+
53
157
  # Gets the captured commands for testing purposes.
54
- # @return [Array] Array of command hashes with :command, :duration, :timeline
158
+ # @return [Array<CommandMessage>] Array of captured command messages
55
159
  attr_reader :commands
56
160
 
57
161
  # Gets the timestamp when DatabaseLogger was loaded.
@@ -59,9 +163,14 @@ module DatabaseLogger
59
163
  attr_reader :process_start
60
164
 
61
165
  # Clears the captured commands array.
62
- # @return [Array] Empty array
166
+ #
167
+ # Thread-safe via mutex to ensure test isolation.
168
+ #
169
+ # @return [nil]
63
170
  def clear_commands
64
- @commands.clear
171
+ @commands_mutex.synchronize do
172
+ @commands.clear
173
+ end
65
174
  nil
66
175
  end
67
176
 
@@ -69,63 +178,84 @@ module DatabaseLogger
69
178
  # This is useful for testing to see what commands were executed.
70
179
  #
71
180
  # @yield [] The block of code to execute while capturing commands.
72
- # @return [Array] Array of captured commands with timing information.
73
- # Each command is a hash with :command, :duration, :timestamp keys.
181
+ # @return [Array<CommandMessage>] Array of captured command messages
74
182
  #
75
183
  # @example Test what Redis commands your code executes
76
184
  # commands = DatabaseLogger.capture_commands do
77
185
  # my_library_method()
78
186
  # end
79
- # assert_equal "SET", commands.first[:command][0]
80
- # assert commands.first[:duration] > 0
187
+ # assert_equal "SET", commands.first.command.split.first
188
+ # assert commands.first.μs > 0
81
189
  def capture_commands
82
190
  clear_commands
83
191
  yield
84
192
  @commands.to_a
85
193
  end
86
194
 
87
- # Gets the current count of Database commands executed.
88
- # @return [Integer] The number of Database commands executed.
195
+ # Gets the current count of captured commands.
196
+ # @return [Integer] The number of commands currently captured
89
197
  def index
90
198
  @commands.size
91
199
  end
92
200
 
93
- # Thread-safe append with bounded size
201
+ # Appends a command message to the captured commands array.
94
202
  #
95
- # @param message [String] The message to append.
96
- # @return [Array] The updated array of commands.
203
+ # When the array reaches max_commands capacity, the oldest command is
204
+ # removed before adding the new one.
205
+ #
206
+ # @param message [CommandMessage] The command message to append
207
+ # @return [Array<CommandMessage>] The updated array of commands
208
+ # @api private
97
209
  def append_command(message)
210
+ # We can throw away commands and not worry about thread race conditions
211
+ # since no one is going to mind if the command list is +/- a few
212
+ # commands. Unlike how we care about the order that the commands
213
+ # appear in the list, we don't care about exact count when trimming.
98
214
  @commands.shift if @commands.size >= @max_commands
99
- @commands << message
215
+ @commands << message # this is threadsafe thanks to Concurrent::Array
100
216
  end
101
217
 
102
218
  # Returns the current time in microseconds.
103
- # This is used to measure the duration of Database commands.
104
- #
105
- # Alias: now_in_microseconds
219
+ # This is used to measure the duration of Redis commands.
106
220
  #
107
- # @return [Integer] The current time in microseconds.
221
+ # @return [Integer] The current time in microseconds
108
222
  def now_in_μs
109
223
  Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
110
224
  end
111
225
  alias now_in_microseconds now_in_μs
226
+
227
+ # Determines if this command should be logged based on sampling rate.
228
+ #
229
+ # Uses deterministic modulo-based sampling for consistent behavior.
230
+ # Thread-safe via atomic counter increment.
231
+ #
232
+ # @return [Boolean] true if command should be logged
233
+ # @api private
234
+ def should_log?
235
+ return true if @sample_rate.nil?
236
+ return false if @logger.nil?
237
+
238
+ # Deterministic sampling: every Nth command where N = 1/sample_rate
239
+ # e.g., 0.1 = every 10th, 0.01 = every 100th
240
+ sample_interval = (1.0 / @sample_rate).to_i
241
+ (@sample_counter.increment % sample_interval).zero?
242
+ end
112
243
  end
113
244
 
114
- # Logs the Database command and its execution time.
245
+ # Logs the Redis command and its execution time.
115
246
  #
116
- # This method is called for each Database command when the middleware is active.
117
- # It always captures commands for testing and logs them if a logger is set.
247
+ # This method is part of the RedisClient middleware chain. It MUST use `super`
248
+ # instead of `yield` to properly chain with other middlewares.
118
249
  #
119
- # @param command [Array] The Database command and its arguments.
120
- # @param _config [Hash] The configuration options for the Valkey/Redis
121
- # connection.
122
- # @return [Object] The result of the Database command execution.
250
+ # @param command [Array] The Redis command and its arguments
251
+ # @param config [RedisClient::Config, Hash] Connection configuration
252
+ # @return [Object] The result of the Redis command execution
123
253
  #
124
- # @note Commands are always captured with minimal overhead for testing purposes.
125
- # Logging only occurs when DatabaseLogger.logger is set.
126
- def call(command, _config)
254
+ # @note Commands are always captured for testing. Logging only occurs when
255
+ # DatabaseLogger.logger is set and sampling allows it.
256
+ def call(command, config)
127
257
  block_start = DatabaseLogger.now_in_μs
128
- result = yield
258
+ result = super # CRITICAL: Must use super, not yield, to chain middlewares
129
259
  block_duration = DatabaseLogger.now_in_μs - block_start
130
260
 
131
261
  # We intentionally use two different codepaths for getting the
@@ -136,9 +266,48 @@ module DatabaseLogger
136
266
  msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
137
267
  DatabaseLogger.append_command(msgpack)
138
268
 
139
- # Log if logger is set
140
- message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
141
- DatabaseLogger.logger&.trace(message)
269
+ # Dual-mode logging with sampling
270
+ if DatabaseLogger.should_log?
271
+ if DatabaseLogger.structured_logging && DatabaseLogger.logger
272
+ duration_ms = (block_duration / 1000.0).round(2)
273
+ db_num = if config.respond_to?(:db)
274
+ config.db
275
+ elsif config.is_a?(Hash)
276
+ config[:db]
277
+ end
278
+ DatabaseLogger.logger.trace(
279
+ "Redis command cmd=#{command.first} args=#{command[1..-1].inspect} " \
280
+ "duration_μs=#{block_duration} duration_ms=#{duration_ms} " \
281
+ "timeline=#{lifetime_duration} db=#{db_num} index=#{DatabaseLogger.index}"
282
+ )
283
+ elsif DatabaseLogger.logger
284
+ # Existing formatted output
285
+ message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
286
+ DatabaseLogger.logger.trace(message)
287
+ end
288
+ end
289
+
290
+ # Notify instrumentation hooks
291
+ if defined?(Familia::Instrumentation)
292
+ duration_ms = (block_duration / 1000.0).round(2)
293
+ db_num = if config.respond_to?(:db)
294
+ config.db
295
+ elsif config.is_a?(Hash)
296
+ config[:db]
297
+ end
298
+ conn_id = if config.respond_to?(:custom)
299
+ config.custom&.dig(:id)
300
+ elsif config.is_a?(Hash)
301
+ config.dig(:custom, :id)
302
+ end
303
+ Familia::Instrumentation.notify_command(
304
+ command.first,
305
+ duration_ms,
306
+ full_command: command,
307
+ db: db_num,
308
+ connection_id: conn_id,
309
+ )
310
+ end
142
311
 
143
312
  result
144
313
  end
@@ -148,9 +317,13 @@ module DatabaseLogger
148
317
  # Captures MULTI/EXEC and shows you the full transaction. The WATCH
149
318
  # and EXISTS appear separately because they're executed as individual
150
319
  # commands before the transaction starts.
151
- def call_pipelined(commands, _config)
320
+ #
321
+ # @param commands [Array<Array>] Array of command arrays
322
+ # @param config [RedisClient::Config, Hash] Connection configuration
323
+ # @return [Array] Results from pipelined commands
324
+ def call_pipelined(commands, config)
152
325
  block_start = DatabaseLogger.now_in_μs
153
- results = yield
326
+ results = yield # CRITICAL: For call_pipelined, yield is correct (not chaining)
154
327
  block_duration = DatabaseLogger.now_in_μs - block_start
155
328
  lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
156
329
 
@@ -159,138 +332,90 @@ module DatabaseLogger
159
332
  msgpack = CommandMessage.new(cmd_string, block_duration, lifetime_duration)
160
333
  DatabaseLogger.append_command(msgpack)
161
334
 
162
- message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
163
- DatabaseLogger.logger&.trace(message)
335
+ # Dual-mode logging with sampling
336
+ if DatabaseLogger.should_log?
337
+ if DatabaseLogger.structured_logging && DatabaseLogger.logger
338
+ duration_ms = (block_duration / 1000.0).round(2)
339
+ db_num = if config.respond_to?(:db)
340
+ config.db
341
+ elsif config.is_a?(Hash)
342
+ config[:db]
343
+ end
344
+ DatabaseLogger.logger.trace(
345
+ "Redis pipeline commands=#{commands.size} duration_μs=#{block_duration} " \
346
+ "duration_ms=#{duration_ms} timeline=#{lifetime_duration} " \
347
+ "db=#{db_num} index=#{DatabaseLogger.index}"
348
+ )
349
+ elsif DatabaseLogger.logger
350
+ message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
351
+ DatabaseLogger.logger.trace(message)
352
+ end
353
+ end
354
+
355
+ # Notify instrumentation hooks
356
+ if defined?(Familia::Instrumentation)
357
+ duration_ms = (block_duration / 1000.0).round(2)
358
+ db_num = if config.respond_to?(:db)
359
+ config.db
360
+ elsif config.is_a?(Hash)
361
+ config[:db]
362
+ end
363
+ conn_id = if config.respond_to?(:custom)
364
+ config.custom&.dig(:id)
365
+ elsif config.is_a?(Hash)
366
+ config.dig(:custom, :id)
367
+ end
368
+ Familia::Instrumentation.notify_pipeline(
369
+ commands.size,
370
+ duration_ms,
371
+ db: db_num,
372
+ connection_id: conn_id
373
+ )
374
+ end
164
375
 
165
376
  results
166
377
  end
167
378
 
168
- # call_once is used for commands that need dedicated connection handling:
379
+ # Handle call_once for commands requiring dedicated connection handling:
169
380
  #
170
- # * Blocking commands (BLPOP, BRPOP, BRPOPLPUSH)
171
- # * Pub/sub operations (SUBSCRIBE, PSUBSCRIBE)
172
- # * Commands requiring connection affinity
173
- # * Explicit non-pooled command execution
381
+ # * Blocking commands (BLPOP, BRPOP, BRPOPLPUSH)
382
+ # * Pub/sub operations (SUBSCRIBE, PSUBSCRIBE)
383
+ # * Commands requiring connection affinity
384
+ # * Explicit non-pooled command execution
174
385
  #
175
- def call_once(command, _config)
386
+ # @param command [Array] The Redis command and its arguments
387
+ # @param config [RedisClient::Config, Hash] Connection configuration
388
+ # @return [Object] The result of the Redis command execution
389
+ def call_once(command, config)
176
390
  block_start = DatabaseLogger.now_in_μs
177
- result = yield
391
+ result = yield # CRITICAL: For call_once, yield is correct (not chaining)
178
392
  block_duration = DatabaseLogger.now_in_μs - block_start
179
393
  lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
180
394
 
181
395
  msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
182
396
  DatabaseLogger.append_command(msgpack)
183
397
 
184
- message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
185
- DatabaseLogger.logger&.trace(message)
186
-
187
- result
188
- end
189
- end
190
-
191
- # DatabaseCommandCounter is Valkey/RedisClient middleware.
192
- #
193
- # This middleware counts the number of Database commands executed. It can be
194
- # useful for performance monitoring and debugging, allowing you to track
195
- # the volume of Database operations in your application.
196
- #
197
- # @example Enable Database command counting
198
- # DatabaseCommandCounter.reset
199
- # RedisClient.register(DatabaseCommandCounter)
200
- #
201
- # @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
202
- #
203
- module DatabaseCommandCounter
204
- @count = Concurrent::AtomicFixnum.new(0)
205
-
206
- # We skip SELECT because depending on how the Familia is connecting to redis
207
- # the number of SELECT commands can be a lot or just a little. For example in
208
- # a configuration where there's a connection to each logical db, there's only
209
- # one when the connection is made. When using a provider of via thread local
210
- # it could theoretically double the number of statements executed.
211
- @skip_commands = ::Set.new(['SELECT']).freeze
212
-
213
- class << self
214
- # Gets the set of commands to skip counting.
215
- # @return [UnsortedSet] The commands that won't be counted.
216
- attr_reader :skip_commands
217
-
218
- # Gets the current count of Database commands executed.
219
- # @return [Integer] The number of Database commands executed.
220
- def count
221
- @count.value
222
- end
223
-
224
- # Resets the command count to zero.
225
- # This method is thread-safe.
226
- # @return [Integer] The reset count (always 0).
227
- def reset
228
- @count.value = 0
398
+ # Dual-mode logging with sampling
399
+ if DatabaseLogger.should_log?
400
+ if DatabaseLogger.structured_logging && DatabaseLogger.logger
401
+ duration_ms = (block_duration / 1000.0).round(2)
402
+ db_num = if config.respond_to?(:db)
403
+ config.db
404
+ elsif config.is_a?(Hash)
405
+ config[:db]
406
+ end
407
+ DatabaseLogger.logger.trace(
408
+ "Redis command_once cmd=#{command.first} args=#{command[1..-1].inspect} " \
409
+ "duration_μs=#{block_duration} duration_ms=#{duration_ms} " \
410
+ "timeline=#{lifetime_duration} db=#{db_num} index=#{DatabaseLogger.index}"
411
+ )
412
+ elsif DatabaseLogger.logger
413
+ message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
414
+ DatabaseLogger.logger.trace(message)
415
+ end
229
416
  end
230
417
 
231
- # Increments the command count.
232
- # This method is thread-safe.
233
- # @return [Integer] The new count after incrementing.
234
- def increment
235
- @count.increment
236
- end
237
-
238
- def skip_command?(command)
239
- skip_commands.include?(command.first.to_s.upcase)
240
- end
241
-
242
- # Counts the number of Database commands executed within a block.
243
- #
244
- # This method captures the command count before and after executing the
245
- # provided block, returning the difference. This is useful for measuring
246
- # how many Database commands are executed by a specific operation.
247
- #
248
- # @yield [] The block of code to execute while counting commands.
249
- # @return [Integer] The number of Database commands executed within the block.
250
- #
251
- # @example Count commands in a block
252
- # commands_executed = DatabaseCommandCounter.count_commands do
253
- # dbclient.set('key1', 'value1')
254
- # dbclient.get('key1')
255
- # end
256
- # # commands_executed will be 2
257
- def count_commands
258
- start_count = count # Capture the current command count before execution
259
- yield # Execute the provided block
260
- end_count = count # Capture the command count after execution
261
- end_count - start_count # Return the difference (commands executed in block)
262
- end
263
- end
264
-
265
- def klass
266
- DatabaseCommandCounter
267
- end
268
-
269
- # Counts the Database command and delegates its execution.
270
- #
271
- # This method is called for each Database command when the middleware is active.
272
- # It increments the command count (unless the command is in the skip list)
273
- # and then yields to execute the actual command.
274
- #
275
- # @param command [Array] The Database command and its arguments.
276
- # @param _config [Hash] The configuration options for the Database connection.
277
- # @return [Object] The result of the Database command execution.
278
- def call(command, _config)
279
- klass.increment unless klass.skip_command?(command)
280
- yield
281
- end
282
-
283
- def call_pipelined(commands, _config)
284
- # Count all commands in the pipeline (except skipped ones)
285
- commands.each do |command|
286
- klass.increment unless klass.skip_command?(command)
287
- end
288
- yield
289
- end
290
-
291
- def call_once(command, _config)
292
- klass.increment unless klass.skip_command?(command)
293
- yield
418
+ result
294
419
  end
295
420
  end
296
421
  # rubocop:enable ThreadSafety/ClassInstanceVariable