abstract_importer 1.3.4 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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