familia 2.0.0.pre5 → 2.0.0.pre7
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 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -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/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- 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 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- 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 +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- 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 +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_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 +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,412 @@
|
|
1
|
+
# lib/familia/validation/validator.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Validation
|
5
|
+
# Main validation engine that orchestrates command recording and validation.
|
6
|
+
# Provides high-level interface for validating Redis operations against
|
7
|
+
# expectations with detailed reporting and atomicity verification.
|
8
|
+
#
|
9
|
+
# @example Basic validation
|
10
|
+
# validator = Validator.new
|
11
|
+
#
|
12
|
+
# result = validator.validate do |expect|
|
13
|
+
# # Define expectations
|
14
|
+
# expect.hset("user:123", "name", "John")
|
15
|
+
# .incr("counter")
|
16
|
+
#
|
17
|
+
# # Execute code under test
|
18
|
+
# user = User.new(id: "123", name: "John")
|
19
|
+
# user.save
|
20
|
+
# Counter.increment
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# puts result.valid? ? "PASS" : "FAIL"
|
24
|
+
# puts result.detailed_report
|
25
|
+
#
|
26
|
+
# @example Transaction validation
|
27
|
+
# validator = Validator.new
|
28
|
+
#
|
29
|
+
# result = validator.validate do |expect|
|
30
|
+
# expect.transaction do |tx|
|
31
|
+
# tx.hset("user:123", "name", "John")
|
32
|
+
# .incr("counter")
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# # Code should execute atomically
|
36
|
+
# Familia.transaction do |conn|
|
37
|
+
# conn.hset("user:123", "name", "John")
|
38
|
+
# conn.incr("counter")
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
class Validator
|
43
|
+
attr_reader :options
|
44
|
+
|
45
|
+
def initialize(options = {})
|
46
|
+
@options = {
|
47
|
+
auto_register_middleware: true,
|
48
|
+
strict_atomicity: true,
|
49
|
+
performance_tracking: true,
|
50
|
+
command_filtering: :all # :all, :familia_only, :custom
|
51
|
+
}.merge(options)
|
52
|
+
|
53
|
+
register_middleware if @options[:auto_register_middleware]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Main validation method - records commands and validates against expectations
|
57
|
+
def validate(&block)
|
58
|
+
raise ArgumentError, "Block required for validation" unless block_given?
|
59
|
+
|
60
|
+
expectations = nil
|
61
|
+
command_sequence = nil
|
62
|
+
|
63
|
+
begin
|
64
|
+
# Start recording commands
|
65
|
+
CommandRecorder.start_recording
|
66
|
+
register_middleware_if_needed
|
67
|
+
|
68
|
+
# Execute the validation block
|
69
|
+
expectations = CommandExpectations.new
|
70
|
+
block.call(expectations)
|
71
|
+
|
72
|
+
# Get recorded commands
|
73
|
+
command_sequence = CommandRecorder.stop_recording
|
74
|
+
|
75
|
+
rescue => e
|
76
|
+
CommandRecorder.stop_recording
|
77
|
+
raise ValidationError, "Validation failed with error: #{e.message}"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Validate and return result
|
81
|
+
result = expectations.validate(command_sequence)
|
82
|
+
|
83
|
+
if @options[:performance_tracking]
|
84
|
+
add_performance_metrics(result, command_sequence)
|
85
|
+
end
|
86
|
+
|
87
|
+
if @options[:strict_atomicity]
|
88
|
+
apply_atomicity_validation(result, command_sequence)
|
89
|
+
end
|
90
|
+
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
# Validate that specific code executes expected Redis commands
|
95
|
+
def validate_execution(expectations_block, execution_block)
|
96
|
+
expectations = CommandExpectations.new
|
97
|
+
expectations_block.call(expectations)
|
98
|
+
|
99
|
+
CommandRecorder.start_recording
|
100
|
+
register_middleware_if_needed
|
101
|
+
|
102
|
+
begin
|
103
|
+
execution_block.call
|
104
|
+
command_sequence = CommandRecorder.stop_recording
|
105
|
+
rescue => e
|
106
|
+
CommandRecorder.stop_recording
|
107
|
+
raise ValidationError, "Execution failed: #{e.message}"
|
108
|
+
end
|
109
|
+
|
110
|
+
result = expectations.validate(command_sequence)
|
111
|
+
|
112
|
+
if @options[:performance_tracking]
|
113
|
+
add_performance_metrics(result, command_sequence)
|
114
|
+
end
|
115
|
+
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
# Validate that code executes atomically (within transactions)
|
120
|
+
def validate_atomicity(&block)
|
121
|
+
CommandRecorder.start_recording
|
122
|
+
register_middleware_if_needed
|
123
|
+
|
124
|
+
begin
|
125
|
+
block.call
|
126
|
+
command_sequence = CommandRecorder.stop_recording
|
127
|
+
rescue => e
|
128
|
+
CommandRecorder.stop_recording
|
129
|
+
raise ValidationError, "Atomicity validation failed: #{e.message}"
|
130
|
+
end
|
131
|
+
|
132
|
+
AtomicityValidator.new(command_sequence, @options).validate
|
133
|
+
end
|
134
|
+
|
135
|
+
# Assert that specific commands were executed
|
136
|
+
def assert_commands_executed(expected_commands, actual_commands = nil)
|
137
|
+
actual_commands ||= get_last_recorded_sequence
|
138
|
+
|
139
|
+
expectations = CommandExpectations.new
|
140
|
+
expected_commands.each do |cmd_spec|
|
141
|
+
case cmd_spec
|
142
|
+
when Array
|
143
|
+
expectations.command(cmd_spec[0], *cmd_spec[1..-1])
|
144
|
+
when Hash
|
145
|
+
cmd_spec.each do |cmd, args|
|
146
|
+
expectations.command(cmd, *Array(args))
|
147
|
+
end
|
148
|
+
when String
|
149
|
+
expectations.match_pattern(cmd_spec)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
expectations.validate(actual_commands)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Performance analysis of recorded commands
|
157
|
+
def analyze_performance(command_sequence)
|
158
|
+
PerformanceAnalyzer.new(command_sequence).analyze
|
159
|
+
end
|
160
|
+
|
161
|
+
# Capture and return Redis commands without validation
|
162
|
+
def capture_redis_commands(&block)
|
163
|
+
CommandRecorder.start_recording
|
164
|
+
register_middleware_if_needed
|
165
|
+
block.call if block_given?
|
166
|
+
CommandRecorder.stop_recording
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def register_middleware
|
172
|
+
return if @middleware_registered
|
173
|
+
|
174
|
+
# Register our command recording middleware using the same pattern as connection.rb
|
175
|
+
if defined?(RedisClient)
|
176
|
+
RedisClient.register(CommandRecorder::Middleware)
|
177
|
+
end
|
178
|
+
@middleware_registered = true
|
179
|
+
end
|
180
|
+
|
181
|
+
def register_middleware_if_needed
|
182
|
+
register_middleware unless @middleware_registered
|
183
|
+
end
|
184
|
+
|
185
|
+
def get_last_recorded_sequence
|
186
|
+
CommandRecorder.current_sequence
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_performance_metrics(result, command_sequence)
|
190
|
+
analyzer = PerformanceAnalyzer.new(command_sequence)
|
191
|
+
metrics = analyzer.analyze
|
192
|
+
|
193
|
+
result.instance_variable_set(:@performance_metrics, metrics)
|
194
|
+
|
195
|
+
# Add singleton methods to result
|
196
|
+
result.define_singleton_method(:performance_metrics) { @performance_metrics }
|
197
|
+
result.define_singleton_method(:total_duration_ms) { @performance_metrics[:total_duration_ms] }
|
198
|
+
result.define_singleton_method(:command_efficiency) { @performance_metrics[:efficiency_score] }
|
199
|
+
end
|
200
|
+
|
201
|
+
def apply_atomicity_validation(result, command_sequence)
|
202
|
+
atomicity_validator = AtomicityValidator.new(command_sequence, @options)
|
203
|
+
atomicity_result = atomicity_validator.validate
|
204
|
+
|
205
|
+
unless atomicity_result.valid?
|
206
|
+
result.instance_variable_get(:@errors).concat(atomicity_result.error_messages)
|
207
|
+
result.instance_variable_get(:@warnings).concat(atomicity_result.warning_messages)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Validates that operations that should be atomic actually execute within transactions
|
213
|
+
class AtomicityValidator
|
214
|
+
attr_reader :command_sequence, :options
|
215
|
+
|
216
|
+
def initialize(command_sequence, options = {})
|
217
|
+
@command_sequence = command_sequence
|
218
|
+
@options = options
|
219
|
+
@errors = []
|
220
|
+
@warnings = []
|
221
|
+
end
|
222
|
+
|
223
|
+
def validate
|
224
|
+
check_transaction_boundaries
|
225
|
+
check_orphaned_commands
|
226
|
+
check_nested_transactions
|
227
|
+
|
228
|
+
ValidationResult.new(nil, @command_sequence).tap do |result|
|
229
|
+
result.instance_variable_set(:@errors, @errors)
|
230
|
+
result.instance_variable_set(:@warnings, @warnings)
|
231
|
+
result.instance_variable_set(:@valid, @errors.empty?)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def check_transaction_boundaries
|
238
|
+
@command_sequence.transaction_blocks.each_with_index do |tx_block, i|
|
239
|
+
unless tx_block.valid?
|
240
|
+
@errors << "Transaction block #{i + 1} is invalid (missing MULTI or EXEC)"
|
241
|
+
end
|
242
|
+
|
243
|
+
if tx_block.command_count == 0
|
244
|
+
@warnings << "Transaction block #{i + 1} contains no commands"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def check_orphaned_commands
|
250
|
+
# Commands that should be in transactions but aren't
|
251
|
+
orphaned_commands = @command_sequence.commands.select do |cmd|
|
252
|
+
!cmd.atomic_command? && should_be_atomic?(cmd)
|
253
|
+
end
|
254
|
+
|
255
|
+
orphaned_commands.each do |cmd|
|
256
|
+
@warnings << "Command #{cmd} should be executed atomically but was not in a transaction"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def check_nested_transactions
|
261
|
+
transaction_depth = 0
|
262
|
+
|
263
|
+
@command_sequence.commands.each do |cmd|
|
264
|
+
case cmd.command
|
265
|
+
when 'MULTI'
|
266
|
+
transaction_depth += 1
|
267
|
+
if transaction_depth > 1
|
268
|
+
@errors << "Nested transactions detected - Redis does not support nested MULTI/EXEC"
|
269
|
+
end
|
270
|
+
when 'EXEC', 'DISCARD'
|
271
|
+
transaction_depth -= 1
|
272
|
+
if transaction_depth < 0
|
273
|
+
@errors << "EXEC/DISCARD without matching MULTI command"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
if transaction_depth > 0
|
279
|
+
@errors << "Unclosed transaction detected - MULTI without matching EXEC/DISCARD"
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def should_be_atomic?(cmd)
|
284
|
+
# Define patterns for commands that should typically be atomic
|
285
|
+
atomic_patterns = [
|
286
|
+
/^HSET.*counter/i, # Counter updates
|
287
|
+
/^INCR/i, # Increments
|
288
|
+
/^DECR/i, # Decrements
|
289
|
+
/batch_update/i # Batch operations
|
290
|
+
]
|
291
|
+
|
292
|
+
atomic_patterns.any? { |pattern| cmd.to_s.match?(pattern) }
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Analyzes performance characteristics of recorded commands
|
297
|
+
class PerformanceAnalyzer
|
298
|
+
attr_reader :command_sequence
|
299
|
+
|
300
|
+
def initialize(command_sequence)
|
301
|
+
@command_sequence = command_sequence
|
302
|
+
end
|
303
|
+
|
304
|
+
def analyze
|
305
|
+
{
|
306
|
+
total_commands: @command_sequence.command_count,
|
307
|
+
total_duration_ms: total_duration_ms,
|
308
|
+
average_command_time_us: average_command_time,
|
309
|
+
slowest_commands: slowest_commands(5),
|
310
|
+
command_type_breakdown: command_type_breakdown,
|
311
|
+
transaction_efficiency: transaction_efficiency,
|
312
|
+
potential_n_plus_one: detect_n_plus_one_patterns,
|
313
|
+
efficiency_score: calculate_efficiency_score
|
314
|
+
}
|
315
|
+
end
|
316
|
+
|
317
|
+
private
|
318
|
+
|
319
|
+
def total_duration_ms
|
320
|
+
@command_sequence.commands.sum(&:duration_us) / 1000.0
|
321
|
+
end
|
322
|
+
|
323
|
+
def average_command_time
|
324
|
+
return 0 if @command_sequence.command_count == 0
|
325
|
+
|
326
|
+
total_time = @command_sequence.commands.sum(&:duration_us)
|
327
|
+
total_time / @command_sequence.command_count
|
328
|
+
end
|
329
|
+
|
330
|
+
def slowest_commands(limit = 5)
|
331
|
+
@command_sequence.commands
|
332
|
+
.sort_by(&:duration_us)
|
333
|
+
.reverse
|
334
|
+
.first(limit)
|
335
|
+
.map { |cmd| { command: cmd.to_s, duration_us: cmd.duration_us } }
|
336
|
+
end
|
337
|
+
|
338
|
+
def command_type_breakdown
|
339
|
+
@command_sequence.commands
|
340
|
+
.group_by(&:command_type)
|
341
|
+
.transform_values(&:count)
|
342
|
+
end
|
343
|
+
|
344
|
+
def transaction_efficiency
|
345
|
+
return { score: 1.0, details: "No transactions" } if @command_sequence.transaction_count == 0
|
346
|
+
|
347
|
+
total_tx_commands = @command_sequence.transaction_blocks.sum(&:command_count)
|
348
|
+
total_commands = @command_sequence.command_count
|
349
|
+
tx_overhead = @command_sequence.transaction_count * 2 # MULTI + EXEC
|
350
|
+
|
351
|
+
efficiency = total_tx_commands.to_f / (total_tx_commands + tx_overhead)
|
352
|
+
|
353
|
+
{
|
354
|
+
score: efficiency,
|
355
|
+
total_transaction_commands: total_tx_commands,
|
356
|
+
transaction_overhead: tx_overhead,
|
357
|
+
details: "#{@command_sequence.transaction_count} transactions with #{total_tx_commands} commands"
|
358
|
+
}
|
359
|
+
end
|
360
|
+
|
361
|
+
def detect_n_plus_one_patterns
|
362
|
+
patterns = []
|
363
|
+
|
364
|
+
# Look for repeated similar commands that could be batched
|
365
|
+
command_groups = @command_sequence.commands.group_by { |cmd| cmd.command }
|
366
|
+
|
367
|
+
command_groups.each do |command, commands|
|
368
|
+
next unless commands.length > 3 # Threshold for N+1 detection
|
369
|
+
|
370
|
+
# Check if commands are similar (same command, similar keys)
|
371
|
+
if similar_commands?(commands)
|
372
|
+
patterns << {
|
373
|
+
command: command,
|
374
|
+
count: commands.length,
|
375
|
+
suggestion: "Consider batching #{command} operations"
|
376
|
+
}
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
patterns
|
381
|
+
end
|
382
|
+
|
383
|
+
def similar_commands?(commands)
|
384
|
+
return false if commands.length < 2
|
385
|
+
|
386
|
+
first_cmd = commands.first
|
387
|
+
commands.drop(1).all? do |cmd|
|
388
|
+
cmd.command == first_cmd.command &&
|
389
|
+
cmd.args.length == first_cmd.args.length
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def calculate_efficiency_score
|
394
|
+
base_score = 100.0
|
395
|
+
|
396
|
+
# Penalize for potential N+1 patterns
|
397
|
+
n_plus_one_penalty = detect_n_plus_one_patterns.sum { |pattern| pattern[:count] * 2 }
|
398
|
+
|
399
|
+
# Penalize for non-atomic operations that should be atomic
|
400
|
+
non_atomic_penalty = @command_sequence.commands.count { |cmd| !cmd.atomic_command? } * 1
|
401
|
+
|
402
|
+
# Bonus for using transactions appropriately
|
403
|
+
transaction_bonus = @command_sequence.transaction_count > 0 ? 10 : 0
|
404
|
+
|
405
|
+
[base_score - n_plus_one_penalty - non_atomic_penalty + transaction_bonus, 0].max
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# Custom error class for validation failures
|
410
|
+
class ValidationError < StandardError; end
|
411
|
+
end
|
412
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# lib/familia/validation.rb
|
2
|
+
|
3
|
+
# Redis Command Validation Framework for Familia
|
4
|
+
#
|
5
|
+
# Provides comprehensive validation of Redis operations to ensure commands
|
6
|
+
# execute exactly as expected, with particular focus on atomicity verification
|
7
|
+
# for transaction-based operations.
|
8
|
+
#
|
9
|
+
# @example Basic command validation
|
10
|
+
# include Familia::Validation
|
11
|
+
#
|
12
|
+
# validator = Validator.new
|
13
|
+
# result = validator.validate do |expect|
|
14
|
+
# expect.hset("user:123", "name", "John")
|
15
|
+
# .incr("counter")
|
16
|
+
#
|
17
|
+
# # Execute code that should perform these operations
|
18
|
+
# user = User.new(id: "123", name: "John")
|
19
|
+
# user.save
|
20
|
+
# Counter.increment
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# puts result.valid? ? "PASS" : "FAIL"
|
24
|
+
#
|
25
|
+
# @example Using test helpers in try files
|
26
|
+
# require_relative 'lib/familia/validation'
|
27
|
+
# extend Familia::Validation::TestHelpers
|
28
|
+
#
|
29
|
+
# ## User save executes expected Redis commands
|
30
|
+
# user = TestUser.new(id: "123", name: "John")
|
31
|
+
# assert_redis_commands do |expect|
|
32
|
+
# expect.hset("testuser:123:object", "name", "John")
|
33
|
+
# user.save
|
34
|
+
# end
|
35
|
+
# #=> true
|
36
|
+
#
|
37
|
+
# @example Transaction atomicity validation
|
38
|
+
# extend Familia::Validation::TestHelpers
|
39
|
+
#
|
40
|
+
# ## Transfer should be atomic
|
41
|
+
# assert_atomic_operation do |expect|
|
42
|
+
# expect.transaction do |tx|
|
43
|
+
# tx.hset("account:123", "balance", "500")
|
44
|
+
# .hset("account:456", "balance", "1500")
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# transfer_funds(from: "123", to: "456", amount: 500)
|
48
|
+
# end
|
49
|
+
# #=> true
|
50
|
+
|
51
|
+
require_relative 'validation/command_recorder'
|
52
|
+
require_relative 'validation/expectations'
|
53
|
+
require_relative 'validation/validator'
|
54
|
+
require_relative 'validation/test_helpers'
|
55
|
+
|
56
|
+
module Familia
|
57
|
+
module Validation
|
58
|
+
# Quick access to main validator
|
59
|
+
def self.validator(options = {})
|
60
|
+
Validator.new(options)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Validate a block of code against expected Redis commands
|
64
|
+
def self.validate(options = {}, &block)
|
65
|
+
validator(options).validate(&block)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Validate that code executes atomically
|
69
|
+
def self.validate_atomicity(options = {}, &block)
|
70
|
+
validator(options).validate_atomicity(&block)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Capture Redis commands without validation
|
74
|
+
def self.capture_commands(&block)
|
75
|
+
CommandRecorder.start_recording
|
76
|
+
block.call if block_given?
|
77
|
+
CommandRecorder.stop_recording
|
78
|
+
end
|
79
|
+
|
80
|
+
# Quick performance analysis
|
81
|
+
def self.analyze_performance(&block)
|
82
|
+
commands = capture_commands(&block)
|
83
|
+
PerformanceAnalyzer.new(commands).analyze
|
84
|
+
end
|
85
|
+
|
86
|
+
# Register validation middleware with Redis client
|
87
|
+
def self.register_middleware!
|
88
|
+
if defined?(RedisClient)
|
89
|
+
RedisClient.register(CommandRecorder::Middleware)
|
90
|
+
else
|
91
|
+
warn "RedisClient not available - command recording will not work"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Configuration for validation framework
|
96
|
+
module Config
|
97
|
+
# Default options for validators
|
98
|
+
DEFAULT_OPTIONS = {
|
99
|
+
auto_register_middleware: true,
|
100
|
+
strict_atomicity: true,
|
101
|
+
performance_tracking: true,
|
102
|
+
command_filtering: :all
|
103
|
+
}.freeze
|
104
|
+
|
105
|
+
@options = DEFAULT_OPTIONS.dup
|
106
|
+
|
107
|
+
class << self
|
108
|
+
attr_accessor :options
|
109
|
+
|
110
|
+
def configure
|
111
|
+
yield self if block_given?
|
112
|
+
end
|
113
|
+
|
114
|
+
def reset!
|
115
|
+
@options = DEFAULT_OPTIONS.dup
|
116
|
+
end
|
117
|
+
|
118
|
+
# Option accessors
|
119
|
+
def auto_register_middleware?
|
120
|
+
@options[:auto_register_middleware]
|
121
|
+
end
|
122
|
+
|
123
|
+
def strict_atomicity?
|
124
|
+
@options[:strict_atomicity]
|
125
|
+
end
|
126
|
+
|
127
|
+
def performance_tracking?
|
128
|
+
@options[:performance_tracking]
|
129
|
+
end
|
130
|
+
|
131
|
+
def command_filtering
|
132
|
+
@options[:command_filtering]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Auto-register middleware if enabled
|
140
|
+
Familia::Validation.register_middleware! if Familia::Validation::Config.auto_register_middleware?
|
data/lib/familia/version.rb
CHANGED