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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +13 -10
  4. data/Rakefile +4 -5
  5. data/abstract_importer.gemspec +10 -9
  6. data/lib/abstract_importer.rb +2 -2
  7. data/lib/abstract_importer/base.rb +113 -48
  8. data/lib/abstract_importer/collection.rb +13 -6
  9. data/lib/abstract_importer/collection_importer.rb +4 -1
  10. data/lib/abstract_importer/id_map.rb +18 -16
  11. data/lib/abstract_importer/import_options.rb +0 -1
  12. data/lib/abstract_importer/reporters.rb +6 -5
  13. data/lib/abstract_importer/reporters/base_reporter.rb +7 -2
  14. data/lib/abstract_importer/reporters/debug_reporter.rb +1 -8
  15. data/lib/abstract_importer/reporters/dot_reporter.rb +5 -0
  16. data/lib/abstract_importer/reporters/null_reporter.rb +1 -1
  17. data/lib/abstract_importer/reporters/progress_reporter.rb +42 -0
  18. data/lib/abstract_importer/strategies.rb +1 -0
  19. data/lib/abstract_importer/strategies/base.rb +13 -3
  20. data/lib/abstract_importer/strategies/default_strategy.rb +1 -1
  21. data/lib/abstract_importer/strategies/insert_strategy.rb +27 -15
  22. data/lib/abstract_importer/strategies/replace_strategy.rb +1 -1
  23. data/lib/abstract_importer/strategies/upsert_strategy.rb +25 -0
  24. data/lib/abstract_importer/summary.rb +13 -2
  25. data/lib/abstract_importer/version.rb +1 -1
  26. data/test/callback_test.rb +3 -3
  27. data/test/id_map_test.rb +18 -0
  28. data/test/insert_strategy_test.rb +28 -11
  29. data/test/support/mock_data_source.rb +6 -0
  30. data/test/support/mock_objects.rb +6 -0
  31. data/test/support/schema.rb +15 -0
  32. data/test/test_helper.rb +10 -6
  33. data/test/upsert_strategy_test.rb +92 -0
  34. metadata +49 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 51e786a9666c22acd429d7f492d0cc84744cd222
4
- data.tar.gz: feea5423d323820bd47c5b701c11187b88d3ff94
3
+ metadata.gz: a64bcc8c97755427d62459b7f1089c22d3806501
4
+ data.tar.gz: d8ef7bf828aa70c7294dd77f8d50a93857324a57
5
5
  SHA512:
6
- metadata.gz: 0840d9026d26c679f66e8f038d00480f77d3d6228ead03b25dd905fd855387121ba30db9036239cd77ac8a83d8933087976afcd265d7f192404da210099d97d3
7
- data.tar.gz: 63bdb142056eefdf38eb3585d98fdce6b7c009286c10d8c81cafc1b2fccc4e185ab3fd41b462d84714626396e242293d44978c3ce634846e6621478aac6447f0
6
+ metadata.gz: 830be8e79c0c5fbc3c8cc7fa372fc808f689dc78bc8518143a0395ad13a5dc1d7cc88ad6ebdbc81c1427688e19a40f5c5902282fa6126975ced79e5a034f4f25
7
+ data.tar.gz: 596609a81ff049f029baad8492aa8b4902c20e31e6a53237b02011c0a4cb7ed3d77070f93e27849b5c70f980852eb5747fc4d09667c9d49068a102a6f1108008
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.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.2
3
+ - 2.3.1
5
4
 
6
- # https://github.com/travis-ci/travis-ci/issues/5239
7
- before_install:
8
- - gem update bundler
9
- - sudo apt-add-repository -y ppa:travis-ci/sqlite3
10
- - sudo apt-get -y update
11
- - sudo apt-get install sqlite3=3.7.15.1-1~travis1
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
- script:
14
- - bundle exec rake test
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 'rake/testtask'
2
+ require "rake/testtask"
3
3
 
4
4
  Rake::TestTask.new(:test) do |t|
5
- t.libs << 'lib'
6
- t.libs << 'test'
7
- t.pattern = 'test/**/*_test.rb'
8
- t.verbose = false
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
9
8
  end
@@ -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 'abstract_importer/version'
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", ">= 4.0"
21
- spec.add_dependency "activerecord-insert_many", ">= 0.1.1"
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", "~> 4.7"
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 "turn"
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
@@ -1,2 +1,2 @@
1
- require 'abstract_importer/base'
2
- require 'abstract_importer/version'
1
+ require "abstract_importer/base"
2
+ require "abstract_importer/version"
@@ -1,13 +1,15 @@
1
- require 'abstract_importer/import_options'
2
- require 'abstract_importer/import_plan'
3
- require 'abstract_importer/reporters'
4
- require 'abstract_importer/collection'
5
- require 'abstract_importer/collection_importer'
6
- require 'abstract_importer/id_map'
7
- require 'abstract_importer/summary'
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
- @id_map = IdMap.new
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, :parent, :reporter, :id_map, :results
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
- reporter.start_all(self)
91
+ {}.tap do |results|
92
+ reporter.start_all(self)
61
93
 
62
- ms = Benchmark.ms do
63
- setup
64
- end
65
- reporter.finish_setup(ms)
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
- ms = Benchmark.ms do
68
- with_transaction do
69
- collections.each &method(:import_collection)
107
+ ms = Benchmark.ms do
108
+ teardown
70
109
  end
71
- end
110
+ reporter.finish_teardown(self, ms)
72
111
 
73
- teardown
74
- reporter.finish_all(self, ms)
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 import_collection(collection)
86
- return if skip? collection
87
- results[collection.name] = CollectionImporter.new(self, collection).perform!
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
- return true if skip.member?(collection.name)
95
- return true if only && !only.member?(collection.name)
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(collection)
145
+ def strategy_for(collection_importer)
146
+ collection = collection_importer.collection
100
147
  strategy_name = @strategies.fetch collection.name, :default
101
- AbstractImporter::Strategies.const_get :"#{strategy_name.capitalize}Strategy"
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
- id_map.apply!(legacy_id, depends_on)
126
- rescue IdMap::IdNotMappedError
127
- record_no_id_in_map_error(legacy_id, plural, foreign_key, depends_on)
128
- nil
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
- id_map.init collection.table_name, collection.scope
185
- .where("#{collection.table_name}.legacy_id IS NOT NULL")
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
- case ENV["IMPORT_REPORTER"].to_s.downcase
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 Rails.env.production?
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 @assocation_attrs if defined?(@assocation_attrs)
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
- @assocation_attrs = {}
11
- assocation = scope.instance_variable_get(:@association)
12
- unless assocation.is_a?(ActiveRecord::Associations::HasManyThroughAssociation)
13
- @assocation_attrs.merge!(assocation.reflection.foreign_key.to_sym => assocation.owner.id)
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
- @assocation_attrs.freeze
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(collection).new(self)
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
- @id_map[table_name].merge! Hash[query.pluck(:legacy_id, :id)]
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!(legacy_id, depends_on)
41
+ def apply!(depends_on, legacy_id)
50
42
  return nil if legacy_id.blank?
51
- id_map = @id_map[depends_on]
52
- raise IdNotMappedError.new unless id_map.key?(legacy_id)
53
- id_map[legacy_id]
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