ector-multi 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +2 -0
- data/ector-multi.gemspec +28 -0
- data/lib/ector-multi.rb +12 -0
- data/lib/ector-multi/errors.rb +48 -0
- data/lib/ector-multi/multi.rb +105 -0
- data/lib/ector-multi/operations.rb +111 -0
- data/lib/ector-multi/result.rb +22 -0
- data/rakefile +8 -0
- data/test/helper.rb +28 -0
- data/test/multi_test.rb +399 -0
- data/test/operations_test.rb +182 -0
- data/test/result_test.rb +42 -0
- metadata +103 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a20f20adbf811c03f487888057135de8267b17e5484792d7ab5a9096379321d4
|
4
|
+
data.tar.gz: f3011de86fd858e580b782059ffc5e36399183252a90c11936f8ffbc114b94a1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3897ed4e2e54db0fbf30a9a5c63a9ab5265c1c7fd2f810893d913ade99e8f8e8207ac58680efe0d4b334b4f7ca780178010cbff770a9c7936f4033466f4f10cb
|
7
|
+
data.tar.gz: 79472b2e945fe5bb44597a68d042a099cff5019600f066244df1f9d123edad5df2623d181cbdc9bd519bb769cd7e335fb13629246b03a9ba27e3950e6fc85793
|
data/README.md
ADDED
data/ector-multi.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'lib/ector-multi'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'ector-multi'
|
5
|
+
s.version = ::Ector::VERSION
|
6
|
+
s.date = Time.now.strftime('%Y-%m-%d')
|
7
|
+
s.summary = 'Grouping multiple DB operations in a single transaction.'
|
8
|
+
s.description = 'Grouping multiple DB operations in a single transaction. 100% Inspired by Ecto'
|
9
|
+
s.authors = ['Emiliano Mancuso']
|
10
|
+
s.email = ['emiliano.mancuso@gmail.com']
|
11
|
+
s.homepage = 'http://github.com/emancu/ector-multi'
|
12
|
+
s.license = 'MIT'
|
13
|
+
|
14
|
+
s.files = Dir[
|
15
|
+
'README.md',
|
16
|
+
'rakefile',
|
17
|
+
'lib/**/*.rb',
|
18
|
+
'*.gemspec'
|
19
|
+
]
|
20
|
+
|
21
|
+
s.test_files = Dir['test/*.*']
|
22
|
+
|
23
|
+
s.required_ruby_version = '>= 2.2'
|
24
|
+
|
25
|
+
s.add_dependency 'activerecord', '~> 5.2'
|
26
|
+
s.add_development_dependency 'protest', '~> 0'
|
27
|
+
s.add_development_dependency 'sqlite3', '~> 0'
|
28
|
+
end
|
data/lib/ector-multi.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
require_relative 'ector-multi/errors'
|
6
|
+
require_relative 'ector-multi/result'
|
7
|
+
require_relative 'ector-multi/operations'
|
8
|
+
require_relative 'ector-multi/multi'
|
9
|
+
|
10
|
+
module Ector
|
11
|
+
VERSION = '0.0.1'
|
12
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ector
|
4
|
+
class Multi
|
5
|
+
# Parent class for all Multi errors
|
6
|
+
Error = Class.new(StandardError)
|
7
|
+
|
8
|
+
# Parent class for all "Rollback-able" errors
|
9
|
+
Rollback = Class.new(Error)
|
10
|
+
|
11
|
+
class OperationFailure < Rollback
|
12
|
+
attr_reader :operation, :arguments, :caused_by
|
13
|
+
alias_method :value, :arguments
|
14
|
+
|
15
|
+
def initialize(operation, arguments, caused_by)
|
16
|
+
@operation = operation
|
17
|
+
@arguments = arguments
|
18
|
+
@caused_by = caused_by
|
19
|
+
|
20
|
+
super("Rollback fired by #{operation.name}")
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
"#<#{self.class.name} #{@operation.name} @caused_by=#{@caused_by.class} @arguments=#{@arguments}>"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class ControlledFailure < OperationFailure
|
29
|
+
def initialize(operation, value)
|
30
|
+
super(operation, value, nil)
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"#<#{self.class.name} #{@operation.name} value=#{@arguments}>"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class UniqueOperationError < Error
|
39
|
+
attr_reader :name
|
40
|
+
|
41
|
+
def initialize(name)
|
42
|
+
@name = name
|
43
|
+
|
44
|
+
super("Operation name '#{name}' is not unique!")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ector
|
4
|
+
class Multi
|
5
|
+
attr_reader :failure, :results, :operations
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@operations = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def append(multi)
|
12
|
+
check_operation_uniqueness!(multi.to_list)
|
13
|
+
|
14
|
+
@operations.push *multi.operations
|
15
|
+
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(name, model, attributes = {}, &block)
|
20
|
+
add_operation Operation::Create.new(name, model, operation_block(attributes, block))
|
21
|
+
end
|
22
|
+
|
23
|
+
def destroy(name, object = nil, &block)
|
24
|
+
add_operation Operation::Destroy.new(name, operation_block(object, block))
|
25
|
+
end
|
26
|
+
|
27
|
+
def destroy_all(name, dataset = nil, &block)
|
28
|
+
add_operation Operation::DestroyAll.new(name, operation_block(dataset, block))
|
29
|
+
end
|
30
|
+
|
31
|
+
def error(name, value)
|
32
|
+
add_operation Operation::Error.new(name, operation_block(value, nil))
|
33
|
+
end
|
34
|
+
|
35
|
+
def merge(multi)
|
36
|
+
raise NotImplemented
|
37
|
+
end
|
38
|
+
|
39
|
+
def prepend(multi)
|
40
|
+
check_operation_uniqueness!(multi.to_list)
|
41
|
+
|
42
|
+
@operations.prepend *multi.operations
|
43
|
+
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def run(name, &procedure)
|
48
|
+
add_operation Operation::Lambda.new(name, procedure)
|
49
|
+
end
|
50
|
+
|
51
|
+
def update(name, object, new_values, &block)
|
52
|
+
add_operation Operation::Update.new(name, object, operation_block(new_values, block))
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_all(name, dataset, new_values, &block)
|
56
|
+
add_operation Operation::UpdateAll.new(name, dataset, operation_block(new_values, block))
|
57
|
+
end
|
58
|
+
|
59
|
+
def commit
|
60
|
+
results = {}
|
61
|
+
|
62
|
+
operations.find(&:fail_fast?)&.run(results)
|
63
|
+
|
64
|
+
::ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
|
65
|
+
operations.each do |operation|
|
66
|
+
::ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
|
67
|
+
results[operation.name] = operation.run(::OpenStruct.new(results))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Ector::Multi::Result.new(results)
|
73
|
+
rescue Ector::Multi::Rollback => error
|
74
|
+
Ector::Multi::Result.new(results, error)
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_list
|
78
|
+
operations.map(&:name)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def add_operation(operation)
|
84
|
+
check_operation_uniqueness!(operation.name)
|
85
|
+
|
86
|
+
@operations << operation
|
87
|
+
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
def check_operation_uniqueness!(names)
|
92
|
+
repeated_names = to_list & Array(names)
|
93
|
+
|
94
|
+
fail UniqueOperationError.new(repeated_names) if repeated_names.any?
|
95
|
+
end
|
96
|
+
|
97
|
+
def operation_block(args, block)
|
98
|
+
if block
|
99
|
+
Proc.new { |results| block.(results, args) }
|
100
|
+
else
|
101
|
+
Proc.new { |_| args }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ector
|
4
|
+
class Multi
|
5
|
+
module Operation
|
6
|
+
class Base
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
def initialize(name, block)
|
10
|
+
@name = name
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
"#<#{self.class.name} #{@name}>"
|
16
|
+
end
|
17
|
+
|
18
|
+
def run(results)
|
19
|
+
output = @block.(results)
|
20
|
+
|
21
|
+
perform(output)
|
22
|
+
rescue Ector::Multi::Rollback => error
|
23
|
+
raise Ector::Multi::OperationFailure.new(self, output, error)
|
24
|
+
end
|
25
|
+
|
26
|
+
def fail_fast?
|
27
|
+
self.is_a?(Ector::Multi::Operation::Error)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def perform(_output)
|
33
|
+
raise NotImplemented
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Create < Base
|
38
|
+
def initialize(name, model, attributes_block)
|
39
|
+
@model = model
|
40
|
+
super(name, attributes_block)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def perform(attributes)
|
46
|
+
@model.create!(attributes)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Update < Base
|
51
|
+
def initialize(name, instance, attributes_block)
|
52
|
+
@instance = instance
|
53
|
+
super(name, attributes_block)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def perform(attributes)
|
59
|
+
@instance.update!(attributes)
|
60
|
+
|
61
|
+
@instance
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class UpdateAll < Base
|
66
|
+
def initialize(name, dataset, attributes_block)
|
67
|
+
@dataset = dataset
|
68
|
+
super(name, attributes_block)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def perform(attributes)
|
74
|
+
@dataset.update_all(attributes)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class Destroy < Base
|
79
|
+
private
|
80
|
+
|
81
|
+
def perform(model)
|
82
|
+
model.destroy
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class DestroyAll < Base
|
87
|
+
private
|
88
|
+
|
89
|
+
def perform(dataset)
|
90
|
+
dataset.destroy_all
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Lambda < Base
|
95
|
+
private
|
96
|
+
|
97
|
+
def perform(block_output)
|
98
|
+
block_output
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class Error < Base
|
103
|
+
def run(results)
|
104
|
+
output = @block.(results)
|
105
|
+
|
106
|
+
raise Ector::Multi::ControlledFailure.new(self, output)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ector
|
4
|
+
class Multi
|
5
|
+
class Result
|
6
|
+
attr_reader :results, :error
|
7
|
+
|
8
|
+
def initialize(results, error = nil)
|
9
|
+
@results = results
|
10
|
+
@error = error
|
11
|
+
end
|
12
|
+
|
13
|
+
def success?
|
14
|
+
!@error
|
15
|
+
end
|
16
|
+
|
17
|
+
def failure?
|
18
|
+
!success?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/rakefile
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Protest
|
4
|
+
require 'protest'
|
5
|
+
|
6
|
+
Protest.report_with(:progress)
|
7
|
+
|
8
|
+
def refute(condition, message="Expected condition to be unsatisfied")
|
9
|
+
assert !condition, message
|
10
|
+
end
|
11
|
+
|
12
|
+
# ActiveRecord
|
13
|
+
require 'active_record'
|
14
|
+
|
15
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
16
|
+
ActiveRecord::Schema.verbose = false
|
17
|
+
# ActiveRecord::Base.logger = Logger.new(STDOUT)
|
18
|
+
|
19
|
+
ActiveRecord::Migration.create_table :dummies, force: true do |t|
|
20
|
+
t.integer :user_id
|
21
|
+
t.string :name
|
22
|
+
end
|
23
|
+
|
24
|
+
class Dummy < ActiveRecord::Base; end
|
25
|
+
|
26
|
+
# Load lib
|
27
|
+
|
28
|
+
require_relative '../lib/ector-multi.rb'
|
data/test/multi_test.rb
ADDED
@@ -0,0 +1,399 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
Protest.describe 'Ector::Multi' do
|
6
|
+
setup do
|
7
|
+
Dummy.delete_all
|
8
|
+
@multi = Ector::Multi.new
|
9
|
+
end
|
10
|
+
|
11
|
+
context '#append' do
|
12
|
+
setup do
|
13
|
+
@left_multi = @multi.run(:left) { |_| }
|
14
|
+
end
|
15
|
+
|
16
|
+
test 'adds operations to the end of the queue' do
|
17
|
+
right_multi =
|
18
|
+
Ector::Multi.new
|
19
|
+
.run(:right) { |_| }
|
20
|
+
.run(:far_right) { |_| }
|
21
|
+
|
22
|
+
@left_multi.append(right_multi)
|
23
|
+
|
24
|
+
assert_equal @left_multi.to_list, [:left, :right, :far_right]
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'nothing changes when appends an empty multi' do
|
28
|
+
@left_multi.append(Ector::Multi.new)
|
29
|
+
|
30
|
+
assert_equal @left_multi.to_list, [:left]
|
31
|
+
end
|
32
|
+
|
33
|
+
test 'fails when there are duplicated operations' do
|
34
|
+
duplicated_multi =
|
35
|
+
Ector::Multi.new
|
36
|
+
.run(:should_fail) { |_| }
|
37
|
+
.run(:left) { |_| }
|
38
|
+
|
39
|
+
assert_raise(Ector::Multi::UniqueOperationError) { @left_multi.append(duplicated_multi) }
|
40
|
+
assert_equal @left_multi.to_list, [:left]
|
41
|
+
end
|
42
|
+
|
43
|
+
test 'returns self' do
|
44
|
+
right_multi = Ector::Multi.new.run(:right) { |_| }
|
45
|
+
|
46
|
+
returned = @left_multi.append(right_multi)
|
47
|
+
|
48
|
+
assert_equal @left_multi, returned
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context '#create' do
|
53
|
+
test 'adds a Create operation to the queue' do
|
54
|
+
@multi.create(:dummy, Dummy, id: 1)
|
55
|
+
|
56
|
+
assert_equal @multi.to_list, [:dummy]
|
57
|
+
assert_equal @multi.operations.first.class, Ector::Multi::Operation::Create
|
58
|
+
end
|
59
|
+
|
60
|
+
test 'fails when the operation name exists' do
|
61
|
+
@multi.run(:operation) { |_| }
|
62
|
+
|
63
|
+
assert_raise(Ector::Multi::UniqueOperationError) { @multi.create(:operation, Dummy, id: 1) }
|
64
|
+
end
|
65
|
+
|
66
|
+
test 'does not create an instance yet' do
|
67
|
+
@multi.create(:dummy, Dummy, name: 'New one')
|
68
|
+
|
69
|
+
assert_equal Dummy.count, 0
|
70
|
+
end
|
71
|
+
|
72
|
+
test 'returns self' do
|
73
|
+
assert_equal @multi, @multi.create(:dummy, Dummy, id: 1)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context '#destroy' do
|
78
|
+
setup do
|
79
|
+
@instance = Dummy.create(name: 'Destroyable')
|
80
|
+
end
|
81
|
+
|
82
|
+
test 'adds a Delete operation to the queue' do
|
83
|
+
@multi.destroy(:destroy_object, @instance)
|
84
|
+
|
85
|
+
assert_equal @multi.to_list, [:destroy_object]
|
86
|
+
assert_equal @multi.operations.first.class, Ector::Multi::Operation::Destroy
|
87
|
+
end
|
88
|
+
|
89
|
+
test 'fails when the operation name exists' do
|
90
|
+
@multi.run(:operation) { |_| }
|
91
|
+
|
92
|
+
assert_raise(Ector::Multi::UniqueOperationError) { @multi.destroy(:operation, @instance) }
|
93
|
+
end
|
94
|
+
|
95
|
+
test 'does not delete the instance yet' do
|
96
|
+
@multi.destroy(:destroy_object, @instance)
|
97
|
+
|
98
|
+
assert Dummy.find(@instance.id)
|
99
|
+
end
|
100
|
+
|
101
|
+
test 'returns self' do
|
102
|
+
assert_equal @multi, @multi.destroy(:dummy, @instance)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context '#destroy_all' do
|
107
|
+
setup do
|
108
|
+
Dummy.create(name: 'Destroyable')
|
109
|
+
end
|
110
|
+
|
111
|
+
test 'adds a Delete operation to the queue' do
|
112
|
+
@multi.destroy_all(:destroy_object, Dummy)
|
113
|
+
|
114
|
+
assert_equal @multi.to_list, [:destroy_object]
|
115
|
+
assert_equal @multi.operations.first.class, Ector::Multi::Operation::DestroyAll
|
116
|
+
end
|
117
|
+
|
118
|
+
test 'fails when the operation name exists' do
|
119
|
+
@multi.run(:operation) { |_| }
|
120
|
+
|
121
|
+
assert_raise(Ector::Multi::UniqueOperationError) { @multi.destroy(:operation, @instance) }
|
122
|
+
end
|
123
|
+
|
124
|
+
test 'does not delete the instance yet' do
|
125
|
+
@multi.destroy(:destroy_object, @instance)
|
126
|
+
|
127
|
+
assert Dummy.count, 1
|
128
|
+
end
|
129
|
+
|
130
|
+
test 'returns self' do
|
131
|
+
assert_equal @multi, @multi.destroy_all(:dummy, Dummy)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context '#error' do
|
136
|
+
test 'adds an Error operation to the queue' do
|
137
|
+
@multi.error(:fail_fast, 1)
|
138
|
+
|
139
|
+
assert_equal @multi.to_list, [:fail_fast]
|
140
|
+
assert_equal @multi.operations.first.class, Ector::Multi::Operation::Error
|
141
|
+
end
|
142
|
+
|
143
|
+
test 'fails when the operation name exists' do
|
144
|
+
@multi.run(:operation) { |_| }
|
145
|
+
|
146
|
+
assert_raise(Ector::Multi::UniqueOperationError) { @multi.error(:operation, 1) }
|
147
|
+
end
|
148
|
+
|
149
|
+
test 'returns self' do
|
150
|
+
assert_equal @multi, @multi.error(:dummy, 1)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context '#merge'
|
155
|
+
|
156
|
+
# test 'returns self' do
|
157
|
+
# assert_equal @multi, @multi.merge(:dummy, ???)
|
158
|
+
# end
|
159
|
+
# end
|
160
|
+
|
161
|
+
context '#prepend' do
|
162
|
+
setup do
|
163
|
+
@left_multi = @multi.run(:left) { }
|
164
|
+
end
|
165
|
+
|
166
|
+
test 'adds operations to the beginning of the queue' do
|
167
|
+
right_multi =
|
168
|
+
Ector::Multi.new
|
169
|
+
.run(:right) { }
|
170
|
+
.run(:far_right) { }
|
171
|
+
|
172
|
+
@left_multi.prepend(right_multi)
|
173
|
+
|
174
|
+
assert_equal @left_multi.to_list, [:right, :far_right, :left]
|
175
|
+
end
|
176
|
+
|
177
|
+
test 'nothing changes when prepends an empty multi' do
|
178
|
+
@left_multi.prepend(Ector::Multi.new)
|
179
|
+
|
180
|
+
assert_equal @left_multi.to_list, [:left]
|
181
|
+
end
|
182
|
+
|
183
|
+
test 'fails when there are duplicated operations' do
|
184
|
+
duplicated_multi =
|
185
|
+
Ector::Multi.new
|
186
|
+
.run(:should_fail) { }
|
187
|
+
.run(:left) { }
|
188
|
+
|
189
|
+
assert_raise(Ector::Multi::UniqueOperationError) { @left_multi.prepend(duplicated_multi) }
|
190
|
+
assert_equal @left_multi.to_list, [:left]
|
191
|
+
end
|
192
|
+
|
193
|
+
test 'returns self' do
|
194
|
+
right_multi = Ector::Multi.new.run(:right) { }
|
195
|
+
|
196
|
+
returned = @left_multi.prepend(right_multi)
|
197
|
+
|
198
|
+
assert_equal @left_multi, returned
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context '#run' do
|
203
|
+
test 'adds an Lambda operation to the queue' do
|
204
|
+
@multi.run(:procedure) { |results| results }
|
205
|
+
|
206
|
+
operation = @multi.operations.first
|
207
|
+
assert_equal @multi.to_list, [:procedure]
|
208
|
+
assert_equal operation.class, Ector::Multi::Operation::Lambda
|
209
|
+
end
|
210
|
+
|
211
|
+
test 'fails when the operation name exists' do
|
212
|
+
@multi.run(:operation) { }
|
213
|
+
|
214
|
+
assert_raise(Ector::Multi::UniqueOperationError) { @multi.run(:operation) { |_| 1 } }
|
215
|
+
end
|
216
|
+
|
217
|
+
test 'does not run the block yet' do
|
218
|
+
call_counter = 0
|
219
|
+
|
220
|
+
@multi.run(:procedure) { call_counter += 1 }
|
221
|
+
|
222
|
+
assert_equal call_counter, 0
|
223
|
+
end
|
224
|
+
|
225
|
+
test 'returns self' do
|
226
|
+
assert_equal(@multi, @multi.run(:dummy ) { })
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
context '#update' do
|
231
|
+
setup do
|
232
|
+
@instance = Dummy.create(name: 'Original')
|
233
|
+
end
|
234
|
+
|
235
|
+
test 'adds an Update operation to the queue' do
|
236
|
+
@multi.update(:change_name, @instance, name: 'Updated')
|
237
|
+
|
238
|
+
operation = @multi.operations.first
|
239
|
+
assert_equal @multi.to_list, [:change_name]
|
240
|
+
assert_equal operation.class, Ector::Multi::Operation::Update
|
241
|
+
end
|
242
|
+
|
243
|
+
test 'fails when the operation name exists' do
|
244
|
+
@multi.run(:operation) { |_| }
|
245
|
+
|
246
|
+
assert_raise(Ector::Multi::UniqueOperationError) { @multi.update(:operation, @instance, id: 1) }
|
247
|
+
end
|
248
|
+
|
249
|
+
test 'does not update the instance yet' do
|
250
|
+
@multi.update(:change_name, @instance, name: 'Updated')
|
251
|
+
|
252
|
+
assert_equal @instance.reload.name, 'Original'
|
253
|
+
end
|
254
|
+
|
255
|
+
test 'returns self' do
|
256
|
+
assert_equal @multi, @multi.update(:dummy, @instance, name: 'asd')
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
context '#update_all' do
|
261
|
+
setup do
|
262
|
+
@instance = Dummy.create(name: 'Original')
|
263
|
+
end
|
264
|
+
|
265
|
+
test 'adds an UpdateAll operation to the queue' do
|
266
|
+
@multi.update_all(:change_name, Dummy, name: 'Updated')
|
267
|
+
|
268
|
+
operation = @multi.operations.first
|
269
|
+
assert_equal @multi.to_list, [:change_name]
|
270
|
+
assert_equal operation.class, Ector::Multi::Operation::UpdateAll
|
271
|
+
end
|
272
|
+
|
273
|
+
test 'fails when the operation name exists' do
|
274
|
+
@multi.run(:operation) { |_| }
|
275
|
+
|
276
|
+
assert_raise(Ector::Multi::UniqueOperationError) { @multi.update(:operation, @instance, id: 1) }
|
277
|
+
end
|
278
|
+
|
279
|
+
test 'does not update the instance yet' do
|
280
|
+
@multi.update_all(:change_name, Dummy, name: 'Updated')
|
281
|
+
|
282
|
+
assert_equal @instance.reload.name, 'Original'
|
283
|
+
end
|
284
|
+
|
285
|
+
test 'returns self' do
|
286
|
+
assert_equal @multi, @multi.update_all(:dummy, Dummy, name: 'asd')
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
context '#commit' do
|
291
|
+
test 'trap Ector::Multi::Rollback exceptions' do
|
292
|
+
@multi.run(:fail_gracefully) { fail Ector::Multi::Rollback.new 'handled' }
|
293
|
+
|
294
|
+
result = @multi.commit
|
295
|
+
|
296
|
+
assert result.failure?
|
297
|
+
assert result.error.caused_by.is_a?(Ector::Multi::Rollback)
|
298
|
+
end
|
299
|
+
|
300
|
+
test 'raises any exception that is not a Ector::Multi::Rollback' do
|
301
|
+
@multi.run(:fail_violently) { fail 'unhandled' }
|
302
|
+
|
303
|
+
assert_raise(StandardError) { @multi.commit }
|
304
|
+
end
|
305
|
+
|
306
|
+
test 'stops execution immediately when an exception occurs' do
|
307
|
+
@multi
|
308
|
+
.run(:op1) { :should_pass }
|
309
|
+
.run(:op2) { fail Ector::Multi::Rollback.new 'sorry' }
|
310
|
+
.run(:op3) { :never_gets_called }
|
311
|
+
|
312
|
+
result = @multi.commit
|
313
|
+
|
314
|
+
assert result.failure?
|
315
|
+
assert_equal result.results.keys, [:op1]
|
316
|
+
assert_equal result.error.operation.name, :op2
|
317
|
+
end
|
318
|
+
|
319
|
+
test 'fails immediately when an Error Operation is enqueued' do
|
320
|
+
@multi
|
321
|
+
.run(:op1) { :never_gets_called }
|
322
|
+
.run(:op2) { :never_gets_called }
|
323
|
+
.run(:op3) { :never_gets_called }
|
324
|
+
.error(:fail_fast, 1)
|
325
|
+
|
326
|
+
result = @multi.commit
|
327
|
+
|
328
|
+
assert result.failure?
|
329
|
+
assert result.results.empty?
|
330
|
+
assert_equal result.error.value, 1
|
331
|
+
assert_equal result.error.operation.name, :fail_fast
|
332
|
+
end
|
333
|
+
|
334
|
+
test 'creates a transaction for each operation' do
|
335
|
+
transaction_ids =
|
336
|
+
@multi
|
337
|
+
.run(:op1) { ActiveRecord::Base.connection.current_transaction.__id__ }
|
338
|
+
.run(:op2) { ActiveRecord::Base.connection.current_transaction.__id__ }
|
339
|
+
.run(:op3) { ActiveRecord::Base.connection.current_transaction.__id__ }
|
340
|
+
.commit
|
341
|
+
.results
|
342
|
+
.values
|
343
|
+
.uniq
|
344
|
+
|
345
|
+
assert_equal transaction_ids.size, 3
|
346
|
+
end
|
347
|
+
|
348
|
+
test 'changes are persisted on success' do
|
349
|
+
instance = Dummy.create(name: 'Original')
|
350
|
+
deletable = Dummy.create(name: 'Deletable')
|
351
|
+
|
352
|
+
result =
|
353
|
+
@multi
|
354
|
+
.create(:new_dummy, Dummy, name: 'From multi')
|
355
|
+
.update(:rename, instance, name: 'Updated')
|
356
|
+
.destroy(:remove, deletable)
|
357
|
+
.commit
|
358
|
+
|
359
|
+
assert result.success?
|
360
|
+
assert_equal Dummy.all.map(&:name), ['Updated', 'From multi']
|
361
|
+
assert_equal result.results.keys, [:new_dummy, :rename, :remove]
|
362
|
+
end
|
363
|
+
|
364
|
+
test 'changes are rolledback on failure' do
|
365
|
+
instance = Dummy.create(name: 'Original')
|
366
|
+
deletable = Dummy.create(name: 'Deletable')
|
367
|
+
|
368
|
+
result =
|
369
|
+
@multi
|
370
|
+
.create(:new_dummy, Dummy, name: 'From multi')
|
371
|
+
.update(:rename, instance, name: 'Updated')
|
372
|
+
.destroy(:remove, deletable)
|
373
|
+
.run(:force_failure) { fail Ector::Multi::Rollback.new('abort')}
|
374
|
+
.commit
|
375
|
+
|
376
|
+
assert result.failure?
|
377
|
+
assert_equal Dummy.all.map(&:name), ['Original', 'Deletable']
|
378
|
+
assert_equal result.results.keys, [:new_dummy, :rename, :remove]
|
379
|
+
assert_equal result.error.operation.name, :force_failure
|
380
|
+
end
|
381
|
+
|
382
|
+
test 'returns a Ector::Multi::Result' do
|
383
|
+
assert @multi.commit.is_a?(Ector::Multi::Result)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
context '#to_list' do
|
388
|
+
test 'returns an ordered list with the name of the operations' do
|
389
|
+
nop = -> { }
|
390
|
+
empty = @multi
|
391
|
+
single = Ector::Multi.new.run(:lambda, &nop)
|
392
|
+
multi = Ector::Multi.new.run(:annonymous, &nop).run(:operations, &nop)
|
393
|
+
|
394
|
+
assert_equal empty.to_list, []
|
395
|
+
assert_equal single.to_list, [:lambda]
|
396
|
+
assert_equal multi.to_list, [:annonymous, :operations]
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
Protest.describe 'Operations' do
|
6
|
+
setup do
|
7
|
+
Dummy.delete_all
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'Create' do
|
11
|
+
setup do
|
12
|
+
@op = Ector::Multi::Operation::Create.new(:t, Dummy, lambda { |results| { name: 'Emi' }.merge(results) } )
|
13
|
+
end
|
14
|
+
|
15
|
+
test 'it does not fail fast' do
|
16
|
+
refute @op.fail_fast?
|
17
|
+
end
|
18
|
+
|
19
|
+
test 'raises an exception when creation fails' do
|
20
|
+
assert_raise(StandardError) do
|
21
|
+
@op.run(unknown_attribute: 123)
|
22
|
+
end
|
23
|
+
|
24
|
+
assert_equal Dummy.count, 0
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'returns the object created' do
|
28
|
+
res = @op.run(name: 'Created')
|
29
|
+
|
30
|
+
assert_equal res.class, Dummy
|
31
|
+
assert_equal res.name, 'Created'
|
32
|
+
assert_equal res.id, Dummy.last.id
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'Update' do
|
37
|
+
setup do
|
38
|
+
@instance = Dummy.create(name: 'Original')
|
39
|
+
@op = Ector::Multi::Operation::Update.new(:t, @instance, lambda { |results| { name: 'Updated' }.merge(results) } )
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'it does not fail fast' do
|
43
|
+
refute @op.fail_fast?
|
44
|
+
end
|
45
|
+
|
46
|
+
test 'raises an exception when update fails' do
|
47
|
+
assert_raise(StandardError) do
|
48
|
+
@op.run(unknown_attribute: 123)
|
49
|
+
end
|
50
|
+
|
51
|
+
assert_equal @instance.reload.name, 'Original'
|
52
|
+
end
|
53
|
+
|
54
|
+
test 'returns the object created' do
|
55
|
+
res = @op.run(name: 'Updated baby')
|
56
|
+
|
57
|
+
assert_equal res.class, Dummy
|
58
|
+
assert_equal res.name, 'Updated baby'
|
59
|
+
assert_equal res.id, @instance.id
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'UpdateAll' do
|
64
|
+
setup do
|
65
|
+
@instance = Dummy.create(name: 'Original')
|
66
|
+
@op = Ector::Multi::Operation::UpdateAll.new(:t, Dummy, lambda { |results| { name: 'Updated' }.merge(results) } )
|
67
|
+
end
|
68
|
+
|
69
|
+
test 'it does not fail fast' do
|
70
|
+
refute @op.fail_fast?
|
71
|
+
end
|
72
|
+
|
73
|
+
test 'raises an exception when update fails' do
|
74
|
+
assert_raise(StandardError) do
|
75
|
+
@op.run(unknown_attribute: 123)
|
76
|
+
end
|
77
|
+
|
78
|
+
assert_equal @instance.reload.name, 'Original'
|
79
|
+
end
|
80
|
+
|
81
|
+
test 'returns how many records were updated' do
|
82
|
+
res = @op.run(name: 'Mass update')
|
83
|
+
|
84
|
+
assert_equal res, 1
|
85
|
+
assert_equal @instance.reload.name, 'Mass update'
|
86
|
+
end
|
87
|
+
|
88
|
+
test 'accepts a dataset' do
|
89
|
+
Dummy.create(name: 'original 2')
|
90
|
+
Dummy.create(name: 'original 3')
|
91
|
+
|
92
|
+
@op = Ector::Multi::Operation::UpdateAll.new(:t, Dummy.where.not(name: 'original 2'), lambda { |results| results } )
|
93
|
+
res = @op.run(name: 'MassUpdated')
|
94
|
+
|
95
|
+
assert_equal res, 2
|
96
|
+
assert_equal Dummy.count, 3
|
97
|
+
assert_equal Dummy.all.map(&:name), ['MassUpdated', 'original 2', 'MassUpdated']
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'Destroy' do
|
102
|
+
setup do
|
103
|
+
@instance = Dummy.create(name: 'Original')
|
104
|
+
@op = Ector::Multi::Operation::Destroy.new(:t, lambda { |_| @instance } )
|
105
|
+
end
|
106
|
+
|
107
|
+
test 'it does not fail fast' do
|
108
|
+
refute @op.fail_fast?
|
109
|
+
end
|
110
|
+
|
111
|
+
test 'returns the object destroyed' do
|
112
|
+
res = @op.run({})
|
113
|
+
|
114
|
+
assert res.frozen?
|
115
|
+
assert_equal res.class, Dummy
|
116
|
+
assert_equal res.id, @instance.id
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'DestroyAll' do
|
121
|
+
setup do
|
122
|
+
@instance = Dummy.create(name: 'Original')
|
123
|
+
@op = Ector::Multi::Operation::DestroyAll.new(:t, lambda { |_| Dummy })
|
124
|
+
end
|
125
|
+
|
126
|
+
test 'it does not fail fast' do
|
127
|
+
refute @op.fail_fast?
|
128
|
+
end
|
129
|
+
|
130
|
+
test 'returns a list of instances destroyed' do
|
131
|
+
res = @op.run(destroy: 'all')
|
132
|
+
|
133
|
+
assert_equal res, [@instance]
|
134
|
+
assert_equal Dummy.count, 0
|
135
|
+
assert_raise(ActiveRecord::RecordNotFound) { @instance.reload }
|
136
|
+
end
|
137
|
+
|
138
|
+
test 'accepts a dataset' do
|
139
|
+
Dummy.create(name: 'original 2')
|
140
|
+
Dummy.create(name: 'original 3')
|
141
|
+
|
142
|
+
@op = Ector::Multi::Operation::DestroyAll.new(:t, lambda { |_| Dummy.where.not(name: 'original 2') } )
|
143
|
+
res = @op.run(destroy: 'with where')
|
144
|
+
|
145
|
+
assert_equal res.map(&:name), ['Original', 'original 3']
|
146
|
+
assert_equal Dummy.count, 1
|
147
|
+
assert_equal Dummy.all.map(&:name), ['original 2']
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context 'Lambda' do
|
152
|
+
setup do
|
153
|
+
@lambda = Proc.new { [true, 1] }
|
154
|
+
@op = Ector::Multi::Operation::Lambda.new(:t, @lambda )
|
155
|
+
end
|
156
|
+
|
157
|
+
test 'it does not fail fast' do
|
158
|
+
refute @op.fail_fast?
|
159
|
+
end
|
160
|
+
|
161
|
+
test 'returns the return value of the block' do
|
162
|
+
res = @op.run({})
|
163
|
+
|
164
|
+
assert_equal res, [true, 1]
|
165
|
+
assert_equal res, @lambda.call
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context 'Error' do
|
170
|
+
setup do
|
171
|
+
@op = Ector::Multi::Operation::Error.new(:t, lambda { |r| 1} )
|
172
|
+
end
|
173
|
+
|
174
|
+
test 'it does not fail fast' do
|
175
|
+
assert @op.fail_fast?
|
176
|
+
end
|
177
|
+
|
178
|
+
test 'returns the return value of the block' do
|
179
|
+
assert_raise(Ector::Multi::ControlledFailure) { @op.run({}) }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
data/test/result_test.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
Protest.describe 'Ector::Multi::Result' do
|
6
|
+
test 'attribute readers' do
|
7
|
+
results = { operation: 1, procedure: 2 }
|
8
|
+
error = Ector::Multi::Rollback.new
|
9
|
+
result = Ector::Multi::Result.new(results, error)
|
10
|
+
|
11
|
+
assert_equal result.results, results
|
12
|
+
assert_equal result.error, error
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'succcess?' do
|
16
|
+
test 'returns `true` when it succeeded' do
|
17
|
+
result = Ector::Multi::Result.new({})
|
18
|
+
|
19
|
+
assert result.success?
|
20
|
+
end
|
21
|
+
|
22
|
+
test 'returns `false` when there is an error' do
|
23
|
+
result = Ector::Multi::Result.new({}, Ector::Multi::Rollback.new)
|
24
|
+
|
25
|
+
refute result.success?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'failure?' do
|
30
|
+
test 'returns `true` when it failed' do
|
31
|
+
result = Ector::Multi::Result.new({}, Ector::Multi::Rollback.new)
|
32
|
+
|
33
|
+
assert result.failure?
|
34
|
+
end
|
35
|
+
|
36
|
+
test 'returns `false` when there is no error' do
|
37
|
+
result = Ector::Multi::Result.new({})
|
38
|
+
|
39
|
+
refute result.failure?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ector-multi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Emiliano Mancuso
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-12-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: protest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Grouping multiple DB operations in a single transaction. 100% Inspired
|
56
|
+
by Ecto
|
57
|
+
email:
|
58
|
+
- emiliano.mancuso@gmail.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- README.md
|
64
|
+
- ector-multi.gemspec
|
65
|
+
- lib/ector-multi.rb
|
66
|
+
- lib/ector-multi/errors.rb
|
67
|
+
- lib/ector-multi/multi.rb
|
68
|
+
- lib/ector-multi/operations.rb
|
69
|
+
- lib/ector-multi/result.rb
|
70
|
+
- rakefile
|
71
|
+
- test/helper.rb
|
72
|
+
- test/multi_test.rb
|
73
|
+
- test/operations_test.rb
|
74
|
+
- test/result_test.rb
|
75
|
+
homepage: http://github.com/emancu/ector-multi
|
76
|
+
licenses:
|
77
|
+
- MIT
|
78
|
+
metadata: {}
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '2.2'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 2.7.8
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Grouping multiple DB operations in a single transaction.
|
99
|
+
test_files:
|
100
|
+
- test/helper.rb
|
101
|
+
- test/multi_test.rb
|
102
|
+
- test/operations_test.rb
|
103
|
+
- test/result_test.rb
|