familia 2.0.0.pre6 → 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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -13
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +2 -2
  9. data/docs/wiki/Feature-System-Guide.md +36 -5
  10. data/docs/wiki/Home.md +30 -20
  11. data/docs/wiki/Relationships-Guide.md +684 -0
  12. data/examples/bit_encoding_integration.rb +237 -0
  13. data/examples/redis_command_validation_example.rb +231 -0
  14. data/examples/relationships_basic.rb +273 -0
  15. data/lib/familia/connection.rb +3 -3
  16. data/lib/familia/data_type.rb +7 -4
  17. data/lib/familia/features/encrypted_fields/concealed_string.rb +21 -23
  18. data/lib/familia/features/encrypted_fields.rb +413 -4
  19. data/lib/familia/features/expiration.rb +319 -33
  20. data/lib/familia/features/quantization.rb +385 -44
  21. data/lib/familia/features/relationships/cascading.rb +438 -0
  22. data/lib/familia/features/relationships/indexing.rb +370 -0
  23. data/lib/familia/features/relationships/membership.rb +503 -0
  24. data/lib/familia/features/relationships/permission_management.rb +264 -0
  25. data/lib/familia/features/relationships/querying.rb +620 -0
  26. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  27. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  28. data/lib/familia/features/relationships/tracking.rb +379 -0
  29. data/lib/familia/features/relationships.rb +466 -0
  30. data/lib/familia/features/transient_fields.rb +192 -10
  31. data/lib/familia/features.rb +2 -1
  32. data/lib/familia/horreum/subclass/definition.rb +1 -1
  33. data/lib/familia/validation/command_recorder.rb +336 -0
  34. data/lib/familia/validation/expectations.rb +519 -0
  35. data/lib/familia/validation/test_helpers.rb +443 -0
  36. data/lib/familia/validation/validator.rb +412 -0
  37. data/lib/familia/validation.rb +140 -0
  38. data/lib/familia/version.rb +1 -1
  39. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  40. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  41. data/try/edge_cases/string_coercion_try.rb +2 -0
  42. data/try/encryption/encryption_core_try.rb +3 -1
  43. data/try/features/categorical_permissions_try.rb +515 -0
  44. data/try/features/encryption_fields/concealed_string_core_try.rb +3 -0
  45. data/try/features/encryption_fields/context_isolation_try.rb +1 -0
  46. data/try/features/relationships_edge_cases_try.rb +145 -0
  47. data/try/features/relationships_performance_minimal_try.rb +132 -0
  48. data/try/features/relationships_performance_simple_try.rb +155 -0
  49. data/try/features/relationships_performance_try.rb +420 -0
  50. data/try/features/relationships_performance_working_try.rb +144 -0
  51. data/try/features/relationships_try.rb +237 -0
  52. data/try/features/safe_dump_try.rb +3 -0
  53. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  54. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  55. data/try/helpers/test_helpers.rb +1 -1
  56. data/try/horreum/base_try.rb +14 -8
  57. data/try/horreum/enhanced_conflict_handling_try.rb +2 -0
  58. data/try/horreum/relations_try.rb +1 -1
  59. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  60. data/try/validation/command_validation_try.rb.disabled +207 -0
  61. data/try/validation/performance_validation_try.rb.disabled +324 -0
  62. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  63. metadata +32 -4
  64. data/docs/wiki/RelatableObjects-Guide.md +0 -563
  65. data/lib/familia/features/relatable_objects.rb +0 -125
  66. data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,324 @@
1
+ # Validation testing for performance analysis and optimization detection
2
+
3
+ require_relative '../helpers/test_helpers'
4
+ require_relative '../../lib/familia/validation'
5
+
6
+ extend Familia::Validation::TestHelpers
7
+
8
+ # Initialize validation framework
9
+ setup_validation_test
10
+
11
+ # Test models for performance validation
12
+ class PerformanceTestUser < Familia::Horreum
13
+ identifier_field :user_id
14
+ field :user_id
15
+ field :name
16
+ field :email
17
+ field :status
18
+ list :activities
19
+ set :tags
20
+ zset :scores
21
+ end
22
+
23
+ class PerformanceTestPost < Familia::Horreum
24
+ identifier_field :post_id
25
+ field :post_id
26
+ field :title
27
+ field :content
28
+ field :author_id
29
+
30
+ def load_with_author
31
+ # Inefficient - loads author separately (N+1 pattern)
32
+ author = PerformanceTestUser.new(user_id: author_id)
33
+ author.refresh!
34
+ { post: self, author: author }
35
+ end
36
+
37
+ def efficient_load_with_author
38
+ # More efficient - could batch this operation
39
+ author_data = Familia.dbclient.hgetall("performancetestuser:#{author_id}:object")
40
+ author = PerformanceTestUser.new(user_id: author_id)
41
+ author_data.each { |k, v| author.send("#{k}=", v) if author.respond_to?("#{k}=") }
42
+ { post: self, author: author }
43
+ end
44
+ end
45
+
46
+ extend Familia::Validation::TestHelpers
47
+ setup_validation_test
48
+
49
+ ## Clean up existing test data
50
+ cleanup_keys = Familia.dbclient.keys("performancetest*")
51
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
52
+ true
53
+ #=> true
54
+
55
+ ## Performance analysis captures timing information
56
+ validator = Familia::Validation::Validator.new(performance_tracking: true)
57
+ commands = validator.capture_redis_commands do
58
+ user = PerformanceTestUser.new(user_id: "perf1", name: "Performance Test", email: "perf@test.com")
59
+ user.save
60
+ user.activities.unshift("activity1", "activity2", "activity3")
61
+ user.tags.add("tag1", "tag2")
62
+ user.scores.add(100, "score1")
63
+ end
64
+
65
+ analysis = validator.analyze_performance(commands)
66
+ analysis[:total_commands] > 0 && analysis[:total_duration_ms] >= 0
67
+ #=> true
68
+
69
+ ## Performance analysis detects command type breakdown
70
+ validator = Familia::Validation::Validator.new(performance_tracking: true)
71
+ commands = validator.capture_redis_commands do
72
+ user = PerformanceTestUser.new(user_id: "perf2", name: "Test User")
73
+ user.save # Hash operations
74
+ user.activities.unshift("item") # List operation
75
+ user.tags.add("tag") # Set operation
76
+ user.scores.add(50, "score") # Sorted set operation
77
+ end
78
+
79
+ analysis = validator.analyze_performance(commands)
80
+ breakdown = analysis[:command_type_breakdown]
81
+ breakdown.keys.length > 1 # Should have multiple command types
82
+ #=> true
83
+
84
+ ## assert_performance_within helper works
85
+ result = assert_performance_within(1000) do # 1 second limit
86
+ user = PerformanceTestUser.new(user_id: "perf3", name: "Speed Test")
87
+ user.save
88
+ end
89
+ result
90
+ #=> true
91
+
92
+ ## assert_efficient_commands detects N+1 patterns
93
+ posts_data = (1..5).map do |i|
94
+ PerformanceTestPost.new(post_id: "post#{i}", title: "Post #{i}",
95
+ content: "Content #{i}", author_id: "user#{i}")
96
+ end
97
+
98
+ # Create users first
99
+ (1..5).each do |i|
100
+ user = PerformanceTestUser.new(user_id: "user#{i}", name: "User #{i}")
101
+ user.save
102
+ end
103
+
104
+ # Save posts
105
+ posts_data.each(&:save)
106
+
107
+ # This should trigger N+1 detection (multiple separate HGETALL calls)
108
+ begin
109
+ assert_efficient_commands do
110
+ posts_data.each do |post|
111
+ post.load_with_author # Separate query for each author
112
+ end
113
+ end
114
+ false # Should not reach here
115
+ rescue Familia::Validation::ValidationError
116
+ true # Expected - N+1 pattern detected
117
+ end
118
+ #=> true
119
+
120
+ ## Efficient commands pass validation
121
+ assert_efficient_commands do
122
+ # Single user operation - no N+1 pattern
123
+ user = PerformanceTestUser.new(user_id: "efficient1", name: "Efficient User")
124
+ user.save
125
+ user.activities.unshift("single_activity")
126
+ end
127
+ #=> true
128
+
129
+ ## Performance metrics include slowest commands
130
+ validator = Familia::Validation::Validator.new(performance_tracking: true)
131
+ commands = validator.capture_redis_commands do
132
+ # Create operations of varying complexity
133
+ user = PerformanceTestUser.new(user_id: "slow_test", name: "Slow Test")
134
+ user.save
135
+
136
+ # Multiple list operations (potentially slower)
137
+ (1..10).each { |i| user.activities.unshift("item#{i}") }
138
+ end
139
+
140
+ analysis = validator.analyze_performance(commands)
141
+ analysis[:slowest_commands].length > 0
142
+ #=> true
143
+
144
+ ## Transaction efficiency is calculated correctly
145
+ validator = Familia::Validation::Validator.new(performance_tracking: true)
146
+ commands = validator.capture_redis_commands do
147
+ user = PerformanceTestUser.new(user_id: "tx_test", name: "Transaction Test")
148
+ user.save
149
+
150
+ # Use transaction for batch operations
151
+ Familia.transaction do |conn|
152
+ conn.hset(user.dbkey, "status", "active")
153
+ conn.hset(user.dbkey, "email", "tx@test.com")
154
+ conn.lpush("#{user.dbkey.sub(':object', ':activities')}", "tx_activity")
155
+ end
156
+ end
157
+
158
+ analysis = validator.analyze_performance(commands)
159
+ efficiency = analysis[:transaction_efficiency]
160
+ efficiency[:score] > 0 && efficiency[:details].include?("transaction")
161
+ #=> true
162
+
163
+ ## Empty command sequence analysis handles gracefully
164
+ empty_commands = Familia::Validation::CommandRecorder::CommandSequence.new
165
+ analysis = Familia::Validation::PerformanceAnalyzer.new(empty_commands).analyze
166
+ analysis[:total_commands] == 0 && analysis[:efficiency_score] >= 0
167
+ #=> true
168
+
169
+ ## Performance analysis identifies potential batching opportunities
170
+ validator = Familia::Validation::Validator.new(performance_tracking: true)
171
+ commands = validator.capture_redis_commands do
172
+ # Multiple similar operations that could be batched
173
+ user = PerformanceTestUser.new(user_id: "batch_test")
174
+ user.save
175
+
176
+ # Multiple separate HSET operations - could be HMSET
177
+ (1..5).each do |i|
178
+ Familia.dbclient.hset(user.dbkey, "field#{i}", "value#{i}")
179
+ end
180
+ end
181
+
182
+ analysis = validator.analyze_performance(commands)
183
+ n_plus_one_patterns = analysis[:potential_n_plus_one]
184
+ n_plus_one_patterns.any? { |pattern| pattern[:command] == "HSET" }
185
+ #=> true
186
+
187
+ ## Command sequence metadata is accurate
188
+ validator = Familia::Validation::Validator.new
189
+ commands = validator.capture_redis_commands do
190
+ user = PerformanceTestUser.new(user_id: "meta_test", name: "Metadata Test")
191
+ user.save
192
+ user.activities.unshift("test_activity")
193
+
194
+ Familia.transaction do |conn|
195
+ conn.hset(user.dbkey, "status", "verified")
196
+ conn.incr("meta_counter")
197
+ end
198
+ end
199
+
200
+ [
201
+ commands&.command_count.to_i > 0,
202
+ commands&.transaction_count.to_i == 1,
203
+ commands&.transaction_blocks&.first&.command_count.to_i == 2
204
+ ]
205
+ #=> [true, true, true]
206
+
207
+ ## Recorded commands contain proper context information
208
+ validator = Familia::Validation::Validator.new
209
+ commands = validator.capture_redis_commands do
210
+ user = PerformanceTestUser.new(user_id: "context_test")
211
+ user.save
212
+
213
+ Familia.transaction do |conn|
214
+ conn.hset(user.dbkey, "test_field", "test_value")
215
+ end
216
+ end
217
+
218
+ transaction_commands = commands.commands.select(&:atomic_command?)
219
+ non_transaction_commands = commands.commands.reject(&:atomic_command?)
220
+
221
+ transaction_commands.any? && non_transaction_commands.any?
222
+ #=> true
223
+
224
+ ## Performance thresholds can be customized
225
+ begin
226
+ assert_performance_within(1) do # Very tight 1ms limit
227
+ # This will likely exceed 1ms
228
+ user = PerformanceTestUser.new(user_id: "timeout_test", name: "Timeout Test")
229
+ user.save
230
+ (1..10).each { |i| user.activities.unshift("item#{i}") }
231
+ end
232
+ false # Should not reach here
233
+ rescue Familia::Validation::ValidationError => e
234
+ e.message.include?("took") && e.message.include?("expected less than")
235
+ end
236
+ #=> true
237
+
238
+ ## Efficiency score calculation includes multiple factors
239
+ validator = Familia::Validation::Validator.new(performance_tracking: true)
240
+
241
+ # Inefficient pattern - lots of individual operations
242
+ commands_inefficient = validator.capture_redis_commands do
243
+ user = PerformanceTestUser.new(user_id: "inefficient", name: "Inefficient")
244
+ user.save
245
+
246
+ # Many individual operations instead of batching
247
+ (1..20).each do |i|
248
+ Familia.dbclient.hset("test:key#{i}", "field", "value#{i}")
249
+ end
250
+ end
251
+
252
+ # Efficient pattern - using transactions
253
+ commands_efficient = validator.capture_redis_commands do
254
+ user = PerformanceTestUser.new(user_id: "efficient", name: "Efficient")
255
+ user.save
256
+
257
+ Familia.transaction do |conn|
258
+ (1..5).each do |i|
259
+ conn.hset(user.dbkey, "field#{i}", "value#{i}")
260
+ end
261
+ end
262
+ end
263
+
264
+ analysis_inefficient = validator.analyze_performance(commands_inefficient)
265
+ analysis_efficient = validator.analyze_performance(commands_efficient)
266
+
267
+ analysis_efficient[:efficiency_score] > analysis_inefficient[:efficiency_score]
268
+ #=> true
269
+
270
+ ## Command duration tracking works
271
+ validator = Familia::Validation::Validator.new
272
+ commands = validator.capture_redis_commands do
273
+ user = PerformanceTestUser.new(user_id: "duration_test", name: "Duration Test")
274
+ user.save
275
+ end
276
+
277
+ first_command = commands&.commands&.first
278
+ first_command&.duration_us.to_i >= 0 && first_command&.timestamp.is_a?(Time)
279
+ #=> true
280
+
281
+ ## Transaction boundaries are properly detected in complex scenarios
282
+ validator = Familia::Validation::Validator.new
283
+ commands = validator.capture_redis_commands do
284
+ user1 = PerformanceTestUser.new(user_id: "complex1", name: "Complex 1")
285
+ user2 = PerformanceTestUser.new(user_id: "complex2", name: "Complex 2")
286
+
287
+ # Mixed atomic and non-atomic operations
288
+ user1.save # Non-atomic
289
+ user2.save # Non-atomic
290
+
291
+ # Atomic batch update
292
+ Familia.transaction do |conn|
293
+ conn.hset(user1.dbkey, "status", "active")
294
+ conn.hset(user2.dbkey, "status", "active")
295
+ conn.incr("active_user_count")
296
+ end
297
+
298
+ # More non-atomic operations
299
+ user1.activities.unshift("logged_in")
300
+ user2.activities.unshift("logged_in")
301
+ end
302
+
303
+ atomic_commands = commands.commands.select(&:atomic_command?)
304
+ non_atomic_commands = commands.commands.reject(&:atomic_command?)
305
+
306
+ [
307
+ commands&.transaction_count.to_i == 1,
308
+ commands&.transaction_blocks&.first&.command_count.to_i == 3,
309
+ atomic_commands&.length.to_i == 3,
310
+ non_atomic_commands&.length.to_i > 3
311
+ ]
312
+ #=> [true, true, true, true]
313
+
314
+ ## Cleanup test environment
315
+ teardown_validation_test
316
+
317
+ ## Clean up test data
318
+ cleanup_keys = Familia.dbclient.keys("performancetest*")
319
+ cleanup_keys.concat(Familia.dbclient.keys("test:key*"))
320
+ cleanup_keys.concat(Familia.dbclient.keys("meta_counter"))
321
+ cleanup_keys.concat(Familia.dbclient.keys("active_user_count"))
322
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
323
+ true
324
+ #=> true
@@ -0,0 +1,390 @@
1
+ # Real-world validation scenarios testing complex application workflows
2
+
3
+ require_relative '../helpers/test_helpers'
4
+ require_relative '../../lib/familia/validation'
5
+
6
+ # Real-world test models that demonstrate complex validation scenarios
7
+ class User < Familia::Horreum
8
+ identifier_field :user_id
9
+ field :user_id
10
+ field :username
11
+ field :email
12
+ field :created_at
13
+ field :last_login
14
+ field :status
15
+
16
+ list :activity_log
17
+ set :permissions
18
+ zset :scores
19
+ hashkey :preferences
20
+ end
21
+
22
+ class BlogPost < Familia::Horreum
23
+ identifier_field :post_id
24
+ field :post_id
25
+ field :title
26
+ field :content
27
+ field :author_id
28
+ field :published_at
29
+ field :status
30
+
31
+ list :comments
32
+ set :tags
33
+ zset :ratings
34
+ end
35
+
36
+ class Session < Familia::Horreum
37
+ identifier_field :session_id
38
+ field :session_id
39
+ field :user_id
40
+ field :created_at
41
+ field :expires_at
42
+ field :ip_address
43
+ end
44
+
45
+ # Service classes that perform complex operations
46
+ class UserService
47
+ def self.create_user_with_profile(user_data)
48
+ # Should be atomic - user creation + initial setup
49
+ user = User.new(user_data)
50
+
51
+ Familia.transaction do |conn|
52
+ # Save user data
53
+ user_data.each do |field, value|
54
+ conn.hset(user.dbkey, field.to_s, value.to_s)
55
+ end
56
+
57
+ # Set default permissions
58
+ conn.sadd("#{user.dbkey.sub(':object', ':permissions')}", "read")
59
+
60
+ # Initialize empty activity log
61
+ conn.lpush("#{user.dbkey.sub(':object', ':activity_log')}", "account_created")
62
+
63
+ # Set default score
64
+ conn.zadd("#{user.dbkey.sub(':object', ':scores')}", 0, "reputation")
65
+ end
66
+
67
+ user
68
+ end
69
+
70
+ def self.login_user(user_id, session_data)
71
+ user = User.new(user_id: user_id)
72
+ session = Session.new(session_data)
73
+
74
+ # Should be atomic - update last login + create session
75
+ Familia.transaction do |conn|
76
+ # Update user last login
77
+ conn.hset(user.dbkey, "last_login", Time.now.to_i.to_s)
78
+
79
+ # Create session
80
+ session_data.each do |field, value|
81
+ conn.hset(session.dbkey, field.to_s, value.to_s)
82
+ end
83
+
84
+ # Log activity
85
+ conn.lpush("#{user.dbkey.sub(':object', ':activity_log')}", "logged_in:#{Time.now.to_i}")
86
+ end
87
+
88
+ { user: user, session: session }
89
+ end
90
+ end
91
+
92
+ class BlogService
93
+ def self.publish_post(post_data, author_id)
94
+ post = BlogPost.new(post_data.merge(author_id: author_id))
95
+
96
+ # Should be atomic - create post + update author stats
97
+ user = User.new(user_id: author_id)
98
+
99
+ Familia.transaction do |conn|
100
+ # Save post
101
+ post_data.each do |field, value|
102
+ conn.hset(post.dbkey, field.to_s, value.to_s)
103
+ end
104
+ conn.hset(post.dbkey, "author_id", author_id)
105
+ conn.hset(post.dbkey, "published_at", Time.now.to_i.to_s)
106
+ conn.hset(post.dbkey, "status", "published")
107
+
108
+ # Update author score
109
+ conn.zincrby("#{user.dbkey.sub(':object', ':scores')}", 10, "posts_published")
110
+
111
+ # Log activity
112
+ conn.lpush("#{user.dbkey.sub(':object', ':activity_log')}", "published_post:#{post.post_id}")
113
+ end
114
+
115
+ post
116
+ end
117
+ end
118
+
119
+ extend Familia::Validation::TestHelpers
120
+ setup_validation_test
121
+
122
+ ## Clean up existing test data
123
+ cleanup_keys = Familia.dbclient.keys("user:*")
124
+ cleanup_keys.concat(Familia.dbclient.keys("blogpost:*"))
125
+ cleanup_keys.concat(Familia.dbclient.keys("session:*"))
126
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
127
+ true
128
+ #=> true
129
+
130
+ ## Real-world user creation scenario validates correctly
131
+ assert_atomic_operation do |expect|
132
+ expect.transaction do |tx|
133
+ tx.hset("user:john123:object", "user_id", "john123")
134
+ .hset("user:john123:object", "username", "john_doe")
135
+ .hset("user:john123:object", "email", "john@example.com")
136
+ .sadd("user:john123:permissions", "read")
137
+ .lpush("user:john123:activity_log", "account_created")
138
+ .zadd("user:john123:scores", "0", "reputation")
139
+ end
140
+
141
+ UserService.create_user_with_profile({
142
+ user_id: "john123",
143
+ username: "john_doe",
144
+ email: "john@example.com"
145
+ })
146
+ end
147
+ #=> true
148
+
149
+ ## User login scenario with session creation validates atomically
150
+ user_data = { user_id: "alice456", username: "alice", email: "alice@example.com" }
151
+ existing_user = UserService.create_user_with_profile(user_data)
152
+
153
+ assert_atomic_operation do |expect|
154
+ expect.transaction do |tx|
155
+ tx.hset("user:alice456:object", "last_login", any_string)
156
+ .hset("session:sess789:object", "session_id", "sess789")
157
+ .hset("session:sess789:object", "user_id", "alice456")
158
+ .hset("session:sess789:object", "ip_address", "192.168.1.1")
159
+ .lpush("user:alice456:activity_log", match_regex(/logged_in:/))
160
+ end
161
+
162
+ UserService.login_user("alice456", {
163
+ session_id: "sess789",
164
+ user_id: "alice456",
165
+ ip_address: "192.168.1.1"
166
+ })
167
+ end
168
+ #=> true
169
+
170
+ ## Blog post publishing with author score update validates correctly
171
+ # Create author first
172
+ author = UserService.create_user_with_profile({
173
+ user_id: "writer1",
174
+ username: "writer",
175
+ email: "writer@blog.com"
176
+ })
177
+
178
+ assert_atomic_operation do |expect|
179
+ expect.transaction do |tx|
180
+ tx.hset("blogpost:post001:object", "post_id", "post001")
181
+ .hset("blogpost:post001:object", "title", "My First Post")
182
+ .hset("blogpost:post001:object", "content", "Hello world!")
183
+ .hset("blogpost:post001:object", "author_id", "writer1")
184
+ .hset("blogpost:post001:object", "published_at", any_string)
185
+ .hset("blogpost:post001:object", "status", "published")
186
+ .zincrby("user:writer1:scores", "10", "posts_published")
187
+ .lpush("user:writer1:activity_log", "published_post:post001")
188
+ end
189
+
190
+ BlogService.publish_post({
191
+ post_id: "post001",
192
+ title: "My First Post",
193
+ content: "Hello world!"
194
+ }, "writer1")
195
+ end
196
+ #=> true
197
+
198
+ ## Complex multi-object operation validates with proper expectations
199
+ assert_redis_commands do |expect|
200
+ expect.strict_order(false) # Allow flexible order for complex operations
201
+ .transaction do |tx|
202
+ tx.hset("user:batch1:object", "user_id", "batch1")
203
+ .hset("user:batch1:object", "username", "batcher")
204
+ .sadd("user:batch1:permissions", "read")
205
+ .lpush("user:batch1:activity_log", "account_created")
206
+ .zadd("user:batch1:scores", "0", "reputation")
207
+ end
208
+ .transaction do |tx|
209
+ tx.hset("blogpost:batch_post:object", "post_id", "batch_post")
210
+ .hset("blogpost:batch_post:object", "title", "Batch Post")
211
+ .hset("blogpost:batch_post:object", "author_id", "batch1")
212
+ .hset("blogpost:batch_post:object", "published_at", any_string)
213
+ .hset("blogpost:batch_post:object", "status", "published")
214
+ .zincrby("user:batch1:scores", "10", "posts_published")
215
+ .lpush("user:batch1:activity_log", "published_post:batch_post")
216
+ end
217
+
218
+ # Execute complex workflow
219
+ user = UserService.create_user_with_profile({
220
+ user_id: "batch1",
221
+ username: "batcher"
222
+ })
223
+
224
+ BlogService.publish_post({
225
+ post_id: "batch_post",
226
+ title: "Batch Post"
227
+ }, "batch1")
228
+ end
229
+ #=> true
230
+
231
+ ## Performance analysis on real-world operations shows efficiency
232
+ validator = Familia::Validation::Validator.new(performance_tracking: true)
233
+ commands = validator.capture_redis_commands do
234
+ # Simulate real application load
235
+ users = []
236
+ posts = []
237
+
238
+ # Create multiple users efficiently
239
+ (1..3).each do |i|
240
+ users << UserService.create_user_with_profile({
241
+ user_id: "perf_user#{i}",
242
+ username: "perfuser#{i}",
243
+ email: "perf#{i}@example.com"
244
+ })
245
+ end
246
+
247
+ # Each user publishes a post
248
+ users.each_with_index do |user, i|
249
+ posts << BlogService.publish_post({
250
+ post_id: "perf_post#{i + 1}",
251
+ title: "Performance Post #{i + 1}",
252
+ content: "Content for performance testing"
253
+ }, user.user_id)
254
+ end
255
+ end
256
+
257
+ analysis = validator.analyze_performance(commands)
258
+ [
259
+ analysis[:total_commands] > 10, # Should have many commands
260
+ analysis[:transaction_efficiency][:score] > 0.5, # Good transaction usage
261
+ analysis[:efficiency_score] > 60 # Reasonable efficiency
262
+ ]
263
+ #=> [true, true, true]
264
+
265
+ ## Familia test helpers work with real-world objects
266
+ user = User.new(user_id: "helper_test", username: "helper", email: "helper@test.com")
267
+
268
+ assert_familia_save(user, {
269
+ user_id: "helper_test",
270
+ username: "helper",
271
+ email: "helper@test.com"
272
+ })
273
+ #=> true
274
+
275
+ ## Data type operations validate correctly with helpers
276
+ user = User.new(user_id: "data_test", username: "datatester")
277
+ user.save
278
+
279
+ assert_list_operation(user, :activity_log, :lpush, "test_activity") do
280
+ # This should execute: LPUSH user:data_test:activity_log test_activity
281
+ end
282
+ #=> true
283
+
284
+ ## Test set operation validation
285
+ assert_set_operation(user, :permissions, :sadd, "admin") do
286
+ # This should execute: SADD user:data_test:permissions admin
287
+ end
288
+ #=> true
289
+
290
+ ## Test sorted set operation validation
291
+ assert_sorted_set_operation(user, :scores, :zadd, 100, "test_score") do
292
+ # This should execute: ZADD user:data_test:scores 100 test_score
293
+ end
294
+ #=> true
295
+
296
+ ## Mixed atomic and non-atomic operations are properly categorized
297
+ validator = Familia::Validation::Validator.new(strict_atomicity: true)
298
+ commands = validator.capture_redis_commands do
299
+ user = User.new(user_id: "mixed_test", username: "mixed")
300
+
301
+ # Non-atomic operation
302
+ user.save
303
+
304
+ # Atomic operation
305
+ user.batch_update(status: "active", last_login: Time.now.to_i.to_s)
306
+
307
+ # Non-atomic operation
308
+ user.activity_log.unshift("status_updated")
309
+ end
310
+
311
+ atomic_count = commands.commands.count(&:atomic_command?)
312
+ non_atomic_count = commands.commands.count { |cmd| !cmd.atomic_command? }
313
+
314
+ [atomic_count > 0, non_atomic_count > 0]
315
+ #=> [true, true]
316
+
317
+ ## Edge case: Empty transaction is detected but doesn't fail validation
318
+ validator = Familia::Validation::Validator.new
319
+ result = validator.validate do |expect|
320
+ expect.transaction do |tx|
321
+ # Empty transaction expectation
322
+ end
323
+
324
+ # Execute empty transaction
325
+ Familia.transaction do |conn|
326
+ # No operations inside transaction
327
+ end
328
+ end
329
+
330
+ result.valid? && result.warning_messages.any?
331
+ #=> true
332
+
333
+ ## Error scenarios are properly handled and reported
334
+ validator = Familia::Validation::Validator.new
335
+ result = validator.validate do |expect|
336
+ expect.hset("wrong:key", "field", "value") # Wrong expectation
337
+
338
+ # Execute different operation
339
+ user = User.new(user_id: "error_test", username: "error")
340
+ user.save
341
+ end
342
+
343
+ [
344
+ !result.valid?,
345
+ result.error_messages.any?,
346
+ result.detailed_report.include?("FAIL")
347
+ ]
348
+ #=> [true, true, true]
349
+
350
+ ## Flexible matching works for dynamic scenarios
351
+ validator = Familia::Validation::Validator.new
352
+ result = validator.validate do |expect|
353
+ expect.strict_order(false)
354
+ .allow_extra_commands(true)
355
+ .match_pattern(/HSET user:dynamic\d+:object/)
356
+ .match_pattern(/SADD user:dynamic\d+:permissions/)
357
+
358
+ # Execute with dynamic ID
359
+ dynamic_id = "dynamic#{Time.now.to_i}"
360
+ user = UserService.create_user_with_profile({
361
+ user_id: dynamic_id,
362
+ username: "dynamic_user"
363
+ })
364
+ end
365
+
366
+ result.valid?
367
+ #=> true
368
+
369
+ ## Debug helpers provide useful output for troubleshooting
370
+ commands = capture_redis_commands do
371
+ user = User.new(user_id: "debug_test", username: "debugger")
372
+ user.save
373
+ user.permissions.add("debug")
374
+ end
375
+
376
+ # These should not raise errors and should provide useful output
377
+ debug_commands = commands.commands.map(&:to_s)
378
+ debug_commands.length > 0 && debug_commands.all? { |cmd| cmd.is_a?(String) }
379
+ #=> true
380
+
381
+ ## Cleanup test environment
382
+ teardown_validation_test
383
+
384
+ ## Clean up test data
385
+ cleanup_keys = Familia.dbclient.keys("user:*")
386
+ cleanup_keys.concat(Familia.dbclient.keys("blogpost:*"))
387
+ cleanup_keys.concat(Familia.dbclient.keys("session:*"))
388
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
389
+ true
390
+ #=> true