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.
Files changed (131) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +74 -8
  5. data/Brewfile +3 -1
  6. data/CHANGELOG.md +448 -2
  7. data/Gemfile +26 -19
  8. data/LICENSE +21 -56
  9. data/README.markdown +568 -32
  10. data/Rakefile +5 -1
  11. data/activerecord-import.gemspec +8 -7
  12. data/benchmarks/README +2 -2
  13. data/benchmarks/benchmark.rb +8 -1
  14. data/benchmarks/lib/base.rb +2 -0
  15. data/benchmarks/lib/cli_parser.rb +5 -2
  16. data/benchmarks/lib/float.rb +2 -0
  17. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  18. data/benchmarks/lib/output_to_csv.rb +2 -0
  19. data/benchmarks/lib/output_to_html.rb +4 -2
  20. data/benchmarks/models/test_innodb.rb +2 -0
  21. data/benchmarks/models/test_memory.rb +2 -0
  22. data/benchmarks/models/test_myisam.rb +2 -0
  23. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  24. data/gemfiles/4.2.gemfile +4 -3
  25. data/gemfiles/5.0.gemfile +4 -3
  26. data/gemfiles/5.1.gemfile +4 -0
  27. data/gemfiles/5.2.gemfile +4 -0
  28. data/gemfiles/6.0.gemfile +4 -0
  29. data/gemfiles/6.1.gemfile +4 -0
  30. data/gemfiles/7.0.gemfile +4 -0
  31. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  33. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +8 -0
  35. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  38. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  39. data/lib/activerecord-import/adapters/abstract_adapter.rb +12 -16
  40. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  41. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  42. data/lib/activerecord-import/adapters/mysql_adapter.rb +35 -18
  43. data/lib/activerecord-import/adapters/postgresql_adapter.rb +107 -28
  44. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +141 -18
  45. data/lib/activerecord-import/base.rb +15 -8
  46. data/lib/activerecord-import/import.rb +642 -178
  47. data/lib/activerecord-import/mysql2.rb +2 -0
  48. data/lib/activerecord-import/postgresql.rb +2 -0
  49. data/lib/activerecord-import/sqlite3.rb +2 -0
  50. data/lib/activerecord-import/synchronize.rb +5 -3
  51. data/lib/activerecord-import/value_sets_parser.rb +29 -3
  52. data/lib/activerecord-import/version.rb +3 -1
  53. data/lib/activerecord-import.rb +5 -16
  54. data/test/adapters/jdbcmysql.rb +2 -0
  55. data/test/adapters/jdbcpostgresql.rb +2 -0
  56. data/test/adapters/jdbcsqlite3.rb +3 -0
  57. data/test/adapters/makara_postgis.rb +3 -0
  58. data/test/adapters/mysql2.rb +2 -0
  59. data/test/adapters/mysql2_makara.rb +2 -0
  60. data/test/adapters/mysql2spatial.rb +2 -0
  61. data/test/adapters/postgis.rb +2 -0
  62. data/test/adapters/postgresql.rb +2 -0
  63. data/test/adapters/postgresql_makara.rb +2 -0
  64. data/test/adapters/seamless_database_pool.rb +2 -0
  65. data/test/adapters/spatialite.rb +2 -0
  66. data/test/adapters/sqlite3.rb +2 -0
  67. data/test/{travis → github}/database.yml +7 -1
  68. data/test/import_test.rb +498 -32
  69. data/test/jdbcmysql/import_test.rb +2 -1
  70. data/test/jdbcpostgresql/import_test.rb +2 -1
  71. data/test/jdbcsqlite3/import_test.rb +6 -0
  72. data/test/makara_postgis/import_test.rb +10 -0
  73. data/test/models/account.rb +5 -0
  74. data/test/models/alarm.rb +4 -0
  75. data/test/models/animal.rb +8 -0
  76. data/test/models/bike_maker.rb +9 -0
  77. data/test/models/book.rb +4 -0
  78. data/test/models/car.rb +5 -0
  79. data/test/models/card.rb +5 -0
  80. data/test/models/chapter.rb +2 -0
  81. data/test/models/customer.rb +8 -0
  82. data/test/models/deck.rb +8 -0
  83. data/test/models/dictionary.rb +6 -0
  84. data/test/models/discount.rb +2 -0
  85. data/test/models/end_note.rb +2 -0
  86. data/test/models/group.rb +2 -0
  87. data/test/models/order.rb +8 -0
  88. data/test/models/playing_card.rb +4 -0
  89. data/test/models/promotion.rb +2 -0
  90. data/test/models/question.rb +2 -0
  91. data/test/models/rule.rb +2 -0
  92. data/test/models/tag.rb +7 -0
  93. data/test/models/tag_alias.rb +5 -0
  94. data/test/models/topic.rb +16 -0
  95. data/test/models/user.rb +5 -0
  96. data/test/models/user_token.rb +6 -0
  97. data/test/models/vendor.rb +9 -0
  98. data/test/models/widget.rb +18 -0
  99. data/test/mysql2/import_test.rb +2 -0
  100. data/test/mysql2_makara/import_test.rb +2 -0
  101. data/test/mysqlspatial2/import_test.rb +2 -0
  102. data/test/postgis/import_test.rb +6 -0
  103. data/test/postgresql/import_test.rb +2 -4
  104. data/test/schema/generic_schema.rb +88 -3
  105. data/test/schema/jdbcpostgresql_schema.rb +3 -0
  106. data/test/schema/mysql2_schema.rb +21 -0
  107. data/test/schema/postgis_schema.rb +3 -0
  108. data/test/schema/postgresql_schema.rb +63 -0
  109. data/test/schema/sqlite3_schema.rb +15 -0
  110. data/test/schema/version.rb +2 -0
  111. data/test/sqlite3/import_test.rb +4 -50
  112. data/test/support/active_support/test_case_extensions.rb +8 -1
  113. data/test/support/assertions.rb +2 -0
  114. data/test/support/factories.rb +17 -8
  115. data/test/support/generate.rb +10 -8
  116. data/test/support/mysql/import_examples.rb +17 -3
  117. data/test/support/postgresql/import_examples.rb +442 -9
  118. data/test/support/shared_examples/on_duplicate_key_ignore.rb +45 -0
  119. data/test/support/shared_examples/on_duplicate_key_update.rb +278 -1
  120. data/test/support/shared_examples/recursive_import.rb +137 -12
  121. data/test/support/sqlite3/import_examples.rb +232 -0
  122. data/test/synchronize_test.rb +10 -0
  123. data/test/test_helper.rb +44 -3
  124. data/test/value_sets_bytes_parser_test.rb +15 -2
  125. data/test/value_sets_records_parser_test.rb +2 -0
  126. metadata +74 -22
  127. data/.travis.yml +0 -52
  128. data/gemfiles/3.2.gemfile +0 -3
  129. data/gemfiles/4.0.gemfile +0 -3
  130. data/gemfiles/4.1.gemfile +0 -3
  131. data/test/schema/mysql_schema.rb +0 -16
@@ -1,4 +1,6 @@
1
- FactoryGirl.define do
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(FactoryGirl.attributes_for(: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: FactoryGirl.generate(:book_title), author_name: 'Stephen King')
45
+ book = topic.books.build(title: FactoryBot.generate(:book_title), author_name: 'Stephen King')
42
46
  3.times do
43
- book.chapters.build(title: FactoryGirl.generate(:chapter_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: FactoryGirl.generate(:end_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
@@ -1,29 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ActiveSupport::TestCase
2
- def Build(*args) # rubocop:disable Style/MethodName
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
- factory_girl_args = args.shift || {}
7
+ factory_bot_args = args.shift || {}
6
8
 
7
9
  if n
8
10
  [].tap do |collection|
9
- n.times.each { collection << FactoryGirl.build(factory.to_s.singularize.to_sym, factory_girl_args) }
11
+ n.times.each { collection << FactoryBot.build(factory.to_s.singularize.to_sym, factory_bot_args) }
10
12
  end
11
13
  else
12
- FactoryGirl.build(factory.to_s.singularize.to_sym, factory_girl_args)
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 Style/MethodName
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
- factory_girl_args = args.shift || {}
21
+ factory_bot_args = args.shift || {}
20
22
 
21
23
  if n
22
24
  [].tap do |collection|
23
- n.times.each { collection << FactoryGirl.create(factory.to_s.singularize.to_sym, factory_girl_args) }
25
+ n.times.each { collection << FactoryBot.create(factory.to_s.singularize.to_sym, factory_bot_args) }
24
26
  end
25
27
  else
26
- FactoryGirl.create(factory.to_s.singularize.to_sym, factory_girl_args)
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
- # encoding: UTF-8
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
- # encoding: UTF-8
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 "with no :conflict_target or :constraint_name" do
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 "default to the primary key" do
188
- let(:update_columns) { [:title, :author_email_address, :parent_id] }
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