ector-multi 0.0.1
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 +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
|