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
@@ -2,7 +2,6 @@ module AbstractImporter
2
2
  class ImportOptions
3
3
  CALLBACKS = [ :finder,
4
4
  :rescue,
5
- :rescue_batch,
6
5
  :before_build,
7
6
  :before_batch,
8
7
  :before_create,
@@ -1,5 +1,6 @@
1
- require 'abstract_importer/reporters/base_reporter'
2
- require 'abstract_importer/reporters/debug_reporter'
3
- require 'abstract_importer/reporters/dot_reporter'
4
- require 'abstract_importer/reporters/null_reporter'
5
- require 'abstract_importer/reporters/performance_reporter'
1
+ require "abstract_importer/reporters/base_reporter"
2
+ require "abstract_importer/reporters/debug_reporter"
3
+ require "abstract_importer/reporters/dot_reporter"
4
+ require "abstract_importer/reporters/null_reporter"
5
+ require "abstract_importer/reporters/performance_reporter"
6
+ require "abstract_importer/reporters/progress_reporter"
@@ -17,8 +17,10 @@ module AbstractImporter
17
17
  io.puts "\n\nFinished in #{distance_of_time(ms)}"
18
18
  end
19
19
 
20
- def finish_setup(ms)
21
- io.puts "Setup took #{distance_of_time(ms)}\n"
20
+ def finish_setup(importer, ms)
21
+ end
22
+
23
+ def finish_teardown(importer, ms)
22
24
  end
23
25
 
24
26
  def start_collection(collection)
@@ -36,6 +38,9 @@ module AbstractImporter
36
38
  def record_failed(record, hash)
37
39
  end
38
40
 
41
+ def batch_inserted(size)
42
+ end
43
+
39
44
 
40
45
 
41
46
  def count_notice(message)
@@ -12,12 +12,6 @@ module AbstractImporter
12
12
 
13
13
 
14
14
 
15
- def production?
16
- Rails.env.production?
17
- end
18
-
19
-
20
-
21
15
  def start_all(importer)
22
16
  super
23
17
  end
@@ -29,7 +23,7 @@ module AbstractImporter
29
23
 
30
24
 
31
25
 
32
- def finish_setup(ms)
26
+ def finish_setup(importer, ms)
33
27
  super
34
28
  end
35
29
 
@@ -75,7 +69,6 @@ module AbstractImporter
75
69
 
76
70
 
77
71
  def count_notice(message)
78
- return if production?
79
72
  @notices[message] = (@notices[message] || 0) + 1
80
73
  end
81
74
 
@@ -13,6 +13,11 @@ module AbstractImporter
13
13
  super
14
14
  end
15
15
 
16
+ def batch_inserted(size)
17
+ io.print "." * size
18
+ super
19
+ end
20
+
16
21
 
17
22
  end
18
23
  end
@@ -8,7 +8,7 @@ module AbstractImporter
8
8
  def finish_all(importer, ms)
9
9
  end
10
10
 
11
- def finish_setup(ms)
11
+ def finish_setup(importer, ms)
12
12
  end
13
13
 
14
14
  def start_collection(collection)
@@ -0,0 +1,42 @@
1
+ require "progressbar"
2
+
3
+ module AbstractImporter
4
+ module Reporters
5
+ class ProgressReporter < BaseReporter
6
+
7
+ def finish_setup(importer, ms)
8
+ total = importer.collections.reduce(0) do |total, collection|
9
+ total + importer.count_collection(collection)
10
+ end
11
+ @pbar = ProgressBar.new("progress", total)
12
+ end
13
+
14
+ def finish_all(importer, ms)
15
+ pbar.finish
16
+ io.puts "Finished in #{distance_of_time(ms)}"
17
+ end
18
+
19
+ def start_collection(collection)
20
+ # Say nothing
21
+ end
22
+
23
+
24
+
25
+ def record_created(record)
26
+ pbar.inc
27
+ end
28
+
29
+ def record_failed(record, hash)
30
+ pbar.inc
31
+ end
32
+
33
+ def batch_inserted(size)
34
+ pbar.inc size
35
+ end
36
+
37
+ protected
38
+ attr_reader :pbar
39
+
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,4 @@
1
1
  require "abstract_importer/strategies/default_strategy"
2
2
  require "abstract_importer/strategies/replace_strategy"
3
3
  require "abstract_importer/strategies/insert_strategy"
4
+ require "abstract_importer/strategies/upsert_strategy"
@@ -7,15 +7,22 @@ module AbstractImporter
7
7
  :remap_foreign_keys!,
8
8
  :redundant_record?,
9
9
  :invoke_callback,
10
+ :use_id_map_for?,
10
11
  :dry_run?,
11
12
  :id_map,
12
13
  :scope,
13
14
  :reporter,
14
15
  :association_attrs,
16
+ :generate_id,
15
17
  to: :collection
16
18
 
17
- def initialize(collection)
19
+ def initialize(collection, options={})
18
20
  @collection = collection
21
+ @remap_ids = options.fetch(:id_map, use_id_map_for?(collection))
22
+ end
23
+
24
+ def remap_ids?
25
+ @remap_ids
19
26
  end
20
27
 
21
28
  def process_record(hash)
@@ -32,9 +39,12 @@ module AbstractImporter
32
39
  def prepare_attributes(hash)
33
40
  hash = invoke_callback(:before_build, hash) || hash
34
41
 
35
- legacy_id = hash.delete(:id)
42
+ if remap_ids?
43
+ hash = hash.merge(legacy_id: hash.delete(:id))
44
+ hash[:id] = generate_id.call if generate_id
45
+ end
36
46
 
37
- hash.merge(legacy_id: legacy_id).merge(association_attrs)
47
+ hash.merge(association_attrs)
38
48
  end
39
49
 
40
50
  end
@@ -44,7 +44,7 @@ module AbstractImporter
44
44
  if record.valid? && record.save
45
45
  invoke_callback(:after_create, hash, record)
46
46
  invoke_callback(:after_save, hash, record)
47
- id_map << record
47
+ id_map << record if remap_ids?
48
48
 
49
49
  reporter.record_created(record)
50
50
  clean_record(record)
@@ -5,10 +5,10 @@ module AbstractImporter
5
5
  module Strategies
6
6
  class InsertStrategy < Base
7
7
 
8
- def initialize(collection)
8
+ def initialize(collection, options={})
9
9
  super
10
10
  @batch = []
11
- @batch_size = 250
11
+ @batch_size = options.fetch(:batch_size, 250)
12
12
  end
13
13
 
14
14
 
@@ -27,8 +27,7 @@ module AbstractImporter
27
27
  return
28
28
  end
29
29
 
30
- @batch << prepare_attributes(hash)
31
- flush if @batch.length >= @batch_size
30
+ add_to_batch prepare_attributes(hash)
32
31
 
33
32
  rescue ::AbstractImporter::Skip
34
33
  summary.skipped += 1
@@ -38,24 +37,37 @@ module AbstractImporter
38
37
  def flush
39
38
  invoke_callback(:before_batch, @batch)
40
39
 
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
40
+ insert_batch(@batch)
49
41
 
50
- ids = collection.scope.where(legacy_id: @batch.map { |hash| hash[:legacy_id] })
51
- id_map.merge! collection.table_name, ids
42
+ id_map_record_batch(@batch) if remap_ids?
52
43
 
53
- summary.created += ids.length
44
+ summary.created += @batch.length
45
+ reporter.batch_inserted(@batch.length)
54
46
 
55
47
  @batch = []
56
48
  end
57
49
 
58
50
 
51
+ def insert_batch(batch)
52
+ collection.scope.insert_many(batch)
53
+ end
54
+
55
+
56
+ def add_to_batch(attributes)
57
+ @batch << attributes
58
+ legacy_id, id = attributes.values_at(:legacy_id, :id)
59
+ id_map.merge! collection.table_name, legacy_id => id if id && legacy_id
60
+ flush if @batch.length >= @batch_size
61
+ end
62
+
63
+
64
+ def id_map_record_batch(batch)
65
+ return if generate_id
66
+ id_map.merge! collection.table_name,
67
+ collection.scope.where(legacy_id: @batch.map { |hash| hash[:legacy_id] })
68
+ end
69
+
70
+
59
71
  end
60
72
  end
61
73
  end
@@ -37,7 +37,7 @@ module AbstractImporter
37
37
  def update_record(hash)
38
38
  hash = invoke_callback(:before_build, hash) || hash
39
39
 
40
- record = scope.find_by(legacy_id: hash.delete(:id))
40
+ record = remap_ids? ? scope.find_by(legacy_id: hash.delete(:id)) : scope.find_by(id: hash[:id])
41
41
  record.attributes = hash
42
42
 
43
43
  return true if dry_run?
@@ -0,0 +1,25 @@
1
+ require "abstract_importer/strategies/insert_strategy"
2
+ require "activerecord/insert_many"
3
+
4
+ module AbstractImporter
5
+ module Strategies
6
+ class UpsertStrategy < InsertStrategy
7
+
8
+
9
+ # We won't skip any records for already being imported
10
+ def already_imported?(hash)
11
+ false
12
+ end
13
+
14
+
15
+ def insert_batch(batch)
16
+ collection.scope.insert_many(batch, on_conflict: {
17
+ column: remap_ids? ? :legacy_id : :id,
18
+ do: :update
19
+ })
20
+ end
21
+
22
+
23
+ end
24
+ end
25
+ end
@@ -1,8 +1,8 @@
1
1
  module AbstractImporter
2
2
  class Summary < Struct.new(:total, :redundant, :created, :already_imported, :invalid, :ms, :skipped)
3
3
 
4
- def initialize
5
- super(0,0,0,0,0,0,0)
4
+ def initialize(a=0, b=0, c=0, d=0, e=0, f=0, g=0)
5
+ super(a,b,c,d,e,f,g)
6
6
  end
7
7
 
8
8
  def average_ms
@@ -10,5 +10,16 @@ module AbstractImporter
10
10
  ms / total
11
11
  end
12
12
 
13
+ def +(other)
14
+ Summary.new(
15
+ total + other.total,
16
+ redundant + other.redundant,
17
+ created + other.created,
18
+ already_imported + other.already_imported,
19
+ invalid + other.invalid,
20
+ ms + other.ms,
21
+ skipped + other.skipped)
22
+ end
23
+
13
24
  end
14
25
  end
@@ -1,3 +1,3 @@
1
1
  module AbstractImporter
2
- VERSION = "1.3.4"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -63,9 +63,9 @@ class CallbackTest < ActiveSupport::TestCase
63
63
  end
64
64
 
65
65
  should "should be invoked after the record is created" do
66
- mock(importer).callback({name: "Harry Potter"}, satisfy(&:persisted?)).once
67
- mock(importer).callback({name: "Ron Weasley"}, satisfy(&:persisted?)).once
68
- mock(importer).callback({name: "Hermione Granger"}, satisfy(&:persisted?)).once
66
+ mock(importer).callback(hash_including(name: "Harry Potter"), satisfy(&:persisted?)).once
67
+ mock(importer).callback(hash_including(name: "Ron Weasley"), satisfy(&:persisted?)).once
68
+ mock(importer).callback(hash_including(name: "Hermione Granger"), satisfy(&:persisted?)).once
69
69
  import!
70
70
  end
71
71
  end
@@ -0,0 +1,18 @@
1
+ require "test_helper"
2
+
3
+ class IdMapTest < ActiveSupport::TestCase
4
+
5
+ context ".dup" do
6
+ should "create an independent copy of an IdMap" do
7
+ map1 = AbstractImporter::IdMap.new
8
+ map1.merge! :examples, { "a" => 1 }
9
+
10
+ map2 = map1.dup
11
+ assert_equal({ "a" => 1 }, map2.get(:examples))
12
+
13
+ map1.merge! :examples, { "b" => 2 }
14
+ assert_equal({ "a" => 1 }, map2.get(:examples))
15
+ end
16
+ end
17
+
18
+ end
@@ -1,7 +1,7 @@
1
1
  require "test_helper"
2
2
 
3
3
 
4
- class ImporterTest < ActiveSupport::TestCase
4
+ class InsertStrategyTest < ActiveSupport::TestCase
5
5
 
6
6
  setup do
7
7
  options.merge!(strategy: {students: :insert})
@@ -68,25 +68,42 @@ class ImporterTest < ActiveSupport::TestCase
68
68
  end
69
69
  end
70
70
 
71
- context "When the import would create a duplicate record" do
71
+ context "When the imported records belong to a parent polymorphically" do
72
72
  setup do
73
+ @account = Owl.create!(name: "Pigwidgeon")
73
74
  plan do |import|
74
- import.students do |options|
75
- options.rescue_batch do |batch|
76
- names = parent.students.pluck :name
77
- batch.reject! { |student| names.member? student[:name] }
78
- end
79
- end
75
+ import.abilities
80
76
  end
81
- account.students.create!(name: "Ron Weasley")
82
77
  end
83
78
 
84
- should "not import existing records twice" do
79
+ should "import records just fine" do
80
+ pet = @account
85
81
  import!
86
- assert_equal 3, account.students.count
82
+ assert_equal [["Owl", pet.id]], Ability.pluck(:pet_type, :pet_id)
87
83
  end
88
84
  end
89
85
 
90
86
 
91
87
 
88
+ context "Given an ID generator" do
89
+ setup do
90
+ plan do |import|
91
+ import.students
92
+ end
93
+
94
+ id = 0
95
+ options.merge!(generate_id: -> { id += 1 })
96
+ end
97
+
98
+ should "insert the records with the specified IDs" do
99
+ import!
100
+ assert_equal [1, 2, 3], account.students.pluck(:id)
101
+ end
102
+
103
+ should "map the specified IDs to the legacy_ids" do
104
+ import!
105
+ assert_equal ({ 456 => 1, 457 => 2, 458 => 3 }), importer.id_map.get(:students)
106
+ end
107
+ end
108
+
92
109
  end
@@ -52,5 +52,11 @@ class MockDataSource
52
52
  end
53
53
  end
54
54
 
55
+ def abilities
56
+ Enumerator.new do |e|
57
+ e.yield id: 701, name: "Hyperactivity"
58
+ end
59
+ end
60
+
55
61
 
56
62
  end