abstract_importer 1.2.1 → 1.3.0
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/.ruby-version +1 -0
- data/.travis.yml +5 -3
- data/abstract_importer.gemspec +2 -1
- data/lib/abstract_importer/base.rb +63 -75
- data/lib/abstract_importer/collection_importer.rb +34 -33
- data/lib/abstract_importer/id_map.rb +22 -14
- data/lib/abstract_importer/import_options.rb +2 -0
- data/lib/abstract_importer/import_plan.rb +4 -4
- data/lib/abstract_importer/reporters/base_reporter.rb +22 -22
- data/lib/abstract_importer/reporters/debug_reporter.rb +36 -36
- data/lib/abstract_importer/reporters/null_reporter.rb +5 -5
- data/lib/abstract_importer/reporters/performance_reporter.rb +17 -17
- data/lib/abstract_importer/strategies.rb +1 -0
- data/lib/abstract_importer/strategies/base.rb +12 -0
- data/lib/abstract_importer/strategies/default_strategy.rb +1 -7
- data/lib/abstract_importer/strategies/insert_strategy.rb +58 -0
- data/lib/abstract_importer/summary.rb +3 -3
- data/lib/abstract_importer/version.rb +1 -1
- data/test/callback_test.rb +29 -29
- data/test/{importer_test.rb → default_strategy_test.rb} +49 -77
- data/test/insert_strategy_test.rb +82 -0
- data/test/replace_strategy_test.rb +29 -0
- data/test/support/mock_data_source.rb +10 -10
- data/test/support/mock_objects.rb +1 -1
- data/test/support/schema.rb +7 -6
- data/test/test_helper.rb +10 -10
- metadata +27 -7
@@ -11,6 +11,7 @@ module AbstractImporter
|
|
11
11
|
:id_map,
|
12
12
|
:scope,
|
13
13
|
:reporter,
|
14
|
+
:association_attrs,
|
14
15
|
to: :collection
|
15
16
|
|
16
17
|
def initialize(collection)
|
@@ -25,6 +26,17 @@ module AbstractImporter
|
|
25
26
|
id_map.contains? collection.table_name, hash[:id]
|
26
27
|
end
|
27
28
|
|
29
|
+
def flush
|
30
|
+
end
|
31
|
+
|
32
|
+
def prepare_attributes(hash)
|
33
|
+
hash = invoke_callback(:before_build, hash) || hash
|
34
|
+
|
35
|
+
legacy_id = hash.delete(:id)
|
36
|
+
|
37
|
+
hash.merge(legacy_id: legacy_id).merge(association_attrs)
|
38
|
+
end
|
39
|
+
|
28
40
|
end
|
29
41
|
end
|
30
42
|
end
|
@@ -58,13 +58,7 @@ module AbstractImporter
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def build_record(hash)
|
61
|
-
|
62
|
-
|
63
|
-
legacy_id = hash.delete(:id)
|
64
|
-
|
65
|
-
collection.model.new(hash
|
66
|
-
.merge(legacy_id: legacy_id)
|
67
|
-
.merge(collection.association_attrs))
|
61
|
+
collection.model.new prepare_attributes(hash)
|
68
62
|
end
|
69
63
|
|
70
64
|
def clean_record(record)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "abstract_importer/strategies/base"
|
2
|
+
require "activerecord/insert_many"
|
3
|
+
|
4
|
+
module AbstractImporter
|
5
|
+
module Strategies
|
6
|
+
class InsertStrategy < Base
|
7
|
+
|
8
|
+
def initialize(collection)
|
9
|
+
super
|
10
|
+
@batch = []
|
11
|
+
@batch_size = 250
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def process_record(hash)
|
16
|
+
summary.total += 1
|
17
|
+
|
18
|
+
if already_imported?(hash)
|
19
|
+
summary.already_imported += 1
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
remap_foreign_keys!(hash)
|
24
|
+
|
25
|
+
if redundant_record?(hash)
|
26
|
+
summary.redundant += 1
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
@batch << prepare_attributes(hash)
|
31
|
+
flush if @batch.length >= @batch_size
|
32
|
+
|
33
|
+
rescue ::AbstractImporter::Skip
|
34
|
+
summary.skipped += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def flush
|
39
|
+
invoke_callback(:before_batch, @batch)
|
40
|
+
|
41
|
+
begin
|
42
|
+
tries = (tries || 0) + 1
|
43
|
+
collection.scope.insert_many(@batch)
|
44
|
+
rescue
|
45
|
+
raise if tries > 1
|
46
|
+
invoke_callback(:rescue_batch, @batch)
|
47
|
+
retry
|
48
|
+
end
|
49
|
+
|
50
|
+
id_map.merge! collection.table_name, collection.scope
|
51
|
+
.where(legacy_id: @batch.map { |hash| hash[:legacy_id] })
|
52
|
+
@batch = []
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module AbstractImporter
|
2
2
|
class Summary < Struct.new(:total, :redundant, :created, :already_imported, :invalid, :ms, :skipped)
|
3
|
-
|
3
|
+
|
4
4
|
def initialize
|
5
5
|
super(0,0,0,0,0,0,0)
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
def average_ms
|
9
9
|
return nil if total == 0
|
10
10
|
ms / total
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
end
|
14
14
|
end
|
data/test/callback_test.rb
CHANGED
@@ -2,9 +2,9 @@ require "test_helper"
|
|
2
2
|
|
3
3
|
|
4
4
|
class CallbackTest < ActiveSupport::TestCase
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
|
6
|
+
|
7
|
+
|
8
8
|
context "before_build" do
|
9
9
|
setup do
|
10
10
|
plan do |import|
|
@@ -13,12 +13,12 @@ class CallbackTest < ActiveSupport::TestCase
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
should "should be invoked on the incoming attributes" do
|
18
18
|
import!
|
19
19
|
assert_equal ["Harry", "Ron", "Hermione"], account.students.pluck(:name)
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
should "allow you to skip certain records" do
|
23
23
|
plan do |import|
|
24
24
|
import.students do |options|
|
@@ -27,15 +27,15 @@ class CallbackTest < ActiveSupport::TestCase
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
import!
|
32
32
|
assert_equal ["Ron Weasley", "Hermione Granger"], account.students.pluck(:name)
|
33
33
|
assert_equal 1, results[:students].skipped
|
34
34
|
end
|
35
35
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
|
37
|
+
|
38
|
+
|
39
39
|
context "before_create" do
|
40
40
|
setup do
|
41
41
|
plan do |import|
|
@@ -44,15 +44,15 @@ class CallbackTest < ActiveSupport::TestCase
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
should "should be invoked on imported records before they are saved" do
|
49
49
|
import!
|
50
50
|
assert_equal ["Gryffindor"], account.students.pluck(:house).uniq
|
51
51
|
end
|
52
52
|
end
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
|
54
|
+
|
55
|
+
|
56
56
|
context "after_create" do
|
57
57
|
setup do
|
58
58
|
plan do |import|
|
@@ -61,7 +61,7 @@ class CallbackTest < ActiveSupport::TestCase
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
should "should be invoked after the record is created" do
|
66
66
|
mock(importer).callback({name: "Harry Potter"}, satisfy(&:persisted?)).once
|
67
67
|
mock(importer).callback({name: "Ron Weasley"}, satisfy(&:persisted?)).once
|
@@ -69,9 +69,9 @@ class CallbackTest < ActiveSupport::TestCase
|
|
69
69
|
import!
|
70
70
|
end
|
71
71
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
72
|
+
|
73
|
+
|
74
|
+
|
75
75
|
context "rescue" do
|
76
76
|
setup do
|
77
77
|
plan do |import|
|
@@ -80,15 +80,15 @@ class CallbackTest < ActiveSupport::TestCase
|
|
80
80
|
end
|
81
81
|
end
|
82
82
|
end
|
83
|
-
|
83
|
+
|
84
84
|
should "should be given a chance to amend an invalid record" do
|
85
85
|
import!
|
86
86
|
assert_equal ["godrics-hollow", "azkaban"], account.locations.pluck(:slug)
|
87
87
|
end
|
88
88
|
end
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
|
90
|
+
|
91
|
+
|
92
92
|
context "before_all" do
|
93
93
|
setup do
|
94
94
|
plan do |import|
|
@@ -97,15 +97,15 @@ class CallbackTest < ActiveSupport::TestCase
|
|
97
97
|
end
|
98
98
|
end
|
99
99
|
end
|
100
|
-
|
100
|
+
|
101
101
|
should "should be invoked before the collection has been imported" do
|
102
102
|
mock(importer).callback.once
|
103
103
|
import!
|
104
104
|
end
|
105
105
|
end
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
|
107
|
+
|
108
|
+
|
109
109
|
context "after_all" do
|
110
110
|
setup do
|
111
111
|
plan do |import|
|
@@ -114,13 +114,13 @@ class CallbackTest < ActiveSupport::TestCase
|
|
114
114
|
end
|
115
115
|
end
|
116
116
|
end
|
117
|
-
|
117
|
+
|
118
118
|
should "should be invoked after the collection has been imported" do
|
119
119
|
mock(importer).callback.once
|
120
120
|
import!
|
121
121
|
end
|
122
122
|
end
|
123
|
-
|
124
|
-
|
125
|
-
|
123
|
+
|
124
|
+
|
125
|
+
|
126
126
|
end
|
@@ -1,30 +1,30 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
class DefaultStrategyTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
|
7
|
+
|
8
8
|
context "with a simple data source" do
|
9
9
|
setup do
|
10
10
|
plan do |import|
|
11
11
|
import.students
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
should "import the given records" do
|
16
16
|
import!
|
17
17
|
assert_equal ["Harry Potter", "Ron Weasley", "Hermione Granger"], account.students.pluck(:name)
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
should "record their legacy_id" do
|
21
21
|
import!
|
22
22
|
assert_equal [456, 457, 458], account.students.pluck(:legacy_id)
|
23
23
|
end
|
24
24
|
end
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
|
26
|
+
|
27
|
+
|
28
28
|
context "with a complex data source" do
|
29
29
|
setup do
|
30
30
|
plan do |import|
|
@@ -32,19 +32,19 @@ class ImporterTest < ActiveSupport::TestCase
|
|
32
32
|
import.parents
|
33
33
|
end
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
should "preserve mappings" do
|
37
37
|
import!
|
38
38
|
harry = account.students.find_by_name("Harry Potter")
|
39
39
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
should "preserve mappings even when a record was previously imported" do
|
43
43
|
harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
|
44
44
|
import!
|
45
45
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
context "when {atomic: true}" do
|
49
49
|
should "rollback the whole import if an part fails" do
|
50
50
|
mock(importer).atomic? { true }
|
@@ -54,7 +54,7 @@ class ImporterTest < ActiveSupport::TestCase
|
|
54
54
|
assert_equal 0, account.students.count, "Expected students to have been rolled back"
|
55
55
|
end
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
context "when {atomic: false}" do
|
59
59
|
should "not rollback the whole import if an part fails" do
|
60
60
|
mock(importer).atomic? { false }
|
@@ -65,9 +65,9 @@ class ImporterTest < ActiveSupport::TestCase
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
68
|
-
|
69
|
-
|
70
|
-
|
68
|
+
|
69
|
+
|
70
|
+
|
71
71
|
context "with a dependency" do
|
72
72
|
setup do
|
73
73
|
depends_on :students
|
@@ -75,16 +75,16 @@ class ImporterTest < ActiveSupport::TestCase
|
|
75
75
|
import.parents
|
76
76
|
end
|
77
77
|
end
|
78
|
-
|
78
|
+
|
79
79
|
should "preserve mappings when a dependency was imported by another importer" do
|
80
80
|
harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
|
81
81
|
import!
|
82
82
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
83
83
|
end
|
84
84
|
end
|
85
|
-
|
86
|
-
|
87
|
-
|
85
|
+
|
86
|
+
|
87
|
+
|
88
88
|
context "when a finder is specified" do
|
89
89
|
setup do
|
90
90
|
plan do |import|
|
@@ -94,22 +94,22 @@ class ImporterTest < ActiveSupport::TestCase
|
|
94
94
|
import.parents
|
95
95
|
end
|
96
96
|
end
|
97
|
-
|
97
|
+
|
98
98
|
should "not import redundant records" do
|
99
99
|
account.students.create!(name: "Ron Weasley", legacy_id: nil)
|
100
100
|
import!
|
101
101
|
assert_equal 3, account.students.count
|
102
102
|
end
|
103
|
-
|
103
|
+
|
104
104
|
should "preserve mappings" do
|
105
105
|
harry = account.students.create!(name: "Harry Potter", legacy_id: nil)
|
106
106
|
import!
|
107
107
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
108
108
|
end
|
109
109
|
end
|
110
|
-
|
111
|
-
|
112
|
-
|
110
|
+
|
111
|
+
|
112
|
+
|
113
113
|
context "with a more complex data source" do
|
114
114
|
setup do
|
115
115
|
plan do |import|
|
@@ -124,16 +124,16 @@ class ImporterTest < ActiveSupport::TestCase
|
|
124
124
|
import.grades
|
125
125
|
end
|
126
126
|
end
|
127
|
-
|
127
|
+
|
128
128
|
should "preserve mappings" do
|
129
129
|
import!
|
130
130
|
ron = account.students.find_by_name "Ron Weasley"
|
131
131
|
assert_equal ["Advanced Potions: Acceptable", "History of Magic: Troll"], ron.report_card
|
132
132
|
end
|
133
133
|
end
|
134
|
-
|
135
|
-
|
136
|
-
|
134
|
+
|
135
|
+
|
136
|
+
|
137
137
|
context "with polymorphic associations" do
|
138
138
|
setup do
|
139
139
|
plan do |import|
|
@@ -142,7 +142,7 @@ class ImporterTest < ActiveSupport::TestCase
|
|
142
142
|
import.students
|
143
143
|
end
|
144
144
|
end
|
145
|
-
|
145
|
+
|
146
146
|
should "preserve mappings" do
|
147
147
|
import!
|
148
148
|
assert_equal 2, account.students.map(&:pet).compact.count, "Expected two students to still be linked to their pets upon import"
|
@@ -150,30 +150,25 @@ class ImporterTest < ActiveSupport::TestCase
|
|
150
150
|
assert_kind_of Cat, account.students.find_by_name("Hermione Granger").pet, "Expected Harry's pet to be a Cat"
|
151
151
|
end
|
152
152
|
end
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
context "When
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
context "When records already exist" do
|
157
157
|
setup do
|
158
158
|
plan do |import|
|
159
159
|
import.students
|
160
160
|
end
|
161
|
+
account.students.create!(name: "Ron Weasley", legacy_id: 457)
|
161
162
|
end
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
end
|
167
|
-
|
168
|
-
should "not import existing records twice" do
|
169
|
-
import!
|
170
|
-
assert_equal 3, account.students.count
|
171
|
-
end
|
163
|
+
|
164
|
+
should "not import existing records twice" do
|
165
|
+
import!
|
166
|
+
assert_equal 3, account.students.count
|
172
167
|
end
|
173
168
|
end
|
174
|
-
|
175
|
-
|
176
|
-
|
169
|
+
|
170
|
+
|
171
|
+
|
177
172
|
context "When we specify collections to skip" do
|
178
173
|
setup do
|
179
174
|
plan do |import|
|
@@ -181,24 +176,24 @@ class ImporterTest < ActiveSupport::TestCase
|
|
181
176
|
import.parents
|
182
177
|
end
|
183
178
|
end
|
184
|
-
|
179
|
+
|
185
180
|
context "using :skip" do
|
186
181
|
setup do
|
187
182
|
options.merge!(skip: :parents)
|
188
183
|
end
|
189
|
-
|
184
|
+
|
190
185
|
should "not import the named collections" do
|
191
186
|
import!
|
192
187
|
assert_equal 3, account.students.length
|
193
188
|
assert_equal 0, account.parents.length
|
194
189
|
end
|
195
190
|
end
|
196
|
-
|
191
|
+
|
197
192
|
context "using :only" do
|
198
193
|
setup do
|
199
194
|
options.merge!(only: [:students])
|
200
195
|
end
|
201
|
-
|
196
|
+
|
202
197
|
should "import only the named collections" do
|
203
198
|
import!
|
204
199
|
assert_equal 3, account.students.length
|
@@ -206,30 +201,7 @@ class ImporterTest < ActiveSupport::TestCase
|
|
206
201
|
end
|
207
202
|
end
|
208
203
|
end
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
context "When we use the :replace strategy" do
|
213
|
-
setup do
|
214
|
-
options.merge!(strategy: {students: :replace})
|
215
|
-
plan do |import|
|
216
|
-
import.students
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
context "and records already exist" do
|
221
|
-
setup do
|
222
|
-
account.students.create!(name: "Ron Weasley", legacy_id: 457)
|
223
|
-
end
|
224
|
-
|
225
|
-
should "reimport the existing records" do
|
226
|
-
import!
|
227
|
-
assert_equal "Gryffindor", account.students.find_by_name("Ron Weasley").house,
|
228
|
-
"Expected Ron's record to have been replaced with one that has a house"
|
229
|
-
end
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
|
234
|
-
|
204
|
+
|
205
|
+
|
206
|
+
|
235
207
|
end
|