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
@@ -1,5 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
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
|
-
|
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
|
|
@@ -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
|
@@ -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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
51
|
-
id_map.merge! collection.table_name, ids
|
42
|
+
id_map_record_batch(@batch) if remap_ids?
|
52
43
|
|
53
|
-
summary.created +=
|
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(
|
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
|
data/test/callback_test.rb
CHANGED
@@ -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(
|
67
|
-
mock(importer).callback(
|
68
|
-
mock(importer).callback(
|
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
|
data/test/id_map_test.rb
ADDED
@@ -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
|
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
|
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.
|
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 "
|
79
|
+
should "import records just fine" do
|
80
|
+
pet = @account
|
85
81
|
import!
|
86
|
-
assert_equal
|
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
|