activerecord-import 0.14.1 → 1.4.1
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 +5 -5
- data/.github/workflows/test.yaml +107 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +74 -8
- data/Brewfile +3 -1
- data/CHANGELOG.md +448 -2
- data/Gemfile +26 -19
- data/LICENSE +21 -56
- data/README.markdown +568 -32
- data/Rakefile +5 -1
- data/activerecord-import.gemspec +8 -7
- data/benchmarks/README +2 -2
- data/benchmarks/benchmark.rb +8 -1
- data/benchmarks/lib/base.rb +2 -0
- data/benchmarks/lib/cli_parser.rb +5 -2
- data/benchmarks/lib/float.rb +2 -0
- data/benchmarks/lib/mysql2_benchmark.rb +2 -0
- data/benchmarks/lib/output_to_csv.rb +2 -0
- data/benchmarks/lib/output_to_html.rb +4 -2
- data/benchmarks/models/test_innodb.rb +2 -0
- data/benchmarks/models/test_memory.rb +2 -0
- data/benchmarks/models/test_myisam.rb +2 -0
- data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
- data/gemfiles/4.2.gemfile +4 -3
- data/gemfiles/5.0.gemfile +4 -3
- data/gemfiles/5.1.gemfile +4 -0
- data/gemfiles/5.2.gemfile +4 -0
- data/gemfiles/6.0.gemfile +4 -0
- data/gemfiles/6.1.gemfile +4 -0
- data/gemfiles/7.0.gemfile +4 -0
- data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
- data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +8 -0
- data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +12 -16
- data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/mysql_adapter.rb +35 -18
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +107 -28
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +141 -18
- data/lib/activerecord-import/base.rb +15 -8
- data/lib/activerecord-import/import.rb +642 -178
- data/lib/activerecord-import/mysql2.rb +2 -0
- data/lib/activerecord-import/postgresql.rb +2 -0
- data/lib/activerecord-import/sqlite3.rb +2 -0
- data/lib/activerecord-import/synchronize.rb +5 -3
- data/lib/activerecord-import/value_sets_parser.rb +29 -3
- data/lib/activerecord-import/version.rb +3 -1
- data/lib/activerecord-import.rb +5 -16
- data/test/adapters/jdbcmysql.rb +2 -0
- data/test/adapters/jdbcpostgresql.rb +2 -0
- data/test/adapters/jdbcsqlite3.rb +3 -0
- data/test/adapters/makara_postgis.rb +3 -0
- data/test/adapters/mysql2.rb +2 -0
- data/test/adapters/mysql2_makara.rb +2 -0
- data/test/adapters/mysql2spatial.rb +2 -0
- data/test/adapters/postgis.rb +2 -0
- data/test/adapters/postgresql.rb +2 -0
- data/test/adapters/postgresql_makara.rb +2 -0
- data/test/adapters/seamless_database_pool.rb +2 -0
- data/test/adapters/spatialite.rb +2 -0
- data/test/adapters/sqlite3.rb +2 -0
- data/test/{travis → github}/database.yml +7 -1
- data/test/import_test.rb +498 -32
- data/test/jdbcmysql/import_test.rb +2 -1
- data/test/jdbcpostgresql/import_test.rb +2 -1
- data/test/jdbcsqlite3/import_test.rb +6 -0
- data/test/makara_postgis/import_test.rb +10 -0
- data/test/models/account.rb +5 -0
- data/test/models/alarm.rb +4 -0
- data/test/models/animal.rb +8 -0
- data/test/models/bike_maker.rb +9 -0
- data/test/models/book.rb +4 -0
- data/test/models/car.rb +5 -0
- data/test/models/card.rb +5 -0
- data/test/models/chapter.rb +2 -0
- data/test/models/customer.rb +8 -0
- data/test/models/deck.rb +8 -0
- data/test/models/dictionary.rb +6 -0
- data/test/models/discount.rb +2 -0
- data/test/models/end_note.rb +2 -0
- data/test/models/group.rb +2 -0
- data/test/models/order.rb +8 -0
- data/test/models/playing_card.rb +4 -0
- data/test/models/promotion.rb +2 -0
- data/test/models/question.rb +2 -0
- data/test/models/rule.rb +2 -0
- data/test/models/tag.rb +7 -0
- data/test/models/tag_alias.rb +5 -0
- data/test/models/topic.rb +16 -0
- data/test/models/user.rb +5 -0
- data/test/models/user_token.rb +6 -0
- data/test/models/vendor.rb +9 -0
- data/test/models/widget.rb +18 -0
- data/test/mysql2/import_test.rb +2 -0
- data/test/mysql2_makara/import_test.rb +2 -0
- data/test/mysqlspatial2/import_test.rb +2 -0
- data/test/postgis/import_test.rb +6 -0
- data/test/postgresql/import_test.rb +2 -4
- data/test/schema/generic_schema.rb +88 -3
- data/test/schema/jdbcpostgresql_schema.rb +3 -0
- data/test/schema/mysql2_schema.rb +21 -0
- data/test/schema/postgis_schema.rb +3 -0
- data/test/schema/postgresql_schema.rb +63 -0
- data/test/schema/sqlite3_schema.rb +15 -0
- data/test/schema/version.rb +2 -0
- data/test/sqlite3/import_test.rb +4 -50
- data/test/support/active_support/test_case_extensions.rb +8 -1
- data/test/support/assertions.rb +2 -0
- data/test/support/factories.rb +17 -8
- data/test/support/generate.rb +10 -8
- data/test/support/mysql/import_examples.rb +17 -3
- data/test/support/postgresql/import_examples.rb +442 -9
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +45 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +278 -1
- data/test/support/shared_examples/recursive_import.rb +137 -12
- data/test/support/sqlite3/import_examples.rb +232 -0
- data/test/synchronize_test.rb +10 -0
- data/test/test_helper.rb +44 -3
- data/test/value_sets_bytes_parser_test.rb +15 -2
- data/test/value_sets_records_parser_test.rb +2 -0
- metadata +74 -22
- data/.travis.yml +0 -52
- data/gemfiles/3.2.gemfile +0 -3
- data/gemfiles/4.0.gemfile +0 -3
- data/gemfiles/4.1.gemfile +0 -3
- data/test/schema/mysql_schema.rb +0 -16
data/test/support/factories.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FactoryBot.define do
|
|
2
4
|
sequence(:book_title) { |n| "Book #{n}" }
|
|
3
5
|
sequence(:chapter_title) { |n| "Chapter #{n}" }
|
|
4
6
|
sequence(:end_note) { |n| "Endnote #{n}" }
|
|
@@ -9,12 +11,13 @@ FactoryGirl.define do
|
|
|
9
11
|
|
|
10
12
|
factory :invalid_topic, class: "Topic" do
|
|
11
13
|
sequence(:title) { |n| "Title #{n}" }
|
|
12
|
-
author_name nil
|
|
14
|
+
author_name { nil }
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
factory :topic do
|
|
16
18
|
sequence(:title) { |n| "Title #{n}" }
|
|
17
19
|
sequence(:author_name) { |n| "Author #{n}" }
|
|
20
|
+
sequence(:content) { |n| "Content #{n}" }
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
factory :widget do
|
|
@@ -26,32 +29,38 @@ FactoryGirl.define do
|
|
|
26
29
|
|
|
27
30
|
trait :with_rule do
|
|
28
31
|
after(:build) do |question|
|
|
29
|
-
question.build_rule(
|
|
32
|
+
question.build_rule(FactoryBot.attributes_for(:rule))
|
|
30
33
|
end
|
|
31
34
|
end
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
factory :rule do
|
|
38
|
+
sequence(:id) { |n| n }
|
|
35
39
|
sequence(:condition_text) { |n| "q_#{n}_#{n}" }
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
factory :topic_with_book, parent: :topic do
|
|
39
43
|
after(:build) do |topic|
|
|
40
44
|
2.times do
|
|
41
|
-
book = topic.books.build(title:
|
|
45
|
+
book = topic.books.build(title: FactoryBot.generate(:book_title), author_name: 'Stephen King')
|
|
42
46
|
3.times do
|
|
43
|
-
book.chapters.build(title:
|
|
47
|
+
book.chapters.build(title: FactoryBot.generate(:chapter_title))
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
4.times do
|
|
47
|
-
book.end_notes.build(note:
|
|
51
|
+
book.end_notes.build(note: FactoryBot.generate(:end_note))
|
|
48
52
|
end
|
|
49
53
|
end
|
|
50
54
|
end
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
factory :book do
|
|
54
|
-
title 'Tortilla Flat'
|
|
55
|
-
author_name 'John Steinbeck'
|
|
58
|
+
title { 'Tortilla Flat' }
|
|
59
|
+
author_name { 'John Steinbeck' }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
factory :car do
|
|
63
|
+
sequence(:Name) { |n| n }
|
|
64
|
+
sequence(:Features) { |n| "Feature #{n}" }
|
|
56
65
|
end
|
|
57
66
|
end
|
data/test/support/generate.rb
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
class ActiveSupport::TestCase
|
|
2
|
-
def Build(*args) # rubocop:disable
|
|
4
|
+
def Build(*args) # rubocop:disable Naming/MethodName
|
|
3
5
|
n = args.shift if args.first.is_a?(Numeric)
|
|
4
6
|
factory = args.shift
|
|
5
|
-
|
|
7
|
+
factory_bot_args = args.shift || {}
|
|
6
8
|
|
|
7
9
|
if n
|
|
8
10
|
[].tap do |collection|
|
|
9
|
-
n.times.each { collection <<
|
|
11
|
+
n.times.each { collection << FactoryBot.build(factory.to_s.singularize.to_sym, factory_bot_args) }
|
|
10
12
|
end
|
|
11
13
|
else
|
|
12
|
-
|
|
14
|
+
FactoryBot.build(factory.to_s.singularize.to_sym, factory_bot_args)
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
def Generate(*args) # rubocop:disable
|
|
18
|
+
def Generate(*args) # rubocop:disable Naming/MethodName
|
|
17
19
|
n = args.shift if args.first.is_a?(Numeric)
|
|
18
20
|
factory = args.shift
|
|
19
|
-
|
|
21
|
+
factory_bot_args = args.shift || {}
|
|
20
22
|
|
|
21
23
|
if n
|
|
22
24
|
[].tap do |collection|
|
|
23
|
-
n.times.each { collection <<
|
|
25
|
+
n.times.each { collection << FactoryBot.create(factory.to_s.singularize.to_sym, factory_bot_args) }
|
|
24
26
|
end
|
|
25
27
|
else
|
|
26
|
-
|
|
28
|
+
FactoryBot.create(factory.to_s.singularize.to_sym, factory_bot_args)
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
def should_support_mysql_import_functionality
|
|
3
4
|
# Forcefully disable strict mode for this session.
|
|
4
5
|
ActiveRecord::Base.connection.execute "set sql_mode='STRICT_ALL_TABLES'"
|
|
5
6
|
|
|
6
7
|
should_support_basic_on_duplicate_key_update
|
|
8
|
+
should_support_on_duplicate_key_ignore
|
|
7
9
|
|
|
8
10
|
describe "#import" do
|
|
9
11
|
context "with :on_duplicate_key_update and validation checks turned off" do
|
|
@@ -63,8 +65,8 @@ def should_support_mysql_import_functionality
|
|
|
63
65
|
let(:columns) { %w(id author_name title) }
|
|
64
66
|
|
|
65
67
|
setup do
|
|
66
|
-
topics << Topic.create!(title: "LDAP", author_name: "Big Bird")
|
|
67
|
-
topics << Topic.create!(title: "Rails Recipes", author_name: "Elmo")
|
|
68
|
+
topics << Topic.create!(title: "LDAP", author_name: "Big Bird", content: "Putting Directories to Work.")
|
|
69
|
+
topics << Topic.create!(title: "Rails Recipes", author_name: "Elmo", content: "A trusted collection of solutions.")
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
it "synchronizes passed in ActiveRecord model instances with the data just imported" do
|
|
@@ -81,5 +83,17 @@ def should_support_mysql_import_functionality
|
|
|
81
83
|
assert_equal "Chad Fowler", topics.last.author_name, "wrong author!"
|
|
82
84
|
end
|
|
83
85
|
end
|
|
86
|
+
|
|
87
|
+
if ENV['AR_VERSION'].to_f >= 5.1
|
|
88
|
+
context "with virtual columns" do
|
|
89
|
+
let(:books) { [Book.new(author_name: "foo", title: "bar")] }
|
|
90
|
+
|
|
91
|
+
it "ignores virtual columns and creates record" do
|
|
92
|
+
assert_difference "Book.count", +1 do
|
|
93
|
+
Book.import books
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
84
98
|
end
|
|
85
99
|
end
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
def should_support_postgresql_import_functionality
|
|
3
4
|
should_support_recursive_import
|
|
4
5
|
|
|
6
|
+
if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
|
|
7
|
+
should_support_postgresql_upsert_functionality
|
|
8
|
+
end
|
|
9
|
+
|
|
5
10
|
describe "#supports_imports?" do
|
|
6
11
|
it "should support import" do
|
|
7
12
|
assert ActiveRecord::Base.supports_import?
|
|
@@ -20,6 +25,36 @@ def should_support_postgresql_import_functionality
|
|
|
20
25
|
end
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
context "setting attributes and marking clean" do
|
|
29
|
+
let(:topic) { Build(:topics) }
|
|
30
|
+
|
|
31
|
+
setup { Topic.import([topic]) }
|
|
32
|
+
|
|
33
|
+
it "assigns ids" do
|
|
34
|
+
assert topic.id.present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "marks models as clean" do
|
|
38
|
+
assert !topic.changed?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if ENV['AR_VERSION'].to_f > 4.1
|
|
42
|
+
it "moves the dirty changes to previous_changes" do
|
|
43
|
+
assert topic.previous_changes.present?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "marks models as persisted" do
|
|
48
|
+
assert !topic.new_record?
|
|
49
|
+
assert topic.persisted?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "assigns timestamps" do
|
|
53
|
+
assert topic.created_at.present?
|
|
54
|
+
assert topic.updated_at.present?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
23
58
|
describe "with query cache enabled" do
|
|
24
59
|
setup do
|
|
25
60
|
unless ActiveRecord::Base.connection.query_cache_enabled
|
|
@@ -57,11 +92,272 @@ def should_support_postgresql_import_functionality
|
|
|
57
92
|
assert_equal [], Book.import(books, no_returning: true).ids
|
|
58
93
|
end
|
|
59
94
|
end
|
|
95
|
+
|
|
96
|
+
describe "returning" do
|
|
97
|
+
let(:books) { [Book.new(author_name: "King", title: "It")] }
|
|
98
|
+
let(:result) { Book.import(books, returning: %w(author_name title)) }
|
|
99
|
+
let(:book_id) do
|
|
100
|
+
if RUBY_PLATFORM == 'java' || ENV['AR_VERSION'].to_i >= 5.0
|
|
101
|
+
books.first.id
|
|
102
|
+
else
|
|
103
|
+
books.first.id.to_s
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
let(:true_returning_value) { ENV['AR_VERSION'].to_f >= 5.0 ? true : 't' }
|
|
107
|
+
let(:false_returning_value) { ENV['AR_VERSION'].to_f >= 5.0 ? false : 'f' }
|
|
108
|
+
|
|
109
|
+
it "creates records" do
|
|
110
|
+
assert_difference("Book.count", +1) { result }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "returns ids" do
|
|
114
|
+
result
|
|
115
|
+
assert_equal [book_id], result.ids
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "returns specified columns" do
|
|
119
|
+
assert_equal [%w(King It)], result.results
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
context "when given an empty array" do
|
|
123
|
+
let(:result) { Book.import([], returning: %w(title)) }
|
|
124
|
+
|
|
125
|
+
setup { result }
|
|
126
|
+
|
|
127
|
+
it "returns empty arrays for ids and results" do
|
|
128
|
+
assert_equal [], result.ids
|
|
129
|
+
assert_equal [], result.results
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
context "when a returning column is a serialized attribute" do
|
|
134
|
+
let(:vendor) { Vendor.new(hours: { monday: '8-5' }) }
|
|
135
|
+
let(:result) { Vendor.import([vendor], returning: %w(hours)) }
|
|
136
|
+
|
|
137
|
+
it "creates records" do
|
|
138
|
+
assert_difference("Vendor.count", +1) { result }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
context "when primary key and returning overlap" do
|
|
143
|
+
let(:result) { Book.import(books, returning: %w(id title)) }
|
|
144
|
+
|
|
145
|
+
setup { result }
|
|
146
|
+
|
|
147
|
+
it "returns ids" do
|
|
148
|
+
assert_equal [book_id], result.ids
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "returns specified columns" do
|
|
152
|
+
assert_equal [[book_id, 'It']], result.results
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
context "when returning is raw sql" do
|
|
157
|
+
let(:result) { Book.import(books, returning: "title, (xmax = '0') AS inserted") }
|
|
158
|
+
|
|
159
|
+
setup { result }
|
|
160
|
+
|
|
161
|
+
it "returns ids" do
|
|
162
|
+
assert_equal [book_id], result.ids
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "returns specified columns" do
|
|
166
|
+
assert_equal [['It', true_returning_value]], result.results
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
context "when returning contains raw sql" do
|
|
171
|
+
let(:result) { Book.import(books, returning: [:title, "id, (xmax = '0') AS inserted"]) }
|
|
172
|
+
|
|
173
|
+
setup { result }
|
|
174
|
+
|
|
175
|
+
it "returns ids" do
|
|
176
|
+
assert_equal [book_id], result.ids
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "returns specified columns" do
|
|
180
|
+
assert_equal [['It', book_id, true_returning_value]], result.results
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
context "setting model attributes" do
|
|
185
|
+
let(:code) { 'abc' }
|
|
186
|
+
let(:discount) { 0.10 }
|
|
187
|
+
let(:original_promotion) do
|
|
188
|
+
Promotion.new(code: code, discount: discount)
|
|
189
|
+
end
|
|
190
|
+
let(:updated_promotion) do
|
|
191
|
+
Promotion.new(code: code, description: 'ABC discount')
|
|
192
|
+
end
|
|
193
|
+
let(:returning_columns) { %w(discount) }
|
|
194
|
+
|
|
195
|
+
setup do
|
|
196
|
+
Promotion.import([original_promotion])
|
|
197
|
+
Promotion.import([updated_promotion],
|
|
198
|
+
on_duplicate_key_update: { conflict_target: %i(code), columns: %i(description) },
|
|
199
|
+
returning: returning_columns)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it "sets model attributes" do
|
|
203
|
+
assert_equal updated_promotion.discount, discount
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
context "returning multiple columns" do
|
|
207
|
+
let(:returning_columns) { %w(discount description) }
|
|
208
|
+
|
|
209
|
+
it "sets model attributes" do
|
|
210
|
+
assert_equal updated_promotion.discount, discount
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
context 'returning raw sql' do
|
|
215
|
+
let(:returning_columns) { [:discount, "(xmax = '0') AS inserted"] }
|
|
216
|
+
|
|
217
|
+
it "sets custom model attributes" do
|
|
218
|
+
assert_equal updated_promotion.inserted, false_returning_value
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if ENV['AR_VERSION'].to_f >= 4.0
|
|
226
|
+
describe "with a uuid primary key" do
|
|
227
|
+
let(:vendor) { Vendor.new(name: "foo") }
|
|
228
|
+
let(:vendors) { [vendor] }
|
|
229
|
+
|
|
230
|
+
it "creates records" do
|
|
231
|
+
assert_difference "Vendor.count", +1 do
|
|
232
|
+
Vendor.import vendors
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it "assigns an id to the model objects" do
|
|
237
|
+
Vendor.import vendors
|
|
238
|
+
assert_not_nil vendor.id
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
describe "with an assigned uuid primary key" do
|
|
243
|
+
let(:id) { SecureRandom.uuid }
|
|
244
|
+
let(:vendor) { Vendor.new(id: id, name: "foo") }
|
|
245
|
+
let(:vendors) { [vendor] }
|
|
246
|
+
|
|
247
|
+
it "creates records with correct id" do
|
|
248
|
+
assert_difference "Vendor.count", +1 do
|
|
249
|
+
Vendor.import vendors
|
|
250
|
+
end
|
|
251
|
+
assert_equal id, vendor.id
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
describe "with store accessor fields" do
|
|
257
|
+
if ENV['AR_VERSION'].to_f >= 4.0
|
|
258
|
+
it "imports values for json fields" do
|
|
259
|
+
vendors = [Vendor.new(name: 'Vendor 1', size: 100)]
|
|
260
|
+
assert_difference "Vendor.count", +1 do
|
|
261
|
+
Vendor.import vendors
|
|
262
|
+
end
|
|
263
|
+
assert_equal(100, Vendor.first.size)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "imports values for hstore fields" do
|
|
267
|
+
vendors = [Vendor.new(name: 'Vendor 1', contact: 'John Smith')]
|
|
268
|
+
assert_difference "Vendor.count", +1 do
|
|
269
|
+
Vendor.import vendors
|
|
270
|
+
end
|
|
271
|
+
assert_equal('John Smith', Vendor.first.contact)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if ENV['AR_VERSION'].to_f >= 4.2
|
|
276
|
+
it "imports values for jsonb fields" do
|
|
277
|
+
vendors = [Vendor.new(name: 'Vendor 1', charge_code: '12345')]
|
|
278
|
+
assert_difference "Vendor.count", +1 do
|
|
279
|
+
Vendor.import vendors
|
|
280
|
+
end
|
|
281
|
+
assert_equal('12345', Vendor.first.charge_code)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
if ENV['AR_VERSION'].to_f >= 4.2
|
|
287
|
+
describe "with serializable fields" do
|
|
288
|
+
it "imports default values as correct data type" do
|
|
289
|
+
vendors = [Vendor.new(name: 'Vendor 1')]
|
|
290
|
+
assert_difference "Vendor.count", +1 do
|
|
291
|
+
Vendor.import vendors
|
|
292
|
+
end
|
|
293
|
+
assert_equal({}, Vendor.first.json_data)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
%w(json jsonb).each do |json_type|
|
|
298
|
+
describe "with pure #{json_type} fields" do
|
|
299
|
+
let(:data) { { a: :b } }
|
|
300
|
+
let(:json_field_name) { "pure_#{json_type}_data" }
|
|
301
|
+
it "imports the values from saved records" do
|
|
302
|
+
vendor = Vendor.create!(name: 'Vendor 1', json_field_name => data)
|
|
303
|
+
|
|
304
|
+
Vendor.import [vendor], on_duplicate_key_update: [json_field_name]
|
|
305
|
+
assert_equal(data.as_json, vendor.reload[json_field_name])
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
describe "with enum field" do
|
|
312
|
+
let(:vendor_type) { "retailer" }
|
|
313
|
+
it "imports the correct values for enum fields" do
|
|
314
|
+
vendor = Vendor.new(name: 'Vendor 1', vendor_type: vendor_type)
|
|
315
|
+
assert_difference "Vendor.count", +1 do
|
|
316
|
+
Vendor.import [vendor]
|
|
317
|
+
end
|
|
318
|
+
assert_equal(vendor_type, Vendor.first.vendor_type)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
describe "with binary field" do
|
|
323
|
+
let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".dup.force_encoding('ASCII-8BIT') }
|
|
324
|
+
it "imports the correct values for binary fields" do
|
|
325
|
+
alarms = [Alarm.new(device_id: 1, alarm_type: 1, status: 1, secret_key: binary_value)]
|
|
326
|
+
assert_difference "Alarm.count", +1 do
|
|
327
|
+
Alarm.import alarms
|
|
328
|
+
end
|
|
329
|
+
assert_equal(binary_value, Alarm.first.secret_key)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
unless ENV["SKIP_COMPOSITE_PK"]
|
|
334
|
+
describe "with composite foreign keys" do
|
|
335
|
+
let(:account_id) { 555 }
|
|
336
|
+
let(:customer) { Customer.new(account_id: account_id, name: "foo") }
|
|
337
|
+
let(:order) { Order.new(account_id: account_id, amount: 100, customer: customer) }
|
|
338
|
+
|
|
339
|
+
it "imports and correctly maps foreign keys" do
|
|
340
|
+
assert_difference "Customer.count", +1 do
|
|
341
|
+
Customer.import [customer]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
assert_difference "Order.count", +1 do
|
|
345
|
+
Order.import [order]
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
db_customer = Customer.last
|
|
349
|
+
db_order = Order.last
|
|
350
|
+
|
|
351
|
+
assert_equal db_customer.orders.last, db_order
|
|
352
|
+
assert_not_equal db_order.customer_id, nil
|
|
353
|
+
end
|
|
354
|
+
end
|
|
60
355
|
end
|
|
61
356
|
end
|
|
62
357
|
|
|
63
358
|
def should_support_postgresql_upsert_functionality
|
|
64
359
|
should_support_basic_on_duplicate_key_update
|
|
360
|
+
should_support_on_duplicate_key_ignore
|
|
65
361
|
|
|
66
362
|
describe "#import" do
|
|
67
363
|
extend ActiveSupport::TestCase::ImportAssertions
|
|
@@ -98,7 +394,7 @@ def should_support_postgresql_upsert_functionality
|
|
|
98
394
|
# would be associated with the wrong parent.
|
|
99
395
|
it ":on_duplicate_key_ignore is ignored" do
|
|
100
396
|
assert_raise ActiveRecord::RecordNotUnique do
|
|
101
|
-
Topic.import mixed_topics, recursive: true, on_duplicate_key_ignore: true
|
|
397
|
+
Topic.import mixed_topics, recursive: true, on_duplicate_key_ignore: true, validate: false
|
|
102
398
|
end
|
|
103
399
|
end
|
|
104
400
|
end
|
|
@@ -112,6 +408,30 @@ def should_support_postgresql_upsert_functionality
|
|
|
112
408
|
end
|
|
113
409
|
|
|
114
410
|
context "using a hash" do
|
|
411
|
+
context "with :columns :all" do
|
|
412
|
+
let(:columns) { %w( id title author_name author_email_address parent_id ) }
|
|
413
|
+
let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
|
|
414
|
+
|
|
415
|
+
macro(:perform_import) do |*opts|
|
|
416
|
+
Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: :all }, validate: false)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
setup do
|
|
420
|
+
values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
|
|
421
|
+
Topic.import columns + ['replies_count'], values, validate: false
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
it "should update all specified columns" do
|
|
425
|
+
perform_import
|
|
426
|
+
updated_topic = Topic.find(99)
|
|
427
|
+
assert_equal 'Book - 2nd Edition', updated_topic.title
|
|
428
|
+
assert_equal 'Jane Doe', updated_topic.author_name
|
|
429
|
+
assert_equal 'janedoe@example.com', updated_topic.author_email_address
|
|
430
|
+
assert_equal 57, updated_topic.parent_id
|
|
431
|
+
assert_equal 3, updated_topic.replies_count
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
115
435
|
context "with :columns a hash" do
|
|
116
436
|
let(:columns) { %w( id title author_name author_email_address parent_id ) }
|
|
117
437
|
let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
|
|
@@ -126,6 +446,13 @@ def should_support_postgresql_upsert_functionality
|
|
|
126
446
|
@topic = Topic.find 99
|
|
127
447
|
end
|
|
128
448
|
|
|
449
|
+
it "should not modify the passed in :on_duplicate_key_update columns array" do
|
|
450
|
+
assert_nothing_raised do
|
|
451
|
+
columns = %w(title author_name).freeze
|
|
452
|
+
Topic.import columns, [%w(foo, bar)], { on_duplicate_key_update: { columns: columns }.freeze }.freeze
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
129
456
|
context "using string hash map" do
|
|
130
457
|
let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
|
|
131
458
|
should_support_on_duplicate_key_update
|
|
@@ -151,6 +478,83 @@ def should_support_postgresql_upsert_functionality
|
|
|
151
478
|
end
|
|
152
479
|
end
|
|
153
480
|
|
|
481
|
+
context 'with :index_predicate' do
|
|
482
|
+
let(:columns) { %w( id device_id alarm_type status metadata ) }
|
|
483
|
+
let(:values) { [[99, 17, 1, 1, 'foo']] }
|
|
484
|
+
let(:updated_values) { [[99, 17, 1, 2, 'bar']] }
|
|
485
|
+
|
|
486
|
+
macro(:perform_import) do |*opts|
|
|
487
|
+
Alarm.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: [:device_id, :alarm_type], index_predicate: 'status <> 0', columns: [:status] }, validate: false)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
macro(:updated_alarm) { Alarm.find(@alarm.id) }
|
|
491
|
+
|
|
492
|
+
setup do
|
|
493
|
+
Alarm.import columns, values, validate: false
|
|
494
|
+
@alarm = Alarm.find 99
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
context 'supports on duplicate key update for partial indexes' do
|
|
498
|
+
it 'should not update created_at timestamp columns' do
|
|
499
|
+
Timecop.freeze Chronic.parse("5 minutes from now") do
|
|
500
|
+
perform_import
|
|
501
|
+
assert_in_delta @alarm.created_at.to_i, updated_alarm.created_at.to_i, 1
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
it 'should update updated_at timestamp columns' do
|
|
506
|
+
time = Chronic.parse("5 minutes from now")
|
|
507
|
+
Timecop.freeze time do
|
|
508
|
+
perform_import
|
|
509
|
+
assert_in_delta time.to_i, updated_alarm.updated_at.to_i, 1
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
it 'should not update fields not mentioned' do
|
|
514
|
+
perform_import
|
|
515
|
+
assert_equal 'foo', updated_alarm.metadata
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
it 'should update fields mentioned with hash mappings' do
|
|
519
|
+
perform_import
|
|
520
|
+
assert_equal 2, updated_alarm.status
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
context 'with :condition' do
|
|
526
|
+
let(:columns) { %w( id device_id alarm_type status metadata) }
|
|
527
|
+
let(:values) { [[99, 17, 1, 1, 'foo']] }
|
|
528
|
+
let(:updated_values) { [[99, 17, 1, 1, 'bar']] }
|
|
529
|
+
|
|
530
|
+
macro(:perform_import) do |*opts|
|
|
531
|
+
Alarm.import(
|
|
532
|
+
columns,
|
|
533
|
+
updated_values,
|
|
534
|
+
opts.extract_options!.merge(
|
|
535
|
+
on_duplicate_key_update: {
|
|
536
|
+
conflict_target: [:id],
|
|
537
|
+
condition: "alarms.metadata NOT LIKE '%foo%'",
|
|
538
|
+
columns: [:metadata]
|
|
539
|
+
},
|
|
540
|
+
validate: false
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
macro(:updated_alarm) { Alarm.find(@alarm.id) }
|
|
546
|
+
|
|
547
|
+
setup do
|
|
548
|
+
Alarm.import columns, values, validate: false
|
|
549
|
+
@alarm = Alarm.find 99
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
it 'should not update fields not matched' do
|
|
553
|
+
perform_import
|
|
554
|
+
assert_equal 'foo', updated_alarm.metadata
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
154
558
|
context "with :constraint_name" do
|
|
155
559
|
let(:columns) { %w( id title author_name author_email_address parent_id ) }
|
|
156
560
|
let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }
|
|
@@ -170,25 +574,54 @@ def should_support_postgresql_upsert_functionality
|
|
|
170
574
|
should_update_fields_mentioned
|
|
171
575
|
end
|
|
172
576
|
|
|
173
|
-
context "
|
|
577
|
+
context "default to the primary key" do
|
|
174
578
|
let(:columns) { %w( id title author_name author_email_address parent_id ) }
|
|
175
579
|
let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }
|
|
176
580
|
let(:updated_values) { [[100, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
|
|
177
|
-
|
|
178
|
-
macro(:perform_import) do |*opts|
|
|
179
|
-
Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { columns: update_columns }, validate: false)
|
|
180
|
-
end
|
|
581
|
+
let(:update_columns) { [:title, :author_email_address, :parent_id] }
|
|
181
582
|
|
|
182
583
|
setup do
|
|
183
584
|
Topic.import columns, values, validate: false
|
|
184
585
|
@topic = Topic.find 100
|
|
185
586
|
end
|
|
186
587
|
|
|
187
|
-
context "
|
|
188
|
-
|
|
588
|
+
context "with no :conflict_target or :constraint_name" do
|
|
589
|
+
macro(:perform_import) do |*opts|
|
|
590
|
+
Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { columns: update_columns }, validate: false)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
should_support_on_duplicate_key_update
|
|
594
|
+
should_update_fields_mentioned
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
context "with empty value for :conflict_target" do
|
|
598
|
+
macro(:perform_import) do |*opts|
|
|
599
|
+
Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: [], columns: update_columns }, validate: false)
|
|
600
|
+
end
|
|
601
|
+
|
|
189
602
|
should_support_on_duplicate_key_update
|
|
190
603
|
should_update_fields_mentioned
|
|
191
604
|
end
|
|
605
|
+
|
|
606
|
+
context "with empty value for :constraint_name" do
|
|
607
|
+
macro(:perform_import) do |*opts|
|
|
608
|
+
Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { constraint_name: '', columns: update_columns }, validate: false)
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
should_support_on_duplicate_key_update
|
|
612
|
+
should_update_fields_mentioned
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
context "with no :conflict_target or :constraint_name" do
|
|
617
|
+
context "with no primary key" do
|
|
618
|
+
it "raises ArgumentError" do
|
|
619
|
+
error = assert_raises ArgumentError do
|
|
620
|
+
Rule.import Build(3, :rules), on_duplicate_key_update: [:condition_text], validate: false
|
|
621
|
+
end
|
|
622
|
+
assert_match(/Expected :conflict_target or :constraint_name to be specified/, error.message)
|
|
623
|
+
end
|
|
624
|
+
end
|
|
192
625
|
end
|
|
193
626
|
|
|
194
627
|
context "with no :columns" do
|