familia 2.0.0.pre6 → 2.0.0.pre8
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.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -13
- data/Gemfile +2 -2
- data/Gemfile.lock +3 -3
- data/README.md +35 -0
- data/docs/wiki/Feature-System-Guide.md +36 -20
- data/docs/wiki/Home.md +30 -20
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type.rb +7 -4
- data/lib/familia/features/encrypted_fields/concealed_string.rb +21 -23
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
- data/lib/familia/features/external_identifiers.rb +111 -0
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
- data/lib/familia/features/object_identifiers.rb +194 -0
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +437 -0
- data/lib/familia/features/relationships/indexing.rb +369 -0
- data/lib/familia/features/relationships/membership.rb +502 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +615 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +440 -0
- data/lib/familia/features/relationships/tracking.rb +378 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/transient_fields.rb +190 -10
- data/lib/familia/features.rb +18 -14
- data/lib/familia/horreum/core/serialization.rb +2 -5
- data/lib/familia/horreum/subclass/definition.rb +35 -1
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -3
- data/try/core/errors_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +3 -1
- data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +3 -0
- data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +1 -0
- data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
- data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
- data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
- data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
- data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
- data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
- data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
- data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
- data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
- data/try/features/relationships/categorical_permissions_try.rb +515 -0
- data/try/features/relationships/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships/relationships_performance_try.rb +420 -0
- data/try/features/relationships/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships/relationships_try.rb +237 -0
- data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
- data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +4 -1
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
- data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
- data/try/helpers/test_helpers.rb +1 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +2 -0
- data/try/horreum/relations_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +62 -27
- data/docs/wiki/RelatableObjects-Guide.md +0 -563
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/try/features/relatable_objects_try.rb +0 -220
- /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/universal_serialization_safety_try.rb +0 -0
data/lib/familia/features.rb
CHANGED
@@ -1,17 +1,15 @@
|
|
1
1
|
# lib/familia/features.rb
|
2
2
|
|
3
3
|
module Familia
|
4
|
-
|
5
4
|
FeatureDefinition = Data.define(:name, :depends_on)
|
6
5
|
|
7
6
|
# Familia::Features
|
8
7
|
#
|
9
8
|
module Features
|
10
|
-
|
11
9
|
@features_enabled = nil
|
12
10
|
attr_reader :features_enabled
|
13
11
|
|
14
|
-
def feature(feature_name = nil)
|
12
|
+
def feature(feature_name = nil, **options)
|
15
13
|
@features_enabled ||= []
|
16
14
|
|
17
15
|
return features_enabled if feature_name.nil?
|
@@ -28,22 +26,28 @@ module Familia
|
|
28
26
|
return
|
29
27
|
end
|
30
28
|
|
31
|
-
if Familia.debug?
|
32
|
-
|
29
|
+
Familia.trace :FEATURE, nil, "#{self} includes #{feature_name.inspect}", caller(1..1) if Familia.debug?
|
30
|
+
|
31
|
+
# Check dependencies and raise error if missing
|
32
|
+
feature_def = Familia::Base.feature_definitions[feature_name]
|
33
|
+
if feature_def&.depends_on&.any?
|
34
|
+
missing = feature_def.depends_on - features_enabled
|
35
|
+
if missing.any?
|
36
|
+
raise Familia::Problem,
|
37
|
+
"Feature #{feature_name} requires missing dependencies: #{missing.join(', ')}"
|
38
|
+
end
|
33
39
|
end
|
34
40
|
|
35
41
|
# Add it to the list available features_enabled for Familia::Base classes.
|
36
42
|
features_enabled << feature_name
|
37
43
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
feature_def = Familia::Base.feature_definitions[feature_name]
|
42
|
-
if feature_def&.depends_on&.any?
|
43
|
-
missing = feature_def.depends_on - features_enabled
|
44
|
-
raise Familia::Problem, "#{feature_name} requires: #{missing.join(', ')}" if missing.any?
|
44
|
+
# Store feature options if any were provided using the new pattern
|
45
|
+
if options.any?
|
46
|
+
add_feature_options(feature_name, **options)
|
45
47
|
end
|
46
48
|
|
49
|
+
klass = Familia::Base.features_available[feature_name]
|
50
|
+
|
47
51
|
# Extend the Familia::Base subclass (e.g. Customer) with the feature module
|
48
52
|
include klass
|
49
53
|
|
@@ -58,15 +62,15 @@ module Familia
|
|
58
62
|
# We'd need to extend the DataType instances for each Horreum subclass. That
|
59
63
|
# avoids it getting included multiple times per DataType
|
60
64
|
end
|
61
|
-
|
62
65
|
end
|
63
|
-
|
64
66
|
end
|
65
67
|
|
66
68
|
# Load all feature files from the features directory
|
67
69
|
features_dir = File.join(__dir__, 'features')
|
70
|
+
Familia.ld "[DEBUG] Loading features from #{features_dir}"
|
68
71
|
if Dir.exist?(features_dir)
|
69
72
|
Dir.glob(File.join(features_dir, '*.rb')).each do |feature_file|
|
73
|
+
Familia.ld "[DEBUG] Loading feature #{feature_file}"
|
70
74
|
require_relative feature_file
|
71
75
|
end
|
72
76
|
end
|
@@ -135,7 +135,7 @@ module Familia
|
|
135
135
|
multi.hmset(dbkey, to_h_for_storage)
|
136
136
|
end
|
137
137
|
|
138
|
-
result.is_a?(Array)
|
138
|
+
result.is_a?(Array) # transaction succeeded
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
@@ -463,9 +463,7 @@ module Familia
|
|
463
463
|
#
|
464
464
|
def serialize_value(val)
|
465
465
|
# Security: Handle ConcealedString safely - extract encrypted data for storage
|
466
|
-
if val.respond_to?(:encrypted_value)
|
467
|
-
return val.encrypted_value
|
468
|
-
end
|
466
|
+
return val.encrypted_value if val.respond_to?(:encrypted_value)
|
469
467
|
|
470
468
|
prepared = Familia.distinguisher(val, strict_values: false)
|
471
469
|
|
@@ -530,6 +528,5 @@ module Familia
|
|
530
528
|
end
|
531
529
|
end
|
532
530
|
end
|
533
|
-
|
534
531
|
end
|
535
532
|
end
|
@@ -169,7 +169,7 @@ module Familia
|
|
169
169
|
@related_fields
|
170
170
|
end
|
171
171
|
|
172
|
-
def
|
172
|
+
def relations?
|
173
173
|
@has_relations ||= false
|
174
174
|
end
|
175
175
|
|
@@ -235,6 +235,40 @@ module Familia
|
|
235
235
|
field_types[field_type.name] = field_type
|
236
236
|
end
|
237
237
|
|
238
|
+
# Get feature options for a specific feature or all features
|
239
|
+
#
|
240
|
+
# @param feature_name [Symbol, nil] The feature name to get options for
|
241
|
+
# @return [Hash] The options hash for the feature, or empty hash if none
|
242
|
+
#
|
243
|
+
def feature_options(feature_name = nil)
|
244
|
+
@feature_options ||= {}
|
245
|
+
return @feature_options if feature_name.nil?
|
246
|
+
|
247
|
+
@feature_options[feature_name.to_sym] || {}
|
248
|
+
end
|
249
|
+
|
250
|
+
# Add feature options for a specific feature
|
251
|
+
#
|
252
|
+
# This method provides a clean way for features to set their default options
|
253
|
+
# without worrying about initialization state. Similar to register_field_type
|
254
|
+
# for field types.
|
255
|
+
#
|
256
|
+
# @param feature_name [Symbol] The feature name
|
257
|
+
# @param options [Hash] The options to add/merge
|
258
|
+
# @return [Hash] The updated options for the feature
|
259
|
+
#
|
260
|
+
def add_feature_options(feature_name, **options)
|
261
|
+
@feature_options ||= {}
|
262
|
+
@feature_options[feature_name.to_sym] ||= {}
|
263
|
+
|
264
|
+
# Only set defaults for options that don't already exist
|
265
|
+
options.each do |key, value|
|
266
|
+
@feature_options[feature_name.to_sym][key] ||= value
|
267
|
+
end
|
268
|
+
|
269
|
+
@feature_options[feature_name.to_sym]
|
270
|
+
end
|
271
|
+
|
238
272
|
# Create and register a transient field type
|
239
273
|
#
|
240
274
|
# @param name [Symbol] The field name
|
@@ -0,0 +1,336 @@
|
|
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
|