familia 2.0.0.pre14 → 2.0.0.pre16

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 (276) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +66 -6
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +4 -4
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre13.md +1 -1
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +623 -19
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +6 -6
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +49 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/commands.rb +53 -51
  65. data/lib/familia/data_type/serialization.rb +108 -107
  66. data/lib/familia/data_type/types/counter.rb +1 -1
  67. data/lib/familia/data_type/types/hashkey.rb +13 -10
  68. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  69. data/lib/familia/data_type/types/lock.rb +3 -2
  70. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  71. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  72. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  73. data/lib/familia/data_type.rb +75 -47
  74. data/lib/familia/distinguisher.rb +85 -0
  75. data/lib/familia/encryption/encrypted_data.rb +15 -24
  76. data/lib/familia/encryption/manager.rb +6 -4
  77. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  78. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  79. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  80. data/lib/familia/encryption/request_cache.rb +7 -7
  81. data/lib/familia/encryption.rb +2 -3
  82. data/lib/familia/errors.rb +9 -3
  83. data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
  84. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  85. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  86. data/lib/familia/features/encrypted_fields.rb +68 -66
  87. data/lib/familia/features/expiration/extensions.rb +61 -0
  88. data/lib/familia/features/expiration.rb +35 -87
  89. data/lib/familia/features/external_identifier.rb +11 -12
  90. data/lib/familia/features/object_identifier.rb +58 -20
  91. data/lib/familia/features/quantization.rb +17 -22
  92. data/lib/familia/features/relationships/README.md +97 -0
  93. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  94. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  96. data/lib/familia/features/relationships/indexing.rb +176 -256
  97. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  98. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  100. data/lib/familia/features/relationships/participation.rb +656 -0
  101. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  102. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  103. data/lib/familia/features/relationships.rb +69 -271
  104. data/lib/familia/features/safe_dump.rb +127 -132
  105. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  106. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  107. data/lib/familia/features/transient_fields.rb +5 -5
  108. data/lib/familia/features.rb +21 -21
  109. data/lib/familia/field_type.rb +24 -4
  110. data/lib/familia/horreum/core/connection.rb +229 -26
  111. data/lib/familia/horreum/core/database_commands.rb +27 -17
  112. data/lib/familia/horreum/core/serialization.rb +40 -20
  113. data/lib/familia/horreum/core/utils.rb +2 -1
  114. data/lib/familia/horreum/shared/settings.rb +2 -1
  115. data/lib/familia/horreum/subclass/definition.rb +33 -45
  116. data/lib/familia/horreum/subclass/management.rb +72 -24
  117. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  118. data/lib/familia/horreum.rb +196 -114
  119. data/lib/familia/json_serializer.rb +0 -1
  120. data/lib/familia/logging.rb +11 -114
  121. data/lib/familia/refinements/dear_json.rb +122 -0
  122. data/lib/familia/refinements/logger_trace.rb +20 -17
  123. data/lib/familia/refinements/stylize_words.rb +65 -0
  124. data/lib/familia/refinements/time_literals.rb +60 -52
  125. data/lib/familia/refinements.rb +2 -1
  126. data/lib/familia/secure_identifier.rb +60 -28
  127. data/lib/familia/settings.rb +83 -7
  128. data/lib/familia/utils.rb +5 -87
  129. data/lib/familia/verifiable_identifier.rb +4 -4
  130. data/lib/familia/version.rb +1 -1
  131. data/lib/familia.rb +72 -15
  132. data/lib/middleware/database_middleware.rb +56 -14
  133. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  134. data/try/configuration/scenarios_try.rb +1 -1
  135. data/try/connection/fiber_context_preservation_try.rb +250 -0
  136. data/try/connection/handler_constraints_try.rb +59 -0
  137. data/try/connection/operation_mode_guards_try.rb +208 -0
  138. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  139. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  140. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  141. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  142. data/try/connection/transaction_mode_strict_try.rb +98 -0
  143. data/try/connection/transaction_mode_warn_try.rb +131 -0
  144. data/try/connection/transaction_modes_try.rb +249 -0
  145. data/try/core/autoloader_try.rb +129 -11
  146. data/try/core/connection_try.rb +7 -7
  147. data/try/core/conventional_inheritance_try.rb +130 -0
  148. data/try/core/create_method_try.rb +15 -23
  149. data/try/core/database_consistency_try.rb +10 -10
  150. data/try/core/errors_try.rb +8 -11
  151. data/try/core/familia_extended_try.rb +2 -2
  152. data/try/core/familia_members_methods_try.rb +76 -0
  153. data/try/core/isolated_dbclient_try.rb +165 -0
  154. data/try/core/middleware_try.rb +16 -16
  155. data/try/core/persistence_operations_try.rb +4 -4
  156. data/try/core/pools_try.rb +42 -26
  157. data/try/core/secure_identifier_try.rb +28 -24
  158. data/try/core/time_utils_try.rb +10 -10
  159. data/try/core/tools_try.rb +1 -1
  160. data/try/core/utils_try.rb +2 -2
  161. data/try/data_types/boolean_try.rb +4 -4
  162. data/try/data_types/datatype_base_try.rb +0 -2
  163. data/try/data_types/list_try.rb +10 -10
  164. data/try/data_types/sorted_set_try.rb +5 -5
  165. data/try/data_types/string_try.rb +12 -12
  166. data/try/data_types/unsortedset_try.rb +33 -0
  167. data/try/debugging/cache_behavior_tracer.rb +7 -7
  168. data/try/debugging/debug_aad_process.rb +1 -1
  169. data/try/debugging/debug_concealed_internal.rb +1 -1
  170. data/try/debugging/debug_cross_context.rb +1 -1
  171. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  172. data/try/debugging/encryption_method_tracer.rb +10 -10
  173. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  174. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  175. data/try/encryption/config_persistence_try.rb +2 -2
  176. data/try/encryption/encryption_core_try.rb +19 -19
  177. data/try/encryption/instance_variable_scope_try.rb +1 -1
  178. data/try/encryption/module_loading_try.rb +2 -2
  179. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  180. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  181. data/try/encryption/secure_memory_handling_try.rb +1 -1
  182. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  183. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  184. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  185. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  186. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  187. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  188. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  189. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  190. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  191. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  192. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  193. data/try/features/feature_dependencies_try.rb +3 -3
  194. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  195. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  196. data/try/features/quantization/quantization_try.rb +1 -1
  197. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  198. data/try/features/relationships/indexing_try.rb +433 -0
  199. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  200. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  201. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  202. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  203. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  204. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  205. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  206. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  207. data/try/features/relationships/relationships_performance_try.rb +20 -20
  208. data/try/features/relationships/relationships_try.rb +27 -38
  209. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  210. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  211. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  212. data/try/helpers/test_cleanup.rb +86 -0
  213. data/try/helpers/test_helpers.rb +3 -3
  214. data/try/horreum/base_try.rb +3 -2
  215. data/try/horreum/commands_try.rb +1 -1
  216. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  217. data/try/horreum/initialization_try.rb +11 -7
  218. data/try/horreum/relations_try.rb +21 -13
  219. data/try/horreum/serialization_try.rb +12 -11
  220. data/try/integration/cross_component_try.rb +3 -3
  221. data/try/memory/memory_basic_test.rb +1 -1
  222. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  223. data/try/models/customer_safe_dump_try.rb +1 -1
  224. data/try/models/customer_try.rb +8 -10
  225. data/try/models/datatype_base_try.rb +3 -3
  226. data/try/models/familia_object_try.rb +9 -8
  227. data/try/performance/benchmarks_try.rb +2 -2
  228. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  229. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  230. data/try/prototypes/atomic_saves_v4.rb +1 -1
  231. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  232. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  234. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  235. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  236. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  237. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  238. data/try/prototypes/pooling/pool_siege.rb +11 -11
  239. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  240. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  241. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  242. data/try/refinements/logger_trace_methods_try.rb +44 -0
  243. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  244. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  245. metadata +77 -45
  246. data/.rubocop_todo.yml +0 -208
  247. data/docs/connection_pooling.md +0 -192
  248. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  249. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  250. data/docs/guides/Feature-System-Autoloading.md +0 -228
  251. data/docs/guides/Home.md +0 -116
  252. data/docs/guides/Relationships-Guide.md +0 -737
  253. data/docs/guides/relationships-methods.md +0 -266
  254. data/docs/reference/auditing_database_commands.rb +0 -228
  255. data/examples/permissions.rb +0 -240
  256. data/lib/familia/features/autoloadable.rb +0 -113
  257. data/lib/familia/features/relationships/cascading.rb +0 -437
  258. data/lib/familia/features/relationships/membership.rb +0 -497
  259. data/lib/familia/features/relationships/permission_management.rb +0 -264
  260. data/lib/familia/features/relationships/querying.rb +0 -615
  261. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  262. data/lib/familia/features/relationships/tracking.rb +0 -418
  263. data/lib/familia/refinements/snake_case.rb +0 -40
  264. data/lib/familia/validation/command_recorder.rb +0 -336
  265. data/lib/familia/validation/expectations.rb +0 -519
  266. data/lib/familia/validation/validation_helpers.rb +0 -443
  267. data/lib/familia/validation/validator.rb +0 -412
  268. data/lib/familia/validation.rb +0 -140
  269. data/try/data_types/set_try.rb +0 -33
  270. data/try/features/autoloadable/autoloadable_try.rb +0 -61
  271. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  272. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
  273. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  274. data/try/validation/command_validation_try.rb.disabled +0 -207
  275. data/try/validation/performance_validation_try.rb.disabled +0 -324
  276. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -1,40 +0,0 @@
1
- # lib/familia/refinements/snake_case.rb
2
-
3
- module Familia
4
- module Refinements
5
- module SnakeCase
6
- # We refine String rather than Class or Module because this method operates on
7
- # string representations of class names (like those from `Class#name`) rather
8
- # than the class objects themselves. Refining String is safer because it
9
- # limits its scope to only the subset string manipulation contexts where it is
10
- # used.
11
- #
12
- # Appropriate for converting Ruby class names to database table names, config
13
- # keys, part of a path or any other snake_case identifiers. The only situation
14
- # it is not appropriate for is investigating actual snakes.
15
- refine String do
16
- # Converts a string from PascalCase/camelCase to snake_case format.
17
- #
18
- # @return [String] the snake_case version of the string
19
- #
20
- # @example Converting simple CamelCase
21
- # "FirstName".snake_case #=> "first_name"
22
- #
23
- # @example Converting PascalCase with acronyms
24
- # XMLHttpRequest.name.snake_case #=> "xml_http_request"
25
- #
26
- # @example Converting namespaced class names
27
- # "MyApp::UserAccount".snake_case #=> "user_account"
28
- #
29
- # @example Handling mixed case with numbers
30
- # "parseHTML5Document".snake_case #=> "parse_html5_document"
31
- def snake_case
32
- split('::').last
33
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
34
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
35
- .downcase
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,336 +0,0 @@
1
- # lib/familia/validation/command_recorder.rb
2
-
3
- require 'concurrent-ruby'
4
-
5
- module Familia
6
- module Validation
7
- # Enhanced command recorder that captures Redis commands with full context
8
- # for validation purposes. Extends the existing DatabaseLogger functionality
9
- # to provide detailed command tracking including transaction boundaries.
10
- #
11
- # @example Basic usage
12
- # CommandRecorder.start_recording
13
- # # ... perform Redis operations
14
- # commands = CommandRecorder.stop_recording
15
- # puts commands.map(&:to_s)
16
- #
17
- # @example Transaction recording
18
- # CommandRecorder.start_recording
19
- # Familia.transaction do |conn|
20
- # conn.hset("key", "field", "value")
21
- # conn.incr("counter")
22
- # end
23
- # commands = CommandRecorder.stop_recording
24
- # commands.transaction_blocks.length #=> 1
25
- # commands.transaction_blocks.first.commands.length #=> 2
26
- #
27
- module CommandRecorder
28
- extend self
29
-
30
- # Thread-safe recording state
31
- @recording_state = Concurrent::ThreadLocalVar.new { false }
32
- @recorded_commands = Concurrent::ThreadLocalVar.new { CommandSequence.new }
33
- @transaction_stack = Concurrent::ThreadLocalVar.new { [] }
34
- @pipeline_stack = Concurrent::ThreadLocalVar.new { [] }
35
-
36
- # Represents a single Redis command with full context
37
- class RecordedCommand
38
- attr_reader :command, :args, :result, :timestamp, :duration_us, :context, :command_type
39
-
40
- def initialize(command:, args:, result:, timestamp:, duration_us:, context: {})
41
- @command = command.to_s.upcase
42
- @args = args.dup.freeze
43
- @result = result
44
- @timestamp = timestamp
45
- @duration_us = duration_us
46
- @context = context.dup.freeze
47
- @command_type = determine_command_type
48
- end
49
-
50
- def to_s
51
- args_str = @args.map(&:inspect).join(', ')
52
- "#{@command}(#{args_str})"
53
- end
54
-
55
- def to_h
56
- {
57
- command: @command,
58
- args: @args,
59
- result: @result,
60
- timestamp: @timestamp,
61
- duration_us: @duration_us,
62
- context: @context,
63
- command_type: @command_type
64
- }
65
- end
66
-
67
- def transaction_command?
68
- %w[MULTI EXEC DISCARD].include?(@command)
69
- end
70
-
71
- def pipeline_command?
72
- @context[:pipeline] == true
73
- end
74
-
75
- def atomic_command?
76
- @context[:transaction] == true
77
- end
78
-
79
- private
80
-
81
- def determine_command_type
82
- case @command
83
- when 'MULTI', 'EXEC', 'DISCARD'
84
- :transaction_control
85
- when 'PIPELINE'
86
- :pipeline_control
87
- when /^H(GET|SET|DEL|EXISTS|KEYS|LEN|MGET|MSET)/
88
- :hash
89
- when /^(L|R)(PUSH|POP|LEN|RANGE|INDEX|SET|REM)/
90
- :list
91
- when /^S(ADD|REM|MEMBERS|CARD|ISMEMBER|DIFF|INTER|UNION)/
92
- :set
93
- when /^Z(ADD|REM|RANGE|SCORE|CARD|COUNT|RANK|INCR)/
94
- :sorted_set
95
- when /^(GET|SET|DEL|EXISTS|EXPIRE|TTL|TYPE|INCR|DECR)/
96
- :string
97
- else
98
- :other
99
- end
100
- end
101
- end
102
-
103
- # Represents a sequence of Redis commands with transaction boundaries
104
- class CommandSequence
105
- attr_reader :commands, :transaction_blocks, :pipeline_blocks
106
-
107
- def initialize
108
- @commands = []
109
- @transaction_blocks = []
110
- @pipeline_blocks = []
111
- end
112
-
113
- def add_command(recorded_command)
114
- @commands << recorded_command
115
- end
116
-
117
- def start_transaction(context = {})
118
- @transaction_blocks << TransactionBlock.new(context)
119
- end
120
-
121
- def end_transaction
122
- return unless current_transaction
123
-
124
- current_transaction.finalize(@commands)
125
- end
126
-
127
- def start_pipeline(context = {})
128
- @pipeline_blocks << PipelineBlock.new(context)
129
- end
130
-
131
- def end_pipeline
132
- return unless current_pipeline
133
-
134
- current_pipeline.finalize(@commands)
135
- end
136
-
137
- def current_transaction
138
- @transaction_blocks.last
139
- end
140
-
141
- def current_pipeline
142
- @pipeline_blocks.last
143
- end
144
-
145
- def command_count
146
- @commands.length
147
- end
148
-
149
- def transaction_count
150
- @transaction_blocks.length
151
- end
152
-
153
- def pipeline_count
154
- @pipeline_blocks.length
155
- end
156
-
157
- def to_a
158
- @commands
159
- end
160
-
161
- def clear
162
- @commands.clear
163
- @transaction_blocks.clear
164
- @pipeline_blocks.clear
165
- end
166
- end
167
-
168
- # Represents a transaction block (MULTI/EXEC)
169
- class TransactionBlock
170
- attr_reader :start_index, :end_index, :commands, :context, :started_at
171
-
172
- def initialize(context = {})
173
- @context = context
174
- @started_at = Time.now
175
- @start_index = nil
176
- @end_index = nil
177
- @commands = []
178
- end
179
-
180
- def finalize(all_commands)
181
- # Find MULTI and EXEC commands
182
- multi_index = all_commands.rindex { |cmd| cmd.command == 'MULTI' }
183
- exec_index = all_commands.rindex { |cmd| cmd.command == 'EXEC' }
184
-
185
- return unless multi_index && exec_index && exec_index > multi_index
186
-
187
- @start_index = multi_index
188
- @end_index = exec_index
189
- @commands = all_commands[(multi_index + 1)...exec_index]
190
- end
191
-
192
- def valid?
193
- @start_index && @end_index && @commands.any?
194
- end
195
-
196
- def command_count
197
- @commands.length
198
- end
199
- end
200
-
201
- # Represents a pipeline block
202
- class PipelineBlock
203
- attr_reader :commands, :context, :started_at
204
-
205
- def initialize(context = {})
206
- @context = context
207
- @started_at = Time.now
208
- @commands = []
209
- end
210
-
211
- def finalize(all_commands)
212
- # Pipeline commands are those executed within pipeline context
213
- @commands = all_commands.select(&:pipeline_command?)
214
- end
215
-
216
- def command_count
217
- @commands.length
218
- end
219
- end
220
-
221
- # Start recording Redis commands for the current thread
222
- def start_recording
223
- @recording_state.value = true
224
- @recorded_commands.value = CommandSequence.new
225
- @transaction_stack.value = []
226
- @pipeline_stack.value = []
227
- end
228
-
229
- # Stop recording and return the recorded command sequence
230
- def stop_recording
231
- @recording_state.value = false
232
- sequence = @recorded_commands.value
233
- @recorded_commands.value = CommandSequence.new
234
- sequence
235
- end
236
-
237
- # Check if currently recording
238
- def recording?
239
- @recording_state.value == true
240
- end
241
-
242
- # Record a Redis command with full context
243
- def record_command(command:, args:, result:, timestamp:, duration_us:, context: {})
244
- return unless recording?
245
-
246
- # Enhance context with transaction/pipeline state
247
- enhanced_context = context.merge(
248
- transaction: in_transaction?,
249
- pipeline: in_pipeline?,
250
- transaction_depth: transaction_depth,
251
- pipeline_depth: pipeline_depth
252
- )
253
-
254
- recorded_cmd = RecordedCommand.new(
255
- command: command,
256
- args: args,
257
- result: result,
258
- timestamp: timestamp,
259
- duration_us: duration_us,
260
- context: enhanced_context
261
- )
262
-
263
- sequence = @recorded_commands.value
264
- sequence.add_command(recorded_cmd)
265
-
266
- # Handle transaction boundaries
267
- case recorded_cmd.command
268
- when 'MULTI'
269
- sequence.start_transaction(enhanced_context)
270
- @transaction_stack.value.push(Time.now)
271
- when 'EXEC', 'DISCARD'
272
- sequence.end_transaction if sequence.current_transaction
273
- @transaction_stack.value.pop
274
- end
275
- end
276
-
277
- # Check if we're currently in a transaction
278
- def in_transaction?
279
- @transaction_stack.value.any?
280
- end
281
-
282
- # Check if we're currently in a pipeline
283
- def in_pipeline?
284
- @pipeline_stack.value.any?
285
- end
286
-
287
- # Get current transaction nesting depth
288
- def transaction_depth
289
- @transaction_stack.value.length
290
- end
291
-
292
- # Get current pipeline nesting depth
293
- def pipeline_depth
294
- @pipeline_stack.value.length
295
- end
296
-
297
- # Get the current command sequence (for inspection during recording)
298
- def current_sequence
299
- @recorded_commands.value
300
- end
301
-
302
- # Clear all recorded data
303
- def clear
304
- @recorded_commands.value.clear
305
- end
306
-
307
- # Enhanced middleware that integrates with DatabaseLogger
308
- module Middleware
309
- def self.call(command, config)
310
- return yield unless CommandRecorder.recording?
311
-
312
- timestamp = Time.now
313
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
314
-
315
- result = yield
316
-
317
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start_time
318
-
319
- CommandRecorder.record_command(
320
- command: command[0],
321
- args: command[1..-1],
322
- result: result,
323
- timestamp: timestamp,
324
- duration_us: duration,
325
- context: {
326
- config: config,
327
- thread_id: Thread.current.object_id
328
- }
329
- )
330
-
331
- result
332
- end
333
- end
334
- end
335
- end
336
- end