familia 2.0.0.pre18 → 2.0.0.pre19

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +58 -6
  3. data/CLAUDE.md +34 -9
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +9 -47
  6. data/README.md +39 -0
  7. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
  8. data/changelog.d/20251011_203905_delano_next.rst +30 -0
  9. data/changelog.d/20251011_212633_delano_next.rst +13 -0
  10. data/changelog.d/20251011_221253_delano_next.rst +26 -0
  11. data/docs/guides/feature-expiration.md +18 -18
  12. data/docs/migrating/v2.0.0-pre19.md +197 -0
  13. data/examples/datatype_standalone.rb +281 -0
  14. data/lib/familia/connection/behavior.rb +252 -0
  15. data/lib/familia/connection/handlers.rb +95 -0
  16. data/lib/familia/connection/operation_core.rb +1 -1
  17. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
  18. data/lib/familia/connection/transaction_core.rb +7 -9
  19. data/lib/familia/connection.rb +3 -2
  20. data/lib/familia/data_type/connection.rb +151 -7
  21. data/lib/familia/data_type/database_commands.rb +7 -4
  22. data/lib/familia/data_type/serialization.rb +4 -0
  23. data/lib/familia/data_type/types/hashkey.rb +1 -1
  24. data/lib/familia/errors.rb +51 -14
  25. data/lib/familia/features/expiration/extensions.rb +8 -10
  26. data/lib/familia/features/expiration.rb +19 -19
  27. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +39 -38
  28. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +115 -43
  29. data/lib/familia/features/relationships/indexing.rb +37 -42
  30. data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
  31. data/lib/familia/field_type.rb +2 -1
  32. data/lib/familia/horreum/connection.rb +11 -35
  33. data/lib/familia/horreum/database_commands.rb +129 -10
  34. data/lib/familia/horreum/definition.rb +2 -1
  35. data/lib/familia/horreum/management.rb +21 -15
  36. data/lib/familia/horreum/persistence.rb +190 -66
  37. data/lib/familia/horreum/serialization.rb +3 -0
  38. data/lib/familia/horreum/utils.rb +0 -8
  39. data/lib/familia/horreum.rb +31 -12
  40. data/lib/familia/logging.rb +2 -5
  41. data/lib/familia/settings.rb +7 -7
  42. data/lib/familia/version.rb +1 -1
  43. data/lib/middleware/database_logger.rb +76 -5
  44. data/try/edge_cases/string_coercion_try.rb +4 -4
  45. data/try/features/expiration/expiration_try.rb +1 -1
  46. data/try/features/relationships/indexing_try.rb +28 -4
  47. data/try/features/relationships/relationships_api_changes_try.rb +4 -4
  48. data/try/integration/connection/fiber_context_preservation_try.rb +3 -3
  49. data/try/integration/connection/operation_mode_guards_try.rb +1 -1
  50. data/try/integration/connection/pipeline_fallback_integration_try.rb +12 -12
  51. data/try/integration/create_method_try.rb +22 -22
  52. data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
  53. data/try/integration/data_types/datatype_transactions_try.rb +247 -0
  54. data/try/integration/models/customer_safe_dump_try.rb +5 -1
  55. data/try/integration/models/familia_object_try.rb +1 -1
  56. data/try/integration/persistence_operations_try.rb +162 -10
  57. data/try/unit/data_types/boolean_try.rb +1 -1
  58. data/try/unit/data_types/string_try.rb +1 -1
  59. data/try/unit/horreum/auto_indexing_on_save_try.rb +32 -16
  60. data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
  61. data/try/unit/horreum/base_try.rb +1 -1
  62. data/try/unit/horreum/class_methods_try.rb +2 -2
  63. data/try/unit/horreum/initialization_try.rb +1 -1
  64. data/try/unit/horreum/relations_try.rb +4 -4
  65. data/try/unit/horreum/serialization_try.rb +2 -2
  66. data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
  67. data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
  68. metadata +14 -2
@@ -130,15 +130,15 @@ process_identifier(@customer)
130
130
 
131
131
  ## Cleanup after test, 1
132
132
  @metadata.delete!
133
- #=> true
133
+ #=> 1
134
134
 
135
135
  ## Cleanup after test, 2
136
136
  @customer.delete!
137
- #=> true
137
+ #=> 1
138
138
 
139
139
  ## Cleanup after test, 3
140
140
  @session.delete!
141
- #=> true
141
+ #=> 1
142
142
 
143
143
  ## to_s handles identifier errors gracefully
144
144
  badboi = BadIdentifierTest.new
@@ -154,4 +154,4 @@ badboi.to_s # .include?('BadIdentifierTest')
154
154
 
155
155
  ## Delete customer2
156
156
  [@customer2.exists?, @customer2.delete!]
157
- #=> [false, false]
157
+ #=> [false, 0]
@@ -58,7 +58,7 @@ ExpiringTest.default_expiration
58
58
  #=> true
59
59
 
60
60
  ## Can call update_expiration method
61
- result = @test_obj.update_expiration(default_expiration: 180)
61
+ result = @test_obj.update_expiration(expiration: 180)
62
62
  [result.class, result]
63
63
  #=> [FalseClass, false]
64
64
 
@@ -62,7 +62,7 @@ end
62
62
  @user3.save
63
63
 
64
64
  @company_id = "comp_#{rand(10000000)}"
65
- @company = TestCompany.create(company_id: @company_id, name: 'Acme Corp')
65
+ @company = TestCompany.create!(company_id: @company_id, name: 'Acme Corp')
66
66
  @emp1 = TestEmployee.new(emp_id: 'emp_001', email: 'alice@acme.com', department: 'engineering', manager_id: 'mgr_001', badge_number: 'BADGE001')
67
67
  @emp1.save
68
68
  @emp2 = TestEmployee.new(emp_id: 'emp_002', email: 'bob@acme.com', department: 'sales', manager_id: 'mgr_002', badge_number: 'BADGE002')
@@ -86,7 +86,7 @@ sample = @company.sample_from_department(@emp2.department)
86
86
 
87
87
  ## First indexing relationship has correct configuration
88
88
  config = @user1.class.indexing_relationships.first
89
- [config.field, config.index_name, config.target_class == TestUser, config.query]
89
+ [config.field, config.index_name, config.scope_class == TestUser, config.query]
90
90
  #=> [:email, :email_lookup, true, true]
91
91
 
92
92
  ## Second indexing relationship has query disabled
@@ -189,7 +189,7 @@ TestUser.respond_to?(:find_by_username)
189
189
 
190
190
  ## Instance-scoped unique index has correct configuration
191
191
  config = @emp1.class.indexing_relationships.find { |r| r.field == :badge_number }
192
- [config.index_name, config.target_class, config.cardinality]
192
+ [config.index_name, config.scope_class, config.cardinality]
193
193
  #=> [:badge_index, TestCompany, :unique]
194
194
 
195
195
  ## Target class gets finder method for unique index
@@ -251,6 +251,16 @@ found_emps = @company.find_all_by_badge_number('BADGE002')
251
251
  found_emps.map(&:emp_id)
252
252
  #=> ["emp_002"]
253
253
 
254
+ ## Instance-scoped bulk query filters nil inputs
255
+ badges_with_nil = [nil, 'BADGE001', nil]
256
+ found_emps = @company.find_all_by_badge_number(badges_with_nil)
257
+ found_emps.map(&:emp_id)
258
+ #=> ["emp_001"]
259
+
260
+ ## Instance-scoped bulk query with only nil returns empty
261
+ @company.find_all_by_badge_number([nil, nil]).length
262
+ #=> 0
263
+
254
264
  ## Update badge index entry
255
265
  old_badge = @emp1.badge_number
256
266
  @emp1.badge_number = 'BADGE001_NEW'
@@ -277,7 +287,7 @@ old_badge = @emp1.badge_number
277
287
 
278
288
  ## Context-scoped multi_index relationship has correct configuration
279
289
  config = @emp1.class.indexing_relationships.find { |r| r.field == :department }
280
- [config.field, config.index_name, config.target_class]
290
+ [config.field, config.index_name, config.scope_class]
281
291
  #=> [:department, :dept_index, TestCompany]
282
292
 
283
293
  ## Context-scoped methods are generated with collision-free naming
@@ -420,6 +430,20 @@ found = TestUser.find_all_by_email(emails)
420
430
  found.map(&:user_id)
421
431
  #=> ["user_002"]
422
432
 
433
+ ## Bulk query filters nil inputs before querying
434
+ emails_with_nil = [nil, 'bob@example.com', nil]
435
+ found = TestUser.find_all_by_email(emails_with_nil)
436
+ found.map(&:user_id)
437
+ #=> ["user_002"]
438
+
439
+ ## Bulk query with only nil inputs returns empty array
440
+ TestUser.find_all_by_email([nil, nil]).length
441
+ #=> 0
442
+
443
+ ## Bulk query with nil as single value returns empty array
444
+ TestUser.find_all_by_email(nil).length
445
+ #=> 0
446
+
423
447
  ## Adding to index with nil field value does nothing
424
448
  @user_nil = TestUser.new(user_id: 'user_nil', email: nil)
425
449
  @user_nil.add_to_class_email_lookup
@@ -264,17 +264,17 @@ participation_meta.target_class.familia_name
264
264
 
265
265
  ## unique_index stores correct target_class
266
266
  indexing_meta = ApiTestUser.indexing_relationships.find { |r| r.index_name == :email_lookup }
267
- indexing_meta.target_class
267
+ indexing_meta.scope_class
268
268
  #=> ApiTestUser
269
269
 
270
- ## unique_index stores correct target_class via familia_name
270
+ ## unique_index stores correct scope_class via familia_name
271
271
  indexing_meta = ApiTestUser.indexing_relationships.find { |r| r.index_name == :email_lookup }
272
- indexing_meta.target_class.familia_name
272
+ indexing_meta.scope_class.familia_name
273
273
  #=> 'ApiTestUser'
274
274
 
275
275
  ## multi_index with within: stores correct metadata
276
276
  membership_meta = ApiTestMembership.indexing_relationships.find { |r| r.index_name == :user_memberships }
277
- membership_meta.target_class
277
+ membership_meta.scope_class
278
278
  #=> ApiTestUser
279
279
 
280
280
  # =============================================
@@ -166,7 +166,7 @@ end
166
166
  ## Operation guards prevent pipeline fiber issues before they occur
167
167
  begin
168
168
  # Ensure we're in strict mode for this test
169
- Familia.configure { |config| config.pipeline_mode = :strict }
169
+ Familia.configure { |config| config.pipelined_mode = :strict }
170
170
 
171
171
  Fiber[:familia_connection] = [Customer.create_dbclient, Familia.middleware_version]
172
172
  Fiber[:familia_connection_handler_class] = Familia::Connection::FiberConnectionHandler
@@ -185,7 +185,7 @@ end
185
185
 
186
186
  ## Method aliases work correctly
187
187
  # pipeline alias for pipelined
188
- result1 = Familia.pipeline do |conn|
188
+ result1 = Familia.pipelined do |conn|
189
189
  conn.set('alias_test', 'alias_success')
190
190
  conn.get('alias_test')
191
191
  end
@@ -203,7 +203,7 @@ result1.results.last == 'alias_success' && result2.results.last == 'alias_succes
203
203
  customer = Customer.new(custid: 'alias_test')
204
204
 
205
205
  # pipeline alias
206
- result1 = customer.pipeline do |conn|
206
+ result1 = customer.pipelined do |conn|
207
207
  conn.set('horreum:alias1', 'success1')
208
208
  conn.get('horreum:alias1')
209
209
  end
@@ -36,7 +36,7 @@ end
36
36
  ## FiberConnectionHandler blocks pipelines
37
37
  begin
38
38
  # Ensure we're in strict mode for this test
39
- Familia.configure { |config| config.pipeline_mode = :strict }
39
+ Familia.configure { |config| config.pipelined_mode = :strict }
40
40
 
41
41
  # Simulate middleware connection
42
42
  Fiber[:familia_connection] = [Customer.create_dbclient, Familia.middleware_version]
@@ -1,13 +1,13 @@
1
1
  #
2
2
  # Tests pipeline fallback modes when connection handlers don't support pipelines.
3
- # Validates that pipeline_mode configuration works correctly with cached connections
3
+ # Validates that pipelined_mode configuration works correctly with cached connections
4
4
  # and that the fallback behavior matches transaction fallback patterns.
5
5
  #
6
6
 
7
7
  require_relative '../../support/helpers/test_helpers'
8
8
 
9
9
  # Store original values
10
- $original_pipeline_mode = Familia.pipeline_mode
10
+ $original_pipelined_mode = Familia.pipelined_mode
11
11
  $original_transaction_mode = Familia.transaction_mode
12
12
 
13
13
  # Test model for pipeline fallback scenarios
@@ -17,7 +17,7 @@ class PipelineFallbackTest < Familia::Horreum
17
17
  end
18
18
 
19
19
  ## Test 1: Strict mode raises error with cached connection
20
- Familia.configure { |c| c.pipeline_mode = :strict }
20
+ Familia.configure { |c| c.pipelined_mode = :strict }
21
21
 
22
22
  # Cache connection at class level (uses DefaultConnectionHandler which doesn't support pipelines)
23
23
  PipelineFallbackTest.instance_variable_set(:@dbclient, Familia.create_dbclient)
@@ -28,7 +28,7 @@ customer.pipelined { |c| c.set('key', 'value') }
28
28
  #=~> /Cannot start pipeline with.*CachedConnectionHandler/
29
29
 
30
30
  ## Test 2: Warn mode falls back successfully with cached connection
31
- Familia.configure { |c| c.pipeline_mode = :warn }
31
+ Familia.configure { |c| c.pipelined_mode = :warn }
32
32
 
33
33
  # Cache connection at class level
34
34
  PipelineFallbackTest.instance_variable_set(:@dbclient, Familia.create_dbclient)
@@ -51,7 +51,7 @@ $warn_result.results[2]
51
51
  #=> 'value1'
52
52
 
53
53
  ## Test 3: Fresh connections still support real pipelines in strict mode
54
- Familia.configure { |c| c.pipeline_mode = :strict }
54
+ Familia.configure { |c| c.pipelined_mode = :strict }
55
55
 
56
56
  # Clear cached class-level connection to force CreateConnectionHandler
57
57
  PipelineFallbackTest.remove_instance_variable(:@dbclient) if PipelineFallbackTest.instance_variable_defined?(:@dbclient)
@@ -70,7 +70,7 @@ $fresh_result.results.size
70
70
  #=> 3
71
71
 
72
72
  ## Test 4: MultiResult format is correct for fallback
73
- Familia.configure { |c| c.pipeline_mode = :permissive }
73
+ Familia.configure { |c| c.pipelined_mode = :permissive }
74
74
 
75
75
  # Cache connection at class level
76
76
  PipelineFallbackTest.instance_variable_set(:@dbclient, Familia.create_dbclient)
@@ -92,7 +92,7 @@ $multi_result.results.class
92
92
  #=> Array
93
93
 
94
94
  ## Test 5: Permissive mode silently falls back
95
- Familia.configure { |c| c.pipeline_mode = :permissive }
95
+ Familia.configure { |c| c.pipelined_mode = :permissive }
96
96
 
97
97
  # Cache connection at class level
98
98
  PipelineFallbackTest.instance_variable_set(:@dbclient, Familia.create_dbclient)
@@ -111,18 +111,18 @@ $permissive_result.results
111
111
  #=> ['OK', 1, '1']
112
112
 
113
113
  ## Test 6: Pipeline mode configuration validation
114
- Familia.configure { |c| c.pipeline_mode = :invalid }
114
+ Familia.configure { |c| c.pipelined_mode = :invalid }
115
115
  #=:> ArgumentError
116
116
  #=~> /Pipeline mode must be :strict, :warn, or :permissive/
117
117
 
118
- ## Test 7: Default pipeline_mode is :warn
119
- Familia.instance_variable_set(:@pipeline_mode, nil)
120
- Familia.pipeline_mode
118
+ ## Test 7: Default pipelined_mode is :warn
119
+ Familia.instance_variable_set(:@pipelined_mode, nil)
120
+ Familia.pipelined_mode
121
121
  #=> :warn
122
122
 
123
123
  ## Cleanup: Restore original values
124
124
  Familia.configure do |c|
125
- c.pipeline_mode = $original_pipeline_mode
125
+ c.pipelined_mode = $original_pipelined_mode
126
126
  c.transaction_mode = $original_transaction_mode
127
127
  end
128
128
  PipelineFallbackTest.remove_instance_variable(:@dbclient) if PipelineFallbackTest.instance_variable_defined?(:@dbclient)
@@ -37,7 +37,7 @@ end
37
37
  # =============================================
38
38
 
39
39
  ## create method successfully creates new object
40
- @created_obj = CreateTestModel.create(id: @first_test_id, name: 'Created Object', value: 'test_value')
40
+ @created_obj = CreateTestModel.create!(id: @first_test_id, name: 'Created Object', value: 'test_value')
41
41
  [@created_obj.class, @created_obj.exists?, @created_obj.name]
42
42
  #=> [CreateTestModel, true, 'Created Object']
43
43
 
@@ -55,17 +55,17 @@ end
55
55
  # =============================================
56
56
 
57
57
  ## create method raises RecordExistsError for duplicate
58
- CreateTestModel.create(id: @first_test_id, name: 'Duplicate Attempt')
58
+ CreateTestModel.create!(id: @first_test_id, name: 'Duplicate Attempt')
59
59
  #=!> Familia::RecordExistsError
60
60
 
61
61
  ## RecordExistsError includes the dbkey in the message
62
- CreateTestModel.create(id: @first_test_id, name: 'Another Duplicate')
62
+ CreateTestModel.create!(id: @first_test_id, name: 'Another Duplicate')
63
63
  #=!> Familia::RecordExistsError
64
64
  #==> !!error.message.match(/create_test_model:#{@first_test_id}:object/)
65
65
 
66
66
  ## RecordExistsError message follows consistent format
67
67
  begin
68
- CreateTestModel.create(id: @first_test_id, name: 'Yet Another Duplicate')
68
+ CreateTestModel.create!(id: @first_test_id, name: 'Yet Another Duplicate')
69
69
  false # Should not reach here
70
70
  rescue Familia::RecordExistsError => e
71
71
  e.message.start_with?('Key already exists:')
@@ -74,10 +74,10 @@ end
74
74
 
75
75
  ## RecordExistsError exposes key property for programmatic access
76
76
  @final_test_id = next_test_id
77
- CreateTestModel.create(id: @final_test_id, name: 'Setup for Key Test')
77
+ CreateTestModel.create!(id: @final_test_id, name: 'Setup for Key Test')
78
78
 
79
79
  begin
80
- CreateTestModel.create(id: @final_test_id, name: 'Key Test Duplicate')
80
+ CreateTestModel.create!(id: @final_test_id, name: 'Key Test Duplicate')
81
81
  false # Should not reach here
82
82
  rescue Familia::RecordExistsError => e
83
83
  # Key should be accessible and contain the identifier
@@ -90,22 +90,22 @@ end
90
90
  # =============================================
91
91
 
92
92
  ## create with empty identifier raises NoIdentifier error
93
- CreateTestModel.create(id: '')
93
+ CreateTestModel.create!(id: '')
94
94
  #=!> Familia::NoIdentifier
95
95
 
96
96
  ## create with nil identifier raises NoIdentifier error
97
- CreateTestModel.create(id: nil)
97
+ CreateTestModel.create!(id: nil)
98
98
  #=!> Familia::NoIdentifier
99
99
 
100
100
  ## create with only some fields set
101
101
  @partial_id = next_test_id
102
- @partial_obj = CreateTestModel.create(id: @partial_id, name: 'Partial Object')
102
+ @partial_obj = CreateTestModel.create!(id: @partial_id, name: 'Partial Object')
103
103
  [@partial_obj.exists?, @partial_obj.name, @partial_obj.value]
104
104
  #=> [true, 'Partial Object', nil]
105
105
 
106
106
  ## create with no additional fields (only identifier)
107
107
  @minimal_id = next_test_id
108
- @minimal_obj = CreateTestModel.create(id: @minimal_id)
108
+ @minimal_obj = CreateTestModel.create!(id: @minimal_id)
109
109
  [@minimal_obj.exists?, @minimal_obj.id]
110
110
  #=> [true, @minimal_id]
111
111
 
@@ -115,14 +115,14 @@ CreateTestModel.create(id: nil)
115
115
 
116
116
  ## create is atomic - no partial state on failure
117
117
  @concurrent_id = next_test_id
118
- @first_obj = CreateTestModel.create(id: @concurrent_id, name: 'First')
118
+ @first_obj = CreateTestModel.create!(id: @concurrent_id, name: 'First')
119
119
 
120
120
  # Verify first object exists
121
121
  first_exists = @first_obj.exists?
122
122
 
123
123
  # Attempt to create duplicate should not affect existing object
124
124
  begin
125
- CreateTestModel.create(id: @concurrent_id, name: 'Concurrent Attempt')
125
+ CreateTestModel.create!(id: @concurrent_id, name: 'Concurrent Attempt')
126
126
  false # Should not reach here
127
127
  rescue Familia::RecordExistsError
128
128
  # Original object should be unchanged
@@ -134,7 +134,7 @@ end
134
134
  ## create failure doesn't leave partial data
135
135
  before_failed_create = Familia.dbclient.keys("create_test_model:#{@concurrent_id}:*").length
136
136
  begin
137
- CreateTestModel.create(id: @concurrent_id, name: 'Should Fail')
137
+ CreateTestModel.create!(id: @concurrent_id, name: 'Should Fail')
138
138
  rescue Familia::RecordExistsError
139
139
  # Should not create any additional keys
140
140
  after_failed_create = Familia.dbclient.keys("create_test_model:#{@concurrent_id}:*").length
@@ -148,20 +148,20 @@ end
148
148
 
149
149
  ## Both create and save_if_not_exists raise same error type for duplicates
150
150
  @consistency_id = next_test_id
151
- @consistency_obj = CreateTestModel.create(id: @consistency_id, name: 'Consistency Test')
151
+ @consistency_obj = CreateTestModel.create!(id: @consistency_id, name: 'Consistency Test')
152
152
 
153
153
  # Test create raises RecordExistsError
154
154
  create_error_class = begin
155
- CreateTestModel.create(id: @consistency_id, name: 'Create Duplicate')
155
+ CreateTestModel.create!(id: @consistency_id, name: 'Create Duplicate')
156
156
  nil
157
157
  rescue => e
158
158
  e.class
159
159
  end
160
160
 
161
- # Test save_if_not_exists raises RecordExistsError
161
+ # Test save_if_not_exists! raises RecordExistsError
162
162
  sine_error_class = begin
163
163
  duplicate_obj = CreateTestModel.new(id: @consistency_id, name: 'SINE Duplicate')
164
- duplicate_obj.save_if_not_exists
164
+ duplicate_obj.save_if_not_exists!
165
165
  nil
166
166
  rescue => e
167
167
  e.class
@@ -172,17 +172,17 @@ end
172
172
 
173
173
  ## Both methods have similar error message patterns
174
174
  @error_comparison_id = next_test_id
175
- CreateTestModel.create(id: @error_comparison_id, name: 'Error Comparison')
175
+ CreateTestModel.create!(id: @error_comparison_id, name: 'Error Comparison')
176
176
 
177
177
  create_error_msg = begin
178
- CreateTestModel.create(id: @error_comparison_id, name: 'Create Error')
178
+ CreateTestModel.create!(id: @error_comparison_id, name: 'Create Error')
179
179
  nil
180
180
  rescue => e
181
181
  e.message
182
182
  end
183
183
 
184
184
  sine_error_msg = begin
185
- CreateTestModel.new(id: @error_comparison_id, name: 'SINE Error').save_if_not_exists
185
+ CreateTestModel.new(id: @error_comparison_id, name: 'SINE Error').save_if_not_exists!
186
186
  nil
187
187
  rescue => e
188
188
  e.message
@@ -198,7 +198,7 @@ end
198
198
 
199
199
  ## create works with complex field values
200
200
  @complex_id = next_test_id
201
- @complex_obj = CreateTestModel.create(
201
+ @complex_obj = CreateTestModel.create!(
202
202
  id: @complex_id,
203
203
  name: 'Complex Object',
204
204
  value: { nested: 'data', array: [1, 2, 3] }
@@ -214,7 +214,7 @@ end
214
214
  @consistency_check_id = next_test_id
215
215
 
216
216
  # Create via class method
217
- @class_created = CreateTestModel.create(id: @consistency_check_id, name: 'Class Created')
217
+ @class_created = CreateTestModel.create!(id: @consistency_check_id, name: 'Class Created')
218
218
 
219
219
  # Both class and instance methods should see the object as existing
220
220
  class_sees_exists = CreateTestModel.exists?(@consistency_check_id)
@@ -0,0 +1,104 @@
1
+ # DataType Pipeline Support Tryouts
2
+ #
3
+ # Tests pipeline support for DataType objects. Pipelines provide performance
4
+ # optimization by batching commands without the atomicity guarantee of transactions.
5
+
6
+ require_relative '../../support/helpers/test_helpers'
7
+
8
+ # Setup
9
+ class PipelineTestUser < Familia::Horreum
10
+ logical_database 4
11
+ identifier_field :userid
12
+ field :userid
13
+ field :name
14
+
15
+ sorted_set :scores
16
+ hashkey :profile
17
+ set :tags
18
+ counter :visits
19
+ end
20
+
21
+ @user = PipelineTestUser.new(userid: 'pipe_user_001')
22
+ @user.name = 'Pipeline Tester'
23
+ @user.save
24
+
25
+ ## Parent-owned SortedSet can execute pipeline
26
+ result = @user.scores.pipelined do |pipe|
27
+ pipe.zadd(@user.scores.dbkey, 100, 'p1')
28
+ pipe.zadd(@user.scores.dbkey, 200, 'p2')
29
+ pipe.zcard(@user.scores.dbkey)
30
+ end
31
+ [result.is_a?(MultiResult), @user.scores.members.size]
32
+ #=> [true, 2]
33
+
34
+ ## Parent-owned HashKey can execute pipeline
35
+ result = @user.profile.pipelined do |pipe|
36
+ pipe.hset(@user.profile.dbkey, 'city', 'NYC')
37
+ pipe.hset(@user.profile.dbkey, 'state', 'NY')
38
+ pipe.hgetall(@user.profile.dbkey)
39
+ end
40
+ [result.is_a?(MultiResult), @user.profile.keys.sort]
41
+ #=> [true, ["city", "state"]]
42
+
43
+ ## Standalone SortedSet can execute pipeline
44
+ leaderboard = Familia::SortedSet.new('pipeline:leaderboard')
45
+ leaderboard.delete!
46
+ result = leaderboard.pipelined do |pipe|
47
+ pipe.zadd(leaderboard.dbkey, 100, 'player1')
48
+ pipe.zadd(leaderboard.dbkey, 200, 'player2')
49
+ pipe.zcard(leaderboard.dbkey)
50
+ end
51
+ [result.is_a?(MultiResult), leaderboard.members.size]
52
+ #=> [true, 2]
53
+
54
+ ## Pipeline with direct_access works correctly
55
+ result = @user.profile.pipelined do |pipe_conn|
56
+ pipe_conn.hset(@user.profile.dbkey, 'pipeline_test', 'yes')
57
+
58
+ @user.profile.direct_access do |conn, key|
59
+ conn.object_id == pipe_conn.object_id &&
60
+ conn.hset(key, 'direct_test', 'yes')
61
+ end
62
+ end
63
+ [@user.profile['pipeline_test'], @user.profile['direct_test']]
64
+ #=> ["yes", "yes"]
65
+
66
+ ## Pipeline returns MultiResult with correct structure
67
+ result = @user.scores.pipelined do |pipe|
68
+ pipe.zadd(@user.scores.dbkey, 300, 'p3')
69
+ pipe.zadd(@user.scores.dbkey, 400, 'p4')
70
+ end
71
+ [result.is_a?(MultiResult), result.results.is_a?(Array)]
72
+ #=> [true, true]
73
+
74
+ ## Empty pipeline returns empty MultiResult
75
+ result = @user.scores.pipelined { |pipe| }
76
+ [result.is_a?(MultiResult), result.results.empty?]
77
+ #=> [true, true]
78
+
79
+ ## Multiple DataType operations in single pipeline
80
+ result = @user.scores.pipelined do |pipe|
81
+ pipe.zadd(@user.scores.dbkey, 500, 'multi')
82
+ pipe.hset(@user.profile.dbkey, 'multi', 'pipeline')
83
+ pipe.sadd(@user.tags.dbkey, 'multi_tag')
84
+ end
85
+ [
86
+ result.is_a?(MultiResult),
87
+ @user.scores.member?('multi'),
88
+ @user.profile['multi'],
89
+ @user.tags.member?('multi_tag')
90
+ ]
91
+ #=> [true, true, "pipeline", true]
92
+
93
+ ## Standalone HashKey with logical_database option
94
+ custom = Familia::HashKey.new('pipeline:custom', logical_database: 5)
95
+ custom.delete!
96
+ result = custom.pipelined do |pipe|
97
+ pipe.hset(custom.dbkey, 'key1', 'value1')
98
+ pipe.hget(custom.dbkey, 'key1')
99
+ end
100
+ result.is_a?(MultiResult)
101
+ #=> true
102
+
103
+ # Cleanup
104
+ @user.destroy!