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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aef74ea6f4a839ec7a9fc349cf07cb44a53001e4
|
4
|
+
data.tar.gz: 8c2a8da38743766e795050d5fe67b1102ec44f60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5215f833089ae23c768c3b9763438e72d43030cb87c8e5d99eb85c61e1736d837c4f4d356a7b7b8708ac3de8136b18f4fb3b9bc2653c7e6c981bebc015b35bfa
|
7
|
+
data.tar.gz: 62ab5e5d9c9b8d984a0bfa098bea336238491ec6546783ae41757dc9922d6f5c7c491a1a3c537fce0bd46d95fb29fc7a8eaa9900ad647f8c66307c8fcbd9e856
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.2
|
data/.travis.yml
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# .travis.yml
|
2
2
|
language: ruby
|
3
3
|
rvm:
|
4
|
-
- 2.
|
5
|
-
|
6
|
-
-
|
4
|
+
- 2.1.2
|
5
|
+
before_install:
|
6
|
+
- sudo apt-add-repository -y ppa:travis-ci/sqlite3
|
7
|
+
- sudo apt-get -y update
|
8
|
+
- sudo apt-get install sqlite3=3.7.15.1-1~travis1
|
7
9
|
script:
|
8
10
|
- bundle exec rake test
|
data/abstract_importer.gemspec
CHANGED
@@ -17,7 +17,8 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
spec.add_dependency "activerecord"
|
20
|
+
spec.add_dependency "activerecord", ">= 4.0"
|
21
|
+
spec.add_dependency "activerecord-insert_many", "~> 0.1.1"
|
21
22
|
|
22
23
|
spec.add_development_dependency "bundler", "~> 1.3"
|
23
24
|
spec.add_development_dependency "minitest", "~> 4.7"
|
@@ -9,29 +9,29 @@ require 'abstract_importer/summary'
|
|
9
9
|
|
10
10
|
module AbstractImporter
|
11
11
|
class Base
|
12
|
-
|
12
|
+
|
13
13
|
class << self
|
14
14
|
def import
|
15
15
|
yield @import_plan = ImportPlan.new
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def depends_on(*dependencies)
|
19
19
|
@dependencies = dependencies
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
attr_reader :import_plan, :dependencies
|
23
23
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
|
25
|
+
|
26
|
+
|
27
27
|
def initialize(parent, source, options={})
|
28
28
|
@source = source
|
29
29
|
@parent = parent
|
30
|
-
|
30
|
+
|
31
31
|
io = options.fetch(:io, $stderr)
|
32
32
|
@reporter = default_reporter(io)
|
33
33
|
@dry_run = options.fetch(:dry_run, false)
|
34
|
-
|
34
|
+
|
35
35
|
@id_map = IdMap.new
|
36
36
|
@results = {}
|
37
37
|
@import_plan = self.class.import_plan.to_h
|
@@ -41,171 +41,159 @@ module AbstractImporter
|
|
41
41
|
@only = Array(options[:only]) if options.key?(:only)
|
42
42
|
@collections = []
|
43
43
|
end
|
44
|
-
|
44
|
+
|
45
45
|
attr_reader :source, :parent, :reporter, :id_map, :results
|
46
|
-
|
46
|
+
|
47
47
|
def atomic?
|
48
48
|
@atomic
|
49
49
|
end
|
50
|
-
|
50
|
+
|
51
51
|
def dry_run?
|
52
52
|
@dry_run
|
53
53
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
|
59
59
|
def perform!
|
60
60
|
reporter.start_all(self)
|
61
|
-
|
61
|
+
|
62
62
|
ms = Benchmark.ms do
|
63
63
|
setup
|
64
64
|
end
|
65
65
|
reporter.finish_setup(ms)
|
66
|
-
|
66
|
+
|
67
67
|
ms = Benchmark.ms do
|
68
68
|
with_transaction do
|
69
69
|
collections.each &method(:import_collection)
|
70
70
|
end
|
71
71
|
end
|
72
|
-
|
72
|
+
|
73
73
|
teardown
|
74
74
|
reporter.finish_all(self, ms)
|
75
75
|
results
|
76
76
|
end
|
77
|
-
|
77
|
+
|
78
78
|
def setup
|
79
79
|
verify_source!
|
80
80
|
verify_parent!
|
81
81
|
instantiate_collections!
|
82
82
|
prepopulate_id_map!
|
83
83
|
end
|
84
|
-
|
84
|
+
|
85
85
|
def import_collection(collection)
|
86
86
|
return if skip? collection
|
87
87
|
results[collection.name] = CollectionImporter.new(self, collection).perform!
|
88
88
|
end
|
89
|
-
|
89
|
+
|
90
90
|
def teardown
|
91
91
|
end
|
92
|
-
|
92
|
+
|
93
93
|
def skip?(collection)
|
94
94
|
return true if skip.member?(collection.name)
|
95
95
|
return true if only && !only.member?(collection.name)
|
96
96
|
false
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
def strategy_for(collection)
|
100
100
|
strategy_name = @strategies.fetch collection.name, :default
|
101
101
|
AbstractImporter::Strategies.const_get :"#{strategy_name.capitalize}Strategy"
|
102
102
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
103
|
+
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
|
108
108
|
def describe_source
|
109
109
|
source.to_s
|
110
110
|
end
|
111
|
-
|
111
|
+
|
112
112
|
def describe_destination
|
113
113
|
parent.to_s
|
114
114
|
end
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
|
119
|
+
|
120
120
|
def remap_foreign_key?(plural, foreign_key)
|
121
121
|
true
|
122
122
|
end
|
123
|
-
|
123
|
+
|
124
124
|
def map_foreign_key(legacy_id, plural, foreign_key, depends_on)
|
125
125
|
id_map.apply!(legacy_id, depends_on)
|
126
126
|
rescue IdMap::IdNotMappedError
|
127
127
|
record_no_id_in_map_error(legacy_id, plural, foreign_key, depends_on)
|
128
128
|
nil
|
129
129
|
end
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
130
|
+
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
|
135
135
|
private
|
136
|
-
|
136
|
+
|
137
137
|
attr_reader :collections, :import_plan, :skip, :only
|
138
|
-
|
138
|
+
|
139
139
|
def verify_source!
|
140
140
|
import_plan.keys.each do |collection|
|
141
141
|
next if source.respond_to?(collection)
|
142
|
-
|
142
|
+
|
143
143
|
raise "#{source.class} does not respond to `#{collection}`; " <<
|
144
|
-
"but #{self.class} plans to import records with that name"
|
144
|
+
"but #{self.class} plans to import records with that name"
|
145
145
|
end
|
146
146
|
end
|
147
|
-
|
147
|
+
|
148
148
|
def verify_parent!
|
149
149
|
import_plan.keys.each do |collection|
|
150
150
|
next if parent.respond_to?(collection)
|
151
|
-
|
151
|
+
|
152
152
|
raise "#{parent.class} does not have a collection named `#{collection}`; " <<
|
153
153
|
"but #{self.class} plans to import records with that name"
|
154
154
|
end
|
155
155
|
end
|
156
|
-
|
156
|
+
|
157
157
|
def instantiate_collections!
|
158
158
|
@collections = import_plan.map do |name, block|
|
159
159
|
reflection = parent.class.reflect_on_association(name)
|
160
160
|
model = reflection.klass
|
161
161
|
table_name = model.table_name
|
162
162
|
scope = parent.public_send(name)
|
163
|
-
|
163
|
+
|
164
164
|
options = ImportOptions.new
|
165
165
|
instance_exec(options, &block) if block
|
166
|
-
|
166
|
+
|
167
167
|
Collection.new(name, model, table_name, scope, options)
|
168
168
|
end
|
169
169
|
end
|
170
|
-
|
170
|
+
|
171
171
|
def dependencies
|
172
172
|
@dependencies ||= Array(self.class.dependencies).map do |name|
|
173
173
|
reflection = parent.class.reflect_on_association(name)
|
174
174
|
model = reflection.klass
|
175
175
|
table_name = model.table_name
|
176
176
|
scope = parent.public_send(name)
|
177
|
-
|
177
|
+
|
178
178
|
Collection.new(name, model, table_name, scope, nil)
|
179
179
|
end
|
180
180
|
end
|
181
|
-
|
181
|
+
|
182
182
|
def prepopulate_id_map!
|
183
183
|
(collections + dependencies).each do |collection|
|
184
|
-
|
185
|
-
|
186
|
-
.each_with_object({}) { |(id, legacy_id), map| map[legacy_id] = id }
|
187
|
-
|
188
|
-
id_map.init collection.table_name, map
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
def values_of(query, *columns)
|
193
|
-
if Rails.version < "4.0.0"
|
194
|
-
query = query.select(columns.map { |column| "#{query.table_name}.#{column}" }.join(", "))
|
195
|
-
ActiveRecord::Base.connection.select_rows(query.to_sql)
|
196
|
-
else
|
197
|
-
query.pluck(*columns)
|
184
|
+
id_map.init collection.table_name, collection.scope
|
185
|
+
.where("#{collection.table_name}.legacy_id IS NOT NULL")
|
198
186
|
end
|
199
187
|
end
|
200
|
-
|
201
|
-
|
202
|
-
|
188
|
+
|
189
|
+
|
190
|
+
|
203
191
|
def record_no_id_in_map_error(legacy_id, plural, foreign_key, depends_on)
|
204
192
|
reporter.count_notice "#{plural}.#{foreign_key} will be nil: a #{depends_on.to_s.singularize} with the legacy id #{legacy_id} was not mapped."
|
205
193
|
end
|
206
|
-
|
207
|
-
|
208
|
-
|
194
|
+
|
195
|
+
|
196
|
+
|
209
197
|
def with_transaction(&block)
|
210
198
|
if atomic?
|
211
199
|
ActiveRecord::Base.transaction(requires_new: true, &block)
|
@@ -213,7 +201,7 @@ module AbstractImporter
|
|
213
201
|
block.call
|
214
202
|
end
|
215
203
|
end
|
216
|
-
|
204
|
+
|
217
205
|
def default_reporter(io)
|
218
206
|
case ENV["IMPORT_REPORTER"].to_s.downcase
|
219
207
|
when "none" then Reporters::NullReporter.new(io)
|
@@ -221,6 +209,6 @@ module AbstractImporter
|
|
221
209
|
else Reporters::DebugReporter.new(io)
|
222
210
|
end
|
223
211
|
end
|
224
|
-
|
212
|
+
|
225
213
|
end
|
226
214
|
end
|
@@ -2,15 +2,15 @@ require "abstract_importer/strategies"
|
|
2
2
|
|
3
3
|
module AbstractImporter
|
4
4
|
class CollectionImporter
|
5
|
-
|
5
|
+
|
6
6
|
def initialize(importer, collection)
|
7
7
|
@importer = importer
|
8
8
|
@collection = collection
|
9
9
|
@strategy = importer.strategy_for(collection).new(self)
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
attr_reader :importer, :collection, :summary, :strategy
|
13
|
-
|
13
|
+
|
14
14
|
delegate :name,
|
15
15
|
:table_name,
|
16
16
|
:model,
|
@@ -18,7 +18,7 @@ module AbstractImporter
|
|
18
18
|
:options,
|
19
19
|
:association_attrs,
|
20
20
|
:to => :collection
|
21
|
-
|
21
|
+
|
22
22
|
delegate :dry_run?,
|
23
23
|
:parent,
|
24
24
|
:source,
|
@@ -27,46 +27,47 @@ module AbstractImporter
|
|
27
27
|
:id_map,
|
28
28
|
:map_foreign_key,
|
29
29
|
:to => :importer
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
|
31
|
+
|
32
|
+
|
33
33
|
def perform!
|
34
34
|
reporter.start_collection(self)
|
35
35
|
prepare!
|
36
|
-
|
36
|
+
|
37
37
|
invoke_callback(:before_all)
|
38
38
|
summary.ms = Benchmark.ms do
|
39
39
|
each_new_record do |attributes|
|
40
40
|
strategy.process_record(attributes)
|
41
41
|
end
|
42
42
|
end
|
43
|
+
strategy.flush
|
43
44
|
invoke_callback(:after_all)
|
44
|
-
|
45
|
+
|
45
46
|
reporter.finish_collection(self, summary)
|
46
47
|
summary
|
47
48
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
49
|
+
|
50
|
+
|
51
|
+
|
51
52
|
def prepare!
|
52
53
|
@summary = Summary.new
|
53
54
|
@mappings = prepare_mappings!
|
54
55
|
end
|
55
|
-
|
56
|
+
|
56
57
|
def prepare_mappings!
|
57
58
|
mappings = []
|
58
59
|
model.reflect_on_all_associations.each do |association|
|
59
|
-
|
60
|
+
|
60
61
|
# We only want the associations where this record
|
61
62
|
# has foreign keys that refer to another
|
62
63
|
next unless association.macro == :belongs_to
|
63
|
-
|
64
|
+
|
64
65
|
# We support skipping some mappings entirely. I believe
|
65
66
|
# this is largely to cut down on verbosity in the log
|
66
67
|
# files and should be refactored to another place in time.
|
67
68
|
foreign_key = association.foreign_key.to_sym
|
68
69
|
next unless remap_foreign_key?(name, foreign_key)
|
69
|
-
|
70
|
+
|
70
71
|
if association.options[:polymorphic]
|
71
72
|
mappings << prepare_polymorphic_mapping!(association)
|
72
73
|
else
|
@@ -75,11 +76,11 @@ module AbstractImporter
|
|
75
76
|
end
|
76
77
|
mappings
|
77
78
|
end
|
78
|
-
|
79
|
+
|
79
80
|
def prepare_mapping!(association)
|
80
81
|
depends_on = association.table_name.to_sym
|
81
82
|
foreign_key = association.foreign_key.to_sym
|
82
|
-
|
83
|
+
|
83
84
|
Proc.new do |attrs|
|
84
85
|
if attrs.key?(foreign_key)
|
85
86
|
attrs[foreign_key] = map_foreign_key(attrs[foreign_key], name, foreign_key, depends_on)
|
@@ -88,11 +89,11 @@ module AbstractImporter
|
|
88
89
|
end
|
89
90
|
end
|
90
91
|
end
|
91
|
-
|
92
|
+
|
92
93
|
def prepare_polymorphic_mapping!(association)
|
93
94
|
foreign_key = association.foreign_key.to_sym
|
94
95
|
foreign_type = association.foreign_key.gsub(/_id$/, "_type").to_sym
|
95
|
-
|
96
|
+
|
96
97
|
Proc.new do |attrs|
|
97
98
|
if attrs.key?(foreign_key) && attrs.key?(foreign_type)
|
98
99
|
foreign_model = attrs[foreign_type]
|
@@ -103,23 +104,23 @@ module AbstractImporter
|
|
103
104
|
end
|
104
105
|
end
|
105
106
|
end
|
106
|
-
|
107
|
-
|
108
|
-
|
107
|
+
|
108
|
+
|
109
|
+
|
109
110
|
def each_new_record
|
110
111
|
source.public_send(name).each do |attrs|
|
111
112
|
yield attrs.dup
|
112
113
|
end
|
113
114
|
end
|
114
|
-
|
115
|
-
|
116
|
-
|
115
|
+
|
116
|
+
|
117
|
+
|
117
118
|
def remap_foreign_keys!(hash)
|
118
119
|
@mappings.each do |proc|
|
119
120
|
proc.call(hash)
|
120
121
|
end
|
121
122
|
end
|
122
|
-
|
123
|
+
|
123
124
|
def redundant_record?(hash)
|
124
125
|
existing_record = invoke_callback(:finder, hash)
|
125
126
|
if existing_record
|
@@ -129,9 +130,9 @@ module AbstractImporter
|
|
129
130
|
false
|
130
131
|
end
|
131
132
|
end
|
132
|
-
|
133
|
-
|
134
|
-
|
133
|
+
|
134
|
+
|
135
|
+
|
135
136
|
def invoke_callback(callback, *args)
|
136
137
|
callback_name = :"#{callback}_callback"
|
137
138
|
callback = options.public_send(callback_name)
|
@@ -139,9 +140,9 @@ module AbstractImporter
|
|
139
140
|
callback = importer.method(callback) if callback.is_a?(Symbol)
|
140
141
|
callback.call(*args)
|
141
142
|
end
|
142
|
-
|
143
|
+
|
143
144
|
end
|
144
|
-
|
145
|
+
|
145
146
|
class Skip < StandardError; end
|
146
|
-
|
147
|
+
|
147
148
|
end
|