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
@@ -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