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.
- checksums.yaml +4 -4
- data/CHANGELOG.rst +58 -6
- data/CLAUDE.md +34 -9
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +39 -0
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +3 -2
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/database_commands.rb +7 -4
- data/lib/familia/data_type/serialization.rb +4 -0
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +39 -38
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +115 -43
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +129 -10
- data/lib/familia/horreum/definition.rb +2 -1
- data/lib/familia/horreum/management.rb +21 -15
- data/lib/familia/horreum/persistence.rb +190 -66
- data/lib/familia/horreum/serialization.rb +3 -0
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +31 -12
- data/lib/familia/logging.rb +2 -5
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/middleware/database_logger.rb +76 -5
- data/try/edge_cases/string_coercion_try.rb +4 -4
- data/try/features/expiration/expiration_try.rb +1 -1
- data/try/features/relationships/indexing_try.rb +28 -4
- data/try/features/relationships/relationships_api_changes_try.rb +4 -4
- data/try/integration/connection/fiber_context_preservation_try.rb +3 -3
- data/try/integration/connection/operation_mode_guards_try.rb +1 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +12 -12
- data/try/integration/create_method_try.rb +22 -22
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/integration/models/customer_safe_dump_try.rb +5 -1
- data/try/integration/models/familia_object_try.rb +1 -1
- data/try/integration/persistence_operations_try.rb +162 -10
- data/try/unit/data_types/boolean_try.rb +1 -1
- data/try/unit/data_types/string_try.rb +1 -1
- data/try/unit/horreum/auto_indexing_on_save_try.rb +32 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/unit/horreum/base_try.rb +1 -1
- data/try/unit/horreum/class_methods_try.rb +2 -2
- data/try/unit/horreum/initialization_try.rb +1 -1
- data/try/unit/horreum/relations_try.rb +4 -4
- data/try/unit/horreum/serialization_try.rb +2 -2
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- metadata +14 -2
@@ -130,15 +130,15 @@ process_identifier(@customer)
|
|
130
130
|
|
131
131
|
## Cleanup after test, 1
|
132
132
|
@metadata.delete!
|
133
|
-
#=>
|
133
|
+
#=> 1
|
134
134
|
|
135
135
|
## Cleanup after test, 2
|
136
136
|
@customer.delete!
|
137
|
-
#=>
|
137
|
+
#=> 1
|
138
138
|
|
139
139
|
## Cleanup after test, 3
|
140
140
|
@session.delete!
|
141
|
-
#=>
|
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,
|
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(
|
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.
|
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.
|
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.
|
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.
|
267
|
+
indexing_meta.scope_class
|
268
268
|
#=> ApiTestUser
|
269
269
|
|
270
|
-
## unique_index stores correct
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
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
|
-
$
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
119
|
-
Familia.instance_variable_set(:@
|
120
|
-
Familia.
|
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.
|
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!
|