abstract_importer 1.3.4 → 1.4.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 -1
- data/.travis.yml +13 -10
- data/Rakefile +4 -5
- data/abstract_importer.gemspec +10 -9
- data/lib/abstract_importer.rb +2 -2
- data/lib/abstract_importer/base.rb +113 -48
- data/lib/abstract_importer/collection.rb +13 -6
- data/lib/abstract_importer/collection_importer.rb +4 -1
- data/lib/abstract_importer/id_map.rb +18 -16
- data/lib/abstract_importer/import_options.rb +0 -1
- data/lib/abstract_importer/reporters.rb +6 -5
- data/lib/abstract_importer/reporters/base_reporter.rb +7 -2
- data/lib/abstract_importer/reporters/debug_reporter.rb +1 -8
- data/lib/abstract_importer/reporters/dot_reporter.rb +5 -0
- data/lib/abstract_importer/reporters/null_reporter.rb +1 -1
- data/lib/abstract_importer/reporters/progress_reporter.rb +42 -0
- data/lib/abstract_importer/strategies.rb +1 -0
- data/lib/abstract_importer/strategies/base.rb +13 -3
- data/lib/abstract_importer/strategies/default_strategy.rb +1 -1
- data/lib/abstract_importer/strategies/insert_strategy.rb +27 -15
- data/lib/abstract_importer/strategies/replace_strategy.rb +1 -1
- data/lib/abstract_importer/strategies/upsert_strategy.rb +25 -0
- data/lib/abstract_importer/summary.rb +13 -2
- data/lib/abstract_importer/version.rb +1 -1
- data/test/callback_test.rb +3 -3
- data/test/id_map_test.rb +18 -0
- data/test/insert_strategy_test.rb +28 -11
- data/test/support/mock_data_source.rb +6 -0
- data/test/support/mock_objects.rb +6 -0
- data/test/support/schema.rb +15 -0
- data/test/test_helper.rb +10 -6
- data/test/upsert_strategy_test.rb +92 -0
- metadata +49 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a64bcc8c97755427d62459b7f1089c22d3806501
|
4
|
+
data.tar.gz: d8ef7bf828aa70c7294dd77f8d50a93857324a57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 830be8e79c0c5fbc3c8cc7fa372fc808f689dc78bc8518143a0395ad13a5dc1d7cc88ad6ebdbc81c1427688e19a40f5c5902282fa6126975ced79e5a034f4f25
|
7
|
+
data.tar.gz: 596609a81ff049f029baad8492aa8b4902c20e31e6a53237b02011c0a4cb7ed3d77070f93e27849b5c70f980852eb5747fc4d09667c9d49068a102a6f1108008
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.3.0
|
data/.travis.yml
CHANGED
@@ -1,14 +1,17 @@
|
|
1
|
-
# .travis.yml
|
2
1
|
language: ruby
|
3
2
|
rvm:
|
4
|
-
- 2.1
|
3
|
+
- 2.3.1
|
5
4
|
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
5
|
+
# Use Postgres 9.5
|
6
|
+
# https://www.brandur.org/fragments/postgres-95-travis
|
7
|
+
dist: trusty
|
8
|
+
sudo: required
|
9
|
+
addons:
|
10
|
+
postgresql: "9.5"
|
12
11
|
|
13
|
-
|
14
|
-
|
12
|
+
before_install: gem update bundler
|
13
|
+
script: bundle exec rake test
|
14
|
+
|
15
|
+
# To stop Travis from running tests for a new commit,
|
16
|
+
# add the following to your commit message: [ci skip]
|
17
|
+
# You should add this when you edit documentation or comments, etc.
|
data/Rakefile
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
-
require
|
2
|
+
require "rake/testtask"
|
3
3
|
|
4
4
|
Rake::TestTask.new(:test) do |t|
|
5
|
-
t.libs <<
|
6
|
-
t.libs <<
|
7
|
-
t.
|
8
|
-
t.verbose = false
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList['test/**/*_test.rb']
|
9
8
|
end
|
data/abstract_importer.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
4
|
+
require "abstract_importer/version"
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "abstract_importer"
|
@@ -16,20 +16,21 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
|
-
|
20
|
-
spec.add_dependency "activerecord", ">=
|
21
|
-
spec.add_dependency "
|
22
|
-
|
19
|
+
|
20
|
+
spec.add_dependency "activerecord", ">= 5.0"
|
21
|
+
spec.add_dependency "activesupport", ">= 5.0"
|
22
|
+
spec.add_dependency "activerecord-insert_many", ">= 0.4.0"
|
23
|
+
spec.add_dependency "progressbar"
|
24
|
+
|
23
25
|
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
-
spec.add_development_dependency "minitest"
|
26
|
+
spec.add_development_dependency "minitest-reporters"
|
27
|
+
spec.add_development_dependency "minitest-reporters-turn_reporter"
|
25
28
|
spec.add_development_dependency "rake"
|
26
|
-
spec.add_development_dependency "rails"
|
27
29
|
spec.add_development_dependency "sqlite3"
|
28
|
-
spec.add_development_dependency "
|
30
|
+
spec.add_development_dependency "pg"
|
29
31
|
spec.add_development_dependency "pry"
|
30
32
|
spec.add_development_dependency "rr"
|
31
33
|
spec.add_development_dependency "database_cleaner"
|
32
34
|
spec.add_development_dependency "simplecov"
|
33
35
|
spec.add_development_dependency "shoulda-context"
|
34
|
-
|
35
36
|
end
|
data/lib/abstract_importer.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "abstract_importer/base"
|
2
|
+
require "abstract_importer/version"
|
@@ -1,13 +1,15 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
1
|
+
require "active_support/core_ext/module/delegation"
|
2
|
+
require "abstract_importer/import_options"
|
3
|
+
require "abstract_importer/import_plan"
|
4
|
+
require "abstract_importer/reporters"
|
5
|
+
require "abstract_importer/collection"
|
6
|
+
require "abstract_importer/collection_importer"
|
7
|
+
require "abstract_importer/id_map"
|
8
|
+
require "abstract_importer/summary"
|
8
9
|
|
9
10
|
|
10
11
|
module AbstractImporter
|
12
|
+
class IdNotMappedError < StandardError; end
|
11
13
|
class Base
|
12
14
|
|
13
15
|
class << self
|
@@ -27,22 +29,51 @@ module AbstractImporter
|
|
27
29
|
def initialize(parent, source, options={})
|
28
30
|
@source = source
|
29
31
|
@parent = parent
|
32
|
+
@options = options
|
30
33
|
|
31
34
|
io = options.fetch(:io, $stderr)
|
32
|
-
@reporter = default_reporter(io)
|
35
|
+
@reporter = default_reporter(options, io)
|
33
36
|
@dry_run = options.fetch(:dry_run, false)
|
34
37
|
|
35
|
-
|
36
|
-
@results = {}
|
38
|
+
id_map = options.fetch(:id_map, true)
|
37
39
|
@import_plan = self.class.import_plan.to_h
|
38
40
|
@atomic = options.fetch(:atomic, false)
|
39
41
|
@strategies = options.fetch(:strategy, {})
|
42
|
+
@generate_id = options[:generate_id]
|
40
43
|
@skip = Array(options[:skip])
|
41
44
|
@only = Array(options[:only]) if options.key?(:only)
|
42
45
|
@collections = []
|
46
|
+
|
47
|
+
@use_id_map, @id_map = id_map.is_a?(IdMap) ? [true, id_map] : [id_map, IdMap.new]
|
48
|
+
|
49
|
+
verify_source!
|
50
|
+
verify_parent!
|
51
|
+
instantiate_collections!
|
52
|
+
|
53
|
+
@collection_importers = []
|
54
|
+
collections.each do |collection|
|
55
|
+
next if skip? collection
|
56
|
+
@collection_importers.push CollectionImporter.new(self, collection)
|
57
|
+
end
|
43
58
|
end
|
44
59
|
|
45
|
-
attr_reader :source,
|
60
|
+
attr_reader :source,
|
61
|
+
:parent,
|
62
|
+
:reporter,
|
63
|
+
:id_map,
|
64
|
+
:collections,
|
65
|
+
:import_plan,
|
66
|
+
:skip,
|
67
|
+
:only,
|
68
|
+
:collection_importers,
|
69
|
+
:options,
|
70
|
+
:generate_id
|
71
|
+
|
72
|
+
def use_id_map_for?(collection)
|
73
|
+
collection = find_collection(collection) if collection.is_a?(Symbol)
|
74
|
+
return false unless collection
|
75
|
+
@use_id_map && collection.has_legacy_id?
|
76
|
+
end
|
46
77
|
|
47
78
|
def atomic?
|
48
79
|
@atomic
|
@@ -57,48 +88,70 @@ module AbstractImporter
|
|
57
88
|
|
58
89
|
|
59
90
|
def perform!
|
60
|
-
|
91
|
+
{}.tap do |results|
|
92
|
+
reporter.start_all(self)
|
61
93
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
94
|
+
ms = Benchmark.ms do
|
95
|
+
setup
|
96
|
+
end
|
97
|
+
reporter.finish_setup(self, ms)
|
98
|
+
|
99
|
+
ms = Benchmark.ms do
|
100
|
+
with_transaction do
|
101
|
+
collection_importers.each do |importer|
|
102
|
+
results[importer.name] = importer.perform!
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
66
106
|
|
67
|
-
|
68
|
-
|
69
|
-
collections.each &method(:import_collection)
|
107
|
+
ms = Benchmark.ms do
|
108
|
+
teardown
|
70
109
|
end
|
71
|
-
|
110
|
+
reporter.finish_teardown(self, ms)
|
72
111
|
|
73
|
-
|
74
|
-
|
75
|
-
results
|
112
|
+
reporter.finish_all(self, ms)
|
113
|
+
end
|
76
114
|
end
|
77
115
|
|
78
116
|
def setup
|
79
|
-
verify_source!
|
80
|
-
verify_parent!
|
81
|
-
instantiate_collections!
|
82
117
|
prepopulate_id_map!
|
83
118
|
end
|
84
119
|
|
85
|
-
def
|
86
|
-
|
87
|
-
|
120
|
+
def count_collection(collection)
|
121
|
+
collection_name = collection.respond_to?(:name) ? collection.name : collection
|
122
|
+
collection_counts[collection_name]
|
123
|
+
end
|
124
|
+
|
125
|
+
def collection_counts
|
126
|
+
@collection_counts ||= Hash.new do |counts, collection_name|
|
127
|
+
counts[collection_name] = if self.source.respond_to?(:"#{collection_name}_count")
|
128
|
+
self.source.public_send(:"#{collection_name}_count")
|
129
|
+
else
|
130
|
+
self.source.public_send(collection_name).count
|
131
|
+
end
|
132
|
+
end
|
88
133
|
end
|
89
134
|
|
90
135
|
def teardown
|
91
136
|
end
|
92
137
|
|
93
138
|
def skip?(collection)
|
94
|
-
|
95
|
-
return true if
|
139
|
+
collection_name = collection.respond_to?(:name) ? collection.name : collection
|
140
|
+
return true if skip.member?(collection_name)
|
141
|
+
return true if only && !only.member?(collection_name)
|
96
142
|
false
|
97
143
|
end
|
98
144
|
|
99
|
-
def strategy_for(
|
145
|
+
def strategy_for(collection_importer)
|
146
|
+
collection = collection_importer.collection
|
100
147
|
strategy_name = @strategies.fetch collection.name, :default
|
101
|
-
|
148
|
+
strategy_options = {}
|
149
|
+
if strategy_name.is_a?(Hash)
|
150
|
+
strategy_options = strategy_name
|
151
|
+
strategy_name = strategy_name[:name]
|
152
|
+
end
|
153
|
+
strategy_klass = AbstractImporter::Strategies.const_get :"#{strategy_name.capitalize}Strategy"
|
154
|
+
strategy_klass.new(collection_importer, strategy_options)
|
102
155
|
end
|
103
156
|
|
104
157
|
|
@@ -122,10 +175,11 @@ module AbstractImporter
|
|
122
175
|
end
|
123
176
|
|
124
177
|
def map_foreign_key(legacy_id, plural, foreign_key, depends_on)
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
178
|
+
return nil if legacy_id.nil?
|
179
|
+
return legacy_id unless use_id_map_for?(depends_on)
|
180
|
+
id_map.apply!(depends_on, legacy_id)
|
181
|
+
rescue KeyError
|
182
|
+
raise IdNotMappedError, "#{plural}.#{foreign_key} will be nil: a #{depends_on.to_s.singularize} with the legacy id #{legacy_id} was not mapped."
|
129
183
|
end
|
130
184
|
|
131
185
|
|
@@ -134,8 +188,6 @@ module AbstractImporter
|
|
134
188
|
|
135
189
|
private
|
136
190
|
|
137
|
-
attr_reader :collections, :import_plan, :skip, :only
|
138
|
-
|
139
191
|
def verify_source!
|
140
192
|
import_plan.keys.each do |collection|
|
141
193
|
next if source.respond_to?(collection)
|
@@ -152,6 +204,13 @@ module AbstractImporter
|
|
152
204
|
raise "#{parent.class} does not have a collection named `#{collection}`; " <<
|
153
205
|
"but #{self.class} plans to import records with that name"
|
154
206
|
end
|
207
|
+
|
208
|
+
Array(self.class.dependencies).each do |collection|
|
209
|
+
next if parent.respond_to?(collection)
|
210
|
+
|
211
|
+
raise "#{parent.class} does not have a collection named `#{collection}`; " <<
|
212
|
+
"but #{self.class} declares it as a dependency"
|
213
|
+
end
|
155
214
|
end
|
156
215
|
|
157
216
|
def instantiate_collections!
|
@@ -179,17 +238,20 @@ module AbstractImporter
|
|
179
238
|
end
|
180
239
|
end
|
181
240
|
|
241
|
+
def find_collection(name)
|
242
|
+
collections.find { |collection| collection.name == name } ||
|
243
|
+
dependencies.find { |collection| collection.name == name }
|
244
|
+
end
|
245
|
+
|
182
246
|
def prepopulate_id_map!
|
183
247
|
(collections + dependencies).each do |collection|
|
184
|
-
|
185
|
-
|
248
|
+
next unless use_id_map_for?(collection)
|
249
|
+
prepopulate_id_map_for!(collection)
|
186
250
|
end
|
187
251
|
end
|
188
252
|
|
189
|
-
|
190
|
-
|
191
|
-
def record_no_id_in_map_error(legacy_id, plural, foreign_key, depends_on)
|
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."
|
253
|
+
def prepopulate_id_map_for!(collection)
|
254
|
+
id_map.init collection.table_name, collection.scope
|
193
255
|
end
|
194
256
|
|
195
257
|
|
@@ -202,14 +264,17 @@ module AbstractImporter
|
|
202
264
|
end
|
203
265
|
end
|
204
266
|
|
205
|
-
def default_reporter(io)
|
206
|
-
|
267
|
+
def default_reporter(options, io)
|
268
|
+
reporter = options.fetch(:reporter, ENV["IMPORT_REPORTER"])
|
269
|
+
return reporter if reporter.is_a?(AbstractImporter::Reporters::BaseReporter)
|
270
|
+
|
271
|
+
case reporter.to_s.downcase
|
207
272
|
when "none" then Reporters::NullReporter.new(io)
|
208
273
|
when "performance" then Reporters::PerformanceReporter.new(io)
|
209
274
|
when "debug" then Reporters::DebugReporter.new(io)
|
210
275
|
when "dot" then Reporters::DotReporter.new(io)
|
211
276
|
else
|
212
|
-
if
|
277
|
+
if ENV["RAILS_ENV"] == "production"
|
213
278
|
Reporters::DebugReporter.new(io)
|
214
279
|
else
|
215
280
|
Reporters::DotReporter.new(io)
|
@@ -2,17 +2,24 @@ module AbstractImporter
|
|
2
2
|
class Collection < Struct.new(:name, :model, :table_name, :scope, :options)
|
3
3
|
|
4
4
|
def association_attrs
|
5
|
-
return @
|
5
|
+
return @association_attrs if defined?(@association_attrs)
|
6
6
|
|
7
7
|
# Instead of calling `tenant.people.build(__)`, we'll reflect on the
|
8
8
|
# association to find its foreign key and its owner's id, so that we
|
9
9
|
# can call `Person.new(__.merge(tenant_id: id))`.
|
10
|
-
@
|
11
|
-
|
12
|
-
unless
|
13
|
-
@
|
10
|
+
@association_attrs = {}
|
11
|
+
association = scope.instance_variable_get(:@association)
|
12
|
+
unless association.is_a?(ActiveRecord::Associations::HasManyThroughAssociation)
|
13
|
+
@association_attrs.merge!(association.reflection.foreign_key.to_sym => association.owner.id)
|
14
14
|
end
|
15
|
-
|
15
|
+
if association.reflection.inverse_of && association.reflection.inverse_of.polymorphic?
|
16
|
+
@association_attrs.merge!(association.reflection.inverse_of.foreign_type.to_sym => association.owner.class.name)
|
17
|
+
end
|
18
|
+
@association_attrs.freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def has_legacy_id?
|
22
|
+
@has_legacy_id ||= model.column_names.member?("legacy_id")
|
16
23
|
end
|
17
24
|
|
18
25
|
end
|
@@ -6,7 +6,7 @@ module AbstractImporter
|
|
6
6
|
def initialize(importer, collection)
|
7
7
|
@importer = importer
|
8
8
|
@collection = collection
|
9
|
-
@strategy = importer.strategy_for(
|
9
|
+
@strategy = importer.strategy_for(self)
|
10
10
|
end
|
11
11
|
|
12
12
|
attr_reader :importer, :collection, :summary, :strategy
|
@@ -17,15 +17,18 @@ module AbstractImporter
|
|
17
17
|
:scope,
|
18
18
|
:options,
|
19
19
|
:association_attrs,
|
20
|
+
:has_legacy_id?,
|
20
21
|
:to => :collection
|
21
22
|
|
22
23
|
delegate :dry_run?,
|
23
24
|
:parent,
|
24
25
|
:source,
|
25
26
|
:reporter,
|
27
|
+
:use_id_map_for?,
|
26
28
|
:remap_foreign_key?,
|
27
29
|
:id_map,
|
28
30
|
:map_foreign_key,
|
31
|
+
:generate_id,
|
29
32
|
:to => :importer
|
30
33
|
|
31
34
|
|
@@ -1,24 +1,16 @@
|
|
1
1
|
module AbstractImporter
|
2
2
|
class IdMap
|
3
3
|
|
4
|
-
class IdNotMappedError < StandardError; end
|
5
|
-
|
6
4
|
def initialize
|
7
5
|
@id_map = Hash.new { |hash, key| hash[key] = {} }
|
8
6
|
end
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
def init(table_name, query)
|
13
|
-
table_name = table_name.to_sym
|
14
|
-
@id_map[table_name] = {}
|
15
|
-
merge! table_name, query
|
16
|
-
end
|
17
|
-
|
18
|
-
def merge!(table_name, query)
|
8
|
+
def merge!(table_name, map)
|
19
9
|
table_name = table_name.to_sym
|
20
|
-
|
10
|
+
map = Hash[map.where.not(legacy_id: nil).pluck(:legacy_id, :id)] unless map.is_a?(Hash)
|
11
|
+
@id_map[table_name].merge! map
|
21
12
|
end
|
13
|
+
alias :init :merge!
|
22
14
|
|
23
15
|
def get(table_name)
|
24
16
|
@id_map[table_name.to_sym].dup
|
@@ -46,11 +38,21 @@ module AbstractImporter
|
|
46
38
|
@id_map[table_name][legacy_id] = record_id
|
47
39
|
end
|
48
40
|
|
49
|
-
def apply!(
|
41
|
+
def apply!(depends_on, legacy_id)
|
50
42
|
return nil if legacy_id.blank?
|
51
|
-
|
52
|
-
|
53
|
-
|
43
|
+
@id_map[depends_on].fetch(legacy_id)
|
44
|
+
end
|
45
|
+
|
46
|
+
def tables
|
47
|
+
@id_map.keys
|
48
|
+
end
|
49
|
+
|
50
|
+
def dup
|
51
|
+
IdMap.new.tap do |other|
|
52
|
+
tables.each do |table|
|
53
|
+
other.init table, get(table)
|
54
|
+
end
|
55
|
+
end
|
54
56
|
end
|
55
57
|
|
56
58
|
end
|