familia 2.0.0.pre18 → 2.0.0.pre21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.rst +205 -88
  8. data/CLAUDE.md +62 -10
  9. data/Gemfile +3 -3
  10. data/Gemfile.lock +27 -62
  11. data/README.md +39 -0
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
  15. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
  16. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
  17. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
  18. data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
  19. data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
  20. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  21. data/docs/guides/encryption.md +486 -0
  22. data/docs/guides/feature-encrypted-fields.md +123 -7
  23. data/docs/guides/feature-expiration.md +177 -133
  24. data/docs/guides/feature-external-identifiers.md +415 -443
  25. data/docs/guides/feature-object-identifiers.md +400 -269
  26. data/docs/guides/feature-quantization.md +120 -6
  27. data/docs/guides/feature-relationships-indexing.md +318 -0
  28. data/docs/guides/feature-relationships-methods.md +146 -604
  29. data/docs/guides/feature-relationships-participation.md +263 -0
  30. data/docs/guides/feature-relationships.md +118 -136
  31. data/docs/guides/feature-system-devs.md +176 -693
  32. data/docs/guides/feature-system.md +119 -6
  33. data/docs/guides/feature-transient-fields.md +81 -0
  34. data/docs/guides/field-system.md +778 -0
  35. data/docs/guides/index.md +32 -15
  36. data/docs/guides/logging.md +187 -0
  37. data/docs/guides/optimized-loading.md +674 -0
  38. data/docs/guides/thread-safety-monitoring.md +61 -0
  39. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  40. data/docs/migrating/v2.0.0-pre19.md +197 -0
  41. data/docs/migrating/v2.0.0-pre22.md +241 -0
  42. data/docs/overview.md +7 -9
  43. data/docs/reference/api-technical.md +267 -320
  44. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  45. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  46. data/examples/autoloader/mega_customer.rb +2 -0
  47. data/examples/datatype_standalone.rb +282 -0
  48. data/examples/encrypted_fields.rb +2 -1
  49. data/examples/json_usage_patterns.rb +2 -0
  50. data/examples/relationships.rb +3 -0
  51. data/examples/safe_dump.rb +2 -1
  52. data/examples/sampling_demo.rb +53 -0
  53. data/examples/single_connection_transaction_confusions.rb +2 -1
  54. data/familia.gemspec +2 -1
  55. data/lib/familia/base.rb +2 -0
  56. data/lib/familia/connection/behavior.rb +254 -0
  57. data/lib/familia/connection/handlers.rb +97 -0
  58. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  59. data/lib/familia/connection/middleware.rb +34 -24
  60. data/lib/familia/connection/operation_core.rb +3 -1
  61. data/lib/familia/connection/operations.rb +2 -0
  62. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
  63. data/lib/familia/connection/transaction_core.rb +75 -9
  64. data/lib/familia/connection.rb +21 -5
  65. data/lib/familia/data_type/class_methods.rb +3 -1
  66. data/lib/familia/data_type/connection.rb +153 -7
  67. data/lib/familia/data_type/database_commands.rb +9 -4
  68. data/lib/familia/data_type/serialization.rb +10 -4
  69. data/lib/familia/data_type/settings.rb +2 -0
  70. data/lib/familia/data_type/types/counter.rb +2 -0
  71. data/lib/familia/data_type/types/hashkey.rb +8 -6
  72. data/lib/familia/data_type/types/listkey.rb +2 -0
  73. data/lib/familia/data_type/types/lock.rb +2 -0
  74. data/lib/familia/data_type/types/sorted_set.rb +2 -0
  75. data/lib/familia/data_type/types/stringkey.rb +2 -0
  76. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  77. data/lib/familia/data_type.rb +2 -0
  78. data/lib/familia/encryption/encrypted_data.rb +4 -2
  79. data/lib/familia/encryption/manager.rb +2 -0
  80. data/lib/familia/encryption/provider.rb +2 -0
  81. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  82. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  83. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  84. data/lib/familia/encryption/registry.rb +2 -0
  85. data/lib/familia/encryption/request_cache.rb +2 -0
  86. data/lib/familia/encryption.rb +9 -2
  87. data/lib/familia/errors.rb +53 -14
  88. data/lib/familia/features/autoloader.rb +2 -0
  89. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  90. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  91. data/lib/familia/features/encrypted_fields.rb +2 -2
  92. data/lib/familia/features/expiration/extensions.rb +11 -11
  93. data/lib/familia/features/expiration.rb +29 -21
  94. data/lib/familia/features/external_identifier.rb +33 -7
  95. data/lib/familia/features/object_identifier.rb +2 -0
  96. data/lib/familia/features/quantization.rb +3 -1
  97. data/lib/familia/features/relationships/README.md +3 -1
  98. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  99. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
  100. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  101. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
  102. data/lib/familia/features/relationships/indexing.rb +40 -42
  103. data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
  104. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  105. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  106. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  107. data/lib/familia/features/relationships/participation.rb +155 -69
  108. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  109. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  110. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  111. data/lib/familia/features/relationships.rb +5 -3
  112. data/lib/familia/features/safe_dump.rb +2 -0
  113. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  114. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  115. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  116. data/lib/familia/features/transient_fields.rb +2 -0
  117. data/lib/familia/features.rb +2 -0
  118. data/lib/familia/field_type.rb +5 -2
  119. data/lib/familia/horreum/connection.rb +28 -36
  120. data/lib/familia/horreum/database_commands.rb +131 -10
  121. data/lib/familia/horreum/definition.rb +18 -7
  122. data/lib/familia/horreum/management.rb +233 -57
  123. data/lib/familia/horreum/persistence.rb +314 -122
  124. data/lib/familia/horreum/related_fields.rb +2 -0
  125. data/lib/familia/horreum/serialization.rb +26 -4
  126. data/lib/familia/horreum/settings.rb +2 -0
  127. data/lib/familia/horreum/utils.rb +2 -8
  128. data/lib/familia/horreum.rb +46 -13
  129. data/lib/familia/identifier_extractor.rb +2 -0
  130. data/lib/familia/instrumentation.rb +156 -0
  131. data/lib/familia/json_serializer.rb +2 -0
  132. data/lib/familia/logging.rb +94 -37
  133. data/lib/familia/refinements/dear_json.rb +2 -0
  134. data/lib/familia/refinements/stylize_words.rb +2 -14
  135. data/lib/familia/refinements/time_literals.rb +2 -0
  136. data/lib/familia/refinements.rb +2 -0
  137. data/lib/familia/secure_identifier.rb +10 -2
  138. data/lib/familia/settings.rb +9 -7
  139. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  140. data/lib/familia/thread_safety/monitor.rb +328 -0
  141. data/lib/familia/utils.rb +13 -0
  142. data/lib/familia/verifiable_identifier.rb +3 -1
  143. data/lib/familia/version.rb +3 -1
  144. data/lib/familia.rb +31 -4
  145. data/lib/middleware/database_command_counter.rb +152 -0
  146. data/lib/middleware/database_logger.rb +325 -129
  147. data/lib/multi_result.rb +2 -0
  148. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  149. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  150. data/try/edge_cases/json_serialization_try.rb +2 -0
  151. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  152. data/try/edge_cases/race_conditions_try.rb +4 -0
  153. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  154. data/try/edge_cases/string_coercion_try.rb +6 -4
  155. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  156. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  157. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  158. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  159. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  160. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  161. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  162. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  164. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  165. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  166. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  167. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  168. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  169. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  170. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  171. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  172. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  173. data/try/features/encryption/config_persistence_try.rb +4 -0
  174. data/try/features/encryption/core_try.rb +4 -0
  175. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  176. data/try/features/encryption/module_loading_try.rb +4 -0
  177. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  178. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  179. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  180. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  181. data/try/features/expiration/expiration_try.rb +5 -1
  182. data/try/features/external_identifier/external_identifier_try.rb +171 -8
  183. data/try/features/feature_dependencies_try.rb +2 -0
  184. data/try/features/feature_improvements_try.rb +2 -0
  185. data/try/features/field_groups_try.rb +2 -0
  186. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  187. data/try/features/object_identifier/object_identifier_try.rb +2 -0
  188. data/try/features/quantization/quantization_try.rb +4 -0
  189. data/try/features/real_feature_integration_try.rb +2 -0
  190. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/indexing_rebuild_try.rb +600 -0
  192. data/try/features/relationships/indexing_try.rb +30 -4
  193. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  194. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  195. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  196. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  197. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  198. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  199. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  200. data/try/features/relationships/relationships_api_changes_try.rb +6 -4
  201. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  202. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  203. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  204. data/try/features/relationships/relationships_performance_try.rb +4 -0
  205. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  206. data/try/features/relationships/relationships_try.rb +6 -4
  207. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  208. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  209. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  210. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  211. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  212. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  213. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  214. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  215. data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
  216. data/try/integration/connection/handler_constraints_try.rb +4 -0
  217. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  218. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  219. data/try/integration/connection/operation_mode_guards_try.rb +5 -1
  220. data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
  221. data/try/integration/connection/pools_try.rb +4 -0
  222. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  223. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  224. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  225. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  226. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  227. data/try/integration/connection/transaction_modes_try.rb +4 -0
  228. data/try/integration/conventional_inheritance_try.rb +4 -0
  229. data/try/integration/create_method_try.rb +26 -22
  230. data/try/integration/cross_component_try.rb +4 -0
  231. data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
  232. data/try/integration/data_types/datatype_transactions_try.rb +251 -0
  233. data/try/integration/database_consistency_try.rb +4 -0
  234. data/try/integration/familia_extended_try.rb +4 -0
  235. data/try/integration/familia_members_methods_try.rb +4 -0
  236. data/try/integration/models/customer_safe_dump_try.rb +9 -1
  237. data/try/integration/models/customer_try.rb +4 -0
  238. data/try/integration/models/datatype_base_try.rb +4 -0
  239. data/try/integration/models/familia_object_try.rb +5 -1
  240. data/try/integration/persistence_operations_try.rb +166 -10
  241. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  242. data/try/integration/save_methods_consistency_try.rb +241 -0
  243. data/try/integration/scenarios_try.rb +4 -0
  244. data/try/integration/secure_identifier_try.rb +4 -0
  245. data/try/integration/transaction_safety_core_try.rb +176 -0
  246. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  247. data/try/integration/verifiable_identifier_try.rb +4 -0
  248. data/try/investigation/pipeline_routing/README.md +228 -0
  249. data/try/performance/benchmarks_try.rb +4 -0
  250. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  251. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  252. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  253. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  254. data/try/support/debugging/debug_aad_process.rb +3 -0
  255. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  256. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  257. data/try/support/debugging/debug_context_aad.rb +3 -0
  258. data/try/support/debugging/debug_context_simple.rb +3 -0
  259. data/try/support/debugging/debug_cross_context.rb +3 -0
  260. data/try/support/debugging/debug_database_load.rb +3 -0
  261. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  262. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  263. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  264. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  265. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  266. data/try/support/debugging/debug_load_path.rb +3 -0
  267. data/try/support/debugging/debug_method_definition.rb +3 -0
  268. data/try/support/debugging/debug_method_resolution.rb +3 -0
  269. data/try/support/debugging/debug_minimal.rb +3 -0
  270. data/try/support/debugging/debug_provider.rb +3 -0
  271. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  272. data/try/support/debugging/debug_string_class.rb +3 -0
  273. data/try/support/debugging/debug_test.rb +3 -0
  274. data/try/support/debugging/debug_test_design.rb +3 -0
  275. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  276. data/try/support/debugging/provider_diagnostics.rb +4 -0
  277. data/try/support/helpers/test_cleanup.rb +4 -0
  278. data/try/support/helpers/test_helpers.rb +5 -0
  279. data/try/support/memory/memory_basic_test.rb +4 -0
  280. data/try/support/memory/memory_detailed_test.rb +4 -0
  281. data/try/support/memory/memory_search_for_string.rb +4 -0
  282. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  283. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  284. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  285. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  286. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  287. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  288. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  289. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  290. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  291. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  292. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  293. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  294. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  295. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  296. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  297. data/try/thread_safety/README.md +496 -0
  298. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  299. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  300. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  301. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  302. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  303. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  304. data/try/thread_safety/field_registration_race_try.rb +222 -0
  305. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  306. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  307. data/try/thread_safety/module_config_race_try.rb +175 -0
  308. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  309. data/try/unit/core/autoloader_try.rb +4 -0
  310. data/try/unit/core/base_enhancements_try.rb +4 -0
  311. data/try/unit/core/connection_try.rb +4 -0
  312. data/try/unit/core/errors_try.rb +4 -0
  313. data/try/unit/core/extensions_try.rb +4 -0
  314. data/try/unit/core/familia_logger_try.rb +2 -0
  315. data/try/unit/core/familia_try.rb +4 -0
  316. data/try/unit/core/middleware_sampling_try.rb +335 -0
  317. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  318. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  319. data/try/unit/core/middleware_try.rb +4 -0
  320. data/try/unit/core/settings_try.rb +4 -0
  321. data/try/unit/core/time_utils_try.rb +4 -0
  322. data/try/unit/core/tools_try.rb +4 -0
  323. data/try/unit/core/utils_try.rb +37 -0
  324. data/try/unit/data_types/boolean_try.rb +5 -1
  325. data/try/unit/data_types/counter_try.rb +4 -0
  326. data/try/unit/data_types/datatype_base_try.rb +4 -0
  327. data/try/unit/data_types/hash_try.rb +4 -0
  328. data/try/unit/data_types/list_try.rb +4 -0
  329. data/try/unit/data_types/lock_try.rb +4 -0
  330. data/try/unit/data_types/sorted_set_try.rb +4 -0
  331. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  332. data/try/unit/data_types/string_try.rb +5 -1
  333. data/try/unit/data_types/unsortedset_try.rb +4 -0
  334. data/try/unit/familia_resolve_class_try.rb +116 -0
  335. data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
  336. data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
  337. data/try/unit/horreum/base_try.rb +5 -1
  338. data/try/unit/horreum/class_methods_try.rb +6 -2
  339. data/try/unit/horreum/commands_try.rb +4 -0
  340. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  341. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
  342. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  343. data/try/unit/horreum/field_categories_try.rb +4 -0
  344. data/try/unit/horreum/field_definition_try.rb +4 -0
  345. data/try/unit/horreum/initialization_try.rb +5 -1
  346. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  347. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  348. data/try/unit/horreum/relations_try.rb +8 -4
  349. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  350. data/try/unit/horreum/serialization_try.rb +6 -2
  351. data/try/unit/horreum/settings_try.rb +4 -0
  352. data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
  353. data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
  354. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  355. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  356. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  357. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  358. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  359. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  360. data/try/unit/thread_safety_monitor_try.rb +149 -0
  361. metadata +81 -14
  362. data/.github/workflows/code-quality.yml +0 -138
  363. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  364. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  365. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  366. data/docs/archive/README.md +0 -64
  367. data/docs/archive/api-reference.md +0 -333
  368. data/docs/guides/core-field-system.md +0 -806
  369. data/docs/guides/implementation.md +0 -276
  370. data/docs/guides/security-model.md +0 -183
@@ -1,39 +1,116 @@
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
+ # ```
6
49
  #
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.
50
+ # ### Critical Implementation Detail: `super` vs `yield`
11
51
  #
12
- # @example Enable Database command logging
13
- # DatabaseLogger.logger = Logger.new(STDOUT)
14
- # RedisClient.register(DatabaseLogger)
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.
55
+ #
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
96
+ # Data structure for captured command metadata
97
+ CommandMessage = Data.define(:command, :μs, :timeline) do
98
+ alias_method :to_a, :deconstruct
99
+
100
+ def inspect
101
+ cmd, duration, timeline = to_a
102
+ format('%.6f %4dμs > %s', timeline, duration, cmd)
103
+ end
104
+ end
105
+
31
106
  @logger = nil
32
107
  @commands = Concurrent::Array.new
33
108
  @max_commands = 10_000
34
109
  @process_start = Time.now.to_f.freeze
35
-
36
- CommandMessage = Data.define(:command, :μs, :timeline)
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
37
114
 
38
115
  class << self
39
116
  # Gets/sets the logger instance used by DatabaseLogger.
@@ -44,8 +121,41 @@ module DatabaseLogger
44
121
  # @return [Integer] The maximum number of commands to capture.
45
122
  attr_accessor :max_commands
46
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
+
47
157
  # Gets the captured commands for testing purposes.
48
- # @return [Array] Array of command hashes with :command, :duration, :timeline
158
+ # @return [Array<CommandMessage>] Array of captured command messages
49
159
  attr_reader :commands
50
160
 
51
161
  # Gets the timestamp when DatabaseLogger was loaded.
@@ -53,9 +163,14 @@ module DatabaseLogger
53
163
  attr_reader :process_start
54
164
 
55
165
  # Clears the captured commands array.
56
- # @return [Array] Empty array
166
+ #
167
+ # Thread-safe via mutex to ensure test isolation.
168
+ #
169
+ # @return [nil]
57
170
  def clear_commands
58
- @commands.clear
171
+ @commands_mutex.synchronize do
172
+ @commands.clear
173
+ end
59
174
  nil
60
175
  end
61
176
 
@@ -63,163 +178,244 @@ module DatabaseLogger
63
178
  # This is useful for testing to see what commands were executed.
64
179
  #
65
180
  # @yield [] The block of code to execute while capturing commands.
66
- # @return [Array] Array of captured commands with timing information.
67
- # Each command is a hash with :command, :duration, :timestamp keys.
181
+ # @return [Array<CommandMessage>] Array of captured command messages
68
182
  #
69
183
  # @example Test what Redis commands your code executes
70
184
  # commands = DatabaseLogger.capture_commands do
71
185
  # my_library_method()
72
186
  # end
73
- # assert_equal "SET", commands.first[:command][0]
74
- # assert commands.first[:duration] > 0
187
+ # assert_equal "SET", commands.first.command.split.first
188
+ # assert commands.first.μs > 0
75
189
  def capture_commands
76
190
  clear_commands
77
191
  yield
78
192
  @commands.to_a
79
193
  end
80
194
 
81
- # Thread-safe append with bounded size
195
+ # Gets the current count of captured commands.
196
+ # @return [Integer] The number of commands currently captured
197
+ def index
198
+ @commands.size
199
+ end
200
+
201
+ # Appends a command message to the captured commands array.
202
+ #
203
+ # When the array reaches max_commands capacity, the oldest command is
204
+ # removed before adding the new one.
82
205
  #
83
- # @param message [String] The message to append.
84
- # @return [Array] The updated array of commands.
206
+ # @param message [CommandMessage] The command message to append
207
+ # @return [Array<CommandMessage>] The updated array of commands
208
+ # @api private
85
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.
86
214
  @commands.shift if @commands.size >= @max_commands
87
- @commands << message
215
+ @commands << message # this is threadsafe thanks to Concurrent::Array
88
216
  end
89
217
 
90
218
  # Returns the current time in microseconds.
91
- # This is used to measure the duration of Database commands.
219
+ # This is used to measure the duration of Redis commands.
92
220
  #
93
- # Alias: now_in_microseconds
94
- #
95
- # @return [Integer] The current time in microseconds.
221
+ # @return [Integer] The current time in microseconds
96
222
  def now_in_μs
97
223
  Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
98
224
  end
99
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
100
243
  end
101
244
 
102
- # Logs the Database command and its execution time.
245
+ # Logs the Redis command and its execution time.
103
246
  #
104
- # This method is called for each Database command when the middleware is active.
105
- # 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.
106
249
  #
107
- # @param command [Array] The Database command and its arguments.
108
- # @param _config [Hash] The configuration options for the Valkey/Redis
109
- # connection.
110
- # @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
111
253
  #
112
- # @note Commands are always captured with minimal overhead for testing purposes.
113
- # Logging only occurs when DatabaseLogger.logger is set.
114
- 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)
115
257
  block_start = DatabaseLogger.now_in_μs
116
- result = yield
258
+ result = super # CRITICAL: Must use super, not yield, to chain middlewares
117
259
  block_duration = DatabaseLogger.now_in_μs - block_start
118
- lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
119
260
 
120
261
  # We intentionally use two different codepaths for getting the
121
262
  # time, although they will almost always be so similar that the
122
263
  # difference is negligible.
123
- message = CommandMessage.new(command, block_duration, lifetime_duration)
124
- DatabaseLogger.append_command(message)
125
-
126
- # Log if logger is set
127
- DatabaseLogger.logger&.debug(Oj.dump(message.to_h, mode: :strict))
128
-
129
- result
130
- end
131
- end
132
-
133
- # DatabaseCommandCounter is Valkey/RedisClient middleware.
134
- #
135
- # This middleware counts the number of Database commands executed. It can be
136
- # useful for performance monitoring and debugging, allowing you to track
137
- # the volume of Database operations in your application.
138
- #
139
- # @example Enable Database command counting
140
- # DatabaseCommandCounter.reset
141
- # RedisClient.register(DatabaseCommandCounter)
142
- #
143
- # @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
144
- #
145
- module DatabaseCommandCounter
146
- @count = Concurrent::AtomicFixnum.new(0)
264
+ lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
147
265
 
148
- # We skip SELECT because depending on how the Familia is connecting to redis
149
- # the number of SELECT commands can be a lot or just a little. For example in
150
- # a configuration where there's a connection to each logical db, there's only
151
- # one when the connection is made. When using a provider of via thread local
152
- # it could theoretically double the number of statements executed.
153
- @skip_commands = ::Set.new(['SELECT']).freeze
266
+ msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
267
+ DatabaseLogger.append_command(msgpack)
154
268
 
155
- class << self
156
- # Gets the set of commands to skip counting.
157
- # @return [UnsortedSet] The commands that won't be counted.
158
- attr_reader :skip_commands
159
-
160
- # Gets the current count of Database commands executed.
161
- # @return [Integer] The number of Database commands executed.
162
- def count
163
- @count.value
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
164
288
  end
165
289
 
166
- # Resets the command count to zero.
167
- # This method is thread-safe.
168
- # @return [Integer] The reset count (always 0).
169
- def reset
170
- @count.value = 0
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
+ )
171
310
  end
172
311
 
173
- # Increments the command count.
174
- # This method is thread-safe.
175
- # @return [Integer] The new count after incrementing.
176
- def increment
177
- @count.increment
178
- end
312
+ result
313
+ end
314
+
315
+ # Handle pipelined commands (including MULTI/EXEC transactions)
316
+ #
317
+ # Captures MULTI/EXEC and shows you the full transaction. The WATCH
318
+ # and EXISTS appear separately because they're executed as individual
319
+ # commands before the transaction starts.
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)
325
+ block_start = DatabaseLogger.now_in_μs
326
+ results = yield # CRITICAL: For call_pipelined, yield is correct (not chaining)
327
+ block_duration = DatabaseLogger.now_in_μs - block_start
328
+ lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
179
329
 
180
- def skip_command?(command)
181
- skip_commands.include?(command.first.to_s.upcase)
330
+ # Log the entire pipeline as a single operation
331
+ cmd_string = commands.map { |cmd| cmd.join(' ') }.join(' | ')
332
+ msgpack = CommandMessage.new(cmd_string, block_duration, lifetime_duration)
333
+ DatabaseLogger.append_command(msgpack)
334
+
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
182
353
  end
183
354
 
184
- # Counts the number of Database commands executed within a block.
185
- #
186
- # This method captures the command count before and after executing the
187
- # provided block, returning the difference. This is useful for measuring
188
- # how many Database commands are executed by a specific operation.
189
- #
190
- # @yield [] The block of code to execute while counting commands.
191
- # @return [Integer] The number of Database commands executed within the block.
192
- #
193
- # @example Count commands in a block
194
- # commands_executed = DatabaseCommandCounter.count_commands do
195
- # dbclient.set('key1', 'value1')
196
- # dbclient.get('key1')
197
- # end
198
- # # commands_executed will be 2
199
- def count_commands
200
- start_count = count # Capture the current command count before execution
201
- yield # Execute the provided block
202
- end_count = count # Capture the command count after execution
203
- end_count - start_count # Return the difference (commands executed in block)
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
+ )
204
374
  end
205
- end
206
375
 
207
- def klass
208
- DatabaseCommandCounter
376
+ results
209
377
  end
210
378
 
211
- # Counts the Database command and delegates its execution.
379
+ # Handle call_once for commands requiring dedicated connection handling:
212
380
  #
213
- # This method is called for each Database command when the middleware is active.
214
- # It increments the command count (unless the command is in the skip list)
215
- # and then yields to execute the actual command.
381
+ # * Blocking commands (BLPOP, BRPOP, BRPOPLPUSH)
382
+ # * Pub/sub operations (SUBSCRIBE, PSUBSCRIBE)
383
+ # * Commands requiring connection affinity
384
+ # * Explicit non-pooled command execution
216
385
  #
217
- # @param command [Array] The Database command and its arguments.
218
- # @param _config [Hash] The configuration options for the Database connection.
219
- # @return [Object] The result of the Database command execution.
220
- def call(command, _config)
221
- klass.increment unless klass.skip_command?(command)
222
- yield
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)
390
+ block_start = DatabaseLogger.now_in_μs
391
+ result = yield # CRITICAL: For call_once, yield is correct (not chaining)
392
+ block_duration = DatabaseLogger.now_in_μs - block_start
393
+ lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
394
+
395
+ msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
396
+ DatabaseLogger.append_command(msgpack)
397
+
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
416
+ end
417
+
418
+ result
223
419
  end
224
420
  end
225
421
  # rubocop:enable ThreadSafety/ClassInstanceVariable
data/lib/multi_result.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # lib/multi_result.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # Represents the result of a Valkey/Redis transaction operation.
4
6
  #
@@ -1,4 +1,6 @@
1
1
  # try/edge_cases/empty_identifiers_try.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # Test empty identifier edge cases
4
6
 
@@ -1,4 +1,6 @@
1
1
  # try/edge_cases/hash_symbolization_try.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # NOTE: These testcases are disabled b/c there's a shared context
4
6
  # bug in Tryouts 3.1 that prevents the setup instance vars from
@@ -1,4 +1,6 @@
1
1
  # try/edge_cases/json_serialization_try.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative '../support/helpers/test_helpers'
4
6
 
@@ -1,3 +1,7 @@
1
+ # try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # Edge case tests for deserialize_value with legacy data detection
2
6
  #
3
7
  # Tests the nuanced deserialization that distinguishes between:
@@ -1,3 +1,7 @@
1
+ # try/edge_cases/race_conditions_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # Test connection race conditions
2
6
 
3
7
  require_relative '../support/helpers/test_helpers'
@@ -1,3 +1,7 @@
1
+ # try/edge_cases/reserved_keywords_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # Test reserved keyword handling
2
6
 
3
7
  require_relative '../support/helpers/test_helpers'
@@ -1,4 +1,6 @@
1
1
  # try/edge_cases/string_coercion_try.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative '../support/helpers/test_helpers'
4
6
 
@@ -130,15 +132,15 @@ process_identifier(@customer)
130
132
 
131
133
  ## Cleanup after test, 1
132
134
  @metadata.delete!
133
- #=> true
135
+ #=> 1
134
136
 
135
137
  ## Cleanup after test, 2
136
138
  @customer.delete!
137
- #=> true
139
+ #=> 1
138
140
 
139
141
  ## Cleanup after test, 3
140
142
  @session.delete!
141
- #=> true
143
+ #=> 1
142
144
 
143
145
  ## to_s handles identifier errors gracefully
144
146
  badboi = BadIdentifierTest.new
@@ -154,4 +156,4 @@ badboi.to_s # .include?('BadIdentifierTest')
154
156
 
155
157
  ## Delete customer2
156
158
  [@customer2.exists?, @customer2.delete!]
157
- #=> [false, false]
159
+ #=> [false, 0]
@@ -1,3 +1,7 @@
1
+ # try/edge_cases/ttl_side_effects_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # Test TTL side effects
2
6
 
3
7
  require_relative '../support/helpers/test_helpers'
@@ -1,3 +1,7 @@
1
+ # try/features/encrypted_fields/aad_protection_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # try/features/encryption_fields/aad_protection_try.rb
2
6
 
3
7
  require 'concurrent'
@@ -1,3 +1,7 @@
1
+ # try/features/encrypted_fields/concealed_string_core_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # try/features/encryption_fields/concealed_string_core_try.rb
2
6
 
3
7
  require_relative '../../support/helpers/test_helpers'